The layout drag was LTR-only, and my own migration was too eager
go-int found both by reading the kit before importing it. Both are in landed code and both reach Big Picture in Arabic, not only a new consumer. THE DRAG HAD NO READING DIRECTION. facetW and railW were computed from (clientX - startX), written once, in LTR. Under dir=rtl the grid mirrors — the facet rail sits on the RIGHT — so dragging its handle toward the map moves clientX the other way, and in Arabic the facet handle shrank the CHARTS rail. Both handles fight and neither does what the cursor says. The delta now carries a sign read from the computed direction per drag, not cached: a host can flip dir between gestures, because our own composites set it on the custom-element host from the runtime locale. The ARROW KEYS take the same sign. If only the pointer mirrored, the two input methods would disagree about which way widens — and aria-valuenow reports the same ref for both, so the announced value would move opposite to the key for someone who cannot see the drag. AND THE LEGACY READ-THROUGH WAS MINE, FROM KIT LANDING 2. I added it so namespacing would not discard a reader's saved layout, and applied it unconditionally. So a caller passing its OWN storageKey — which is the entire point of namespacing — still inherited Big Picture's rail widths on first load. The migration written to protect one dashboard's layout was handing that layout to every other dashboard. It is gated on the default key now, and the default is a named constant rather than a repeated literal, because a fallback comparing against a copy of its own default stops matching the day someone bumps the version suffix. check:layout-key-namespaced grows arms D and E. D asserts BOTH widths take the sign and that the arrows share it — mirroring one and not the other is worse than mirroring neither, because then the handles disagree with each other. E asserts the legacy read is gated. Comments are stripped before both assertions, for the reason that is now written down four times over. Source only. STUDIO: needs a rebuild of research-dashboard and dashboard-plus-grid. NEEDS EYES: the drag is a gesture, so this wants rega in ar — drag the facet handle toward the map and confirm the FACET rail follows the cursor and the charts rail does not move, then the same with ArrowLeft/ArrowRight.
This commit is contained in:
parent
c2ab779623
commit
273785b2e6
2 changed files with 75 additions and 5 deletions
|
|
@ -90,6 +90,10 @@ import { ref, unref, onMounted, onBeforeUnmount } from 'vue'
|
|||
// existing layout survives the change; nothing writes to it. Delete it once no reader can plausibly
|
||||
// still be carrying one.
|
||||
const LEGACY_SHARED_KEY = 'rd-dashboard-layout'
|
||||
// The default key, named once so the legacy read-through can ask "am I the default?" instead of
|
||||
// re-typing the literal — a fallback comparing against a copy of its own default is a fallback that
|
||||
// silently stops matching the day someone bumps the version suffix.
|
||||
const DEFAULT_STORAGE_KEY = 'rd-dashboard-layout-v6'
|
||||
|
||||
// Tree rail WIDER than the charts rail — see the rail-balance note in research-dashboard.vue.
|
||||
// These must stay in lockstep with the CSS `:host` defaults, since reset() snaps back to them.
|
||||
|
|
@ -170,7 +174,7 @@ export function useDashboardLayout(options = {}) {
|
|||
// in the commit that first trips over it.
|
||||
// The caller passes its own id (the shell uses config.id). The default keeps a caller that
|
||||
// passes nothing working, and still namespaces it away from any caller that does.
|
||||
storageKey = 'rd-dashboard-layout-v6',
|
||||
storageKey = DEFAULT_STORAGE_KEY,
|
||||
} = options
|
||||
|
||||
const defaults = { ...DEFAULTS, ...(options.defaults || {}) }
|
||||
|
|
@ -271,7 +275,14 @@ export function useDashboardLayout(options = {}) {
|
|||
// every reader's saved layout. Read-only and never written back: the new key is written on
|
||||
// the next drag-end, and the old value is simply left where it is rather than migrated and
|
||||
// deleted — a migration that also deletes is one that cannot be rolled back.
|
||||
const raw = localStorage.getItem(storageKey) || localStorage.getItem(LEGACY_SHARED_KEY)
|
||||
// ★ ONLY FOR THE DEFAULT KEY (go-int, 2026-09-27) — and this was my own regression from kit
|
||||
// landing 2. I added the read-through so namespacing would not discard a reader's saved
|
||||
// layout, and applied it unconditionally: so a caller that passes its OWN storageKey, which
|
||||
// is the entire point of namespacing, still inherited Big Picture's rail widths on first
|
||||
// load. The migration meant to protect one dashboard's layout was handing it to every other
|
||||
// dashboard. A fallback that ignores the key it is falling back FROM is not a fallback.
|
||||
const legacyOk = storageKey === DEFAULT_STORAGE_KEY
|
||||
const raw = localStorage.getItem(storageKey) || (legacyOk ? localStorage.getItem(LEGACY_SHARED_KEY) : null)
|
||||
if (!raw) return false
|
||||
const s = JSON.parse(raw)
|
||||
if (Number.isFinite(s.facetW)) facetW.value = clamp(s.facetW, clamps.facetW)
|
||||
|
|
@ -333,6 +344,18 @@ export function useDashboardLayout(options = {}) {
|
|||
const startX = e.clientX
|
||||
const startFacet = facetW.value
|
||||
const startRail = railW.value
|
||||
// ★ THE X DELTA HAS A READING DIRECTION (go-int, 2026-09-27). Under dir=rtl the grid mirrors —
|
||||
// the facet rail sits on the RIGHT — so dragging its handle toward the map INCREASES clientX
|
||||
// where in LTR it decreases it. The maths below was written once, in LTR, and the result in
|
||||
// Arabic is that dragging the facet handle shrinks the CHARTS rail: both handles fight, and
|
||||
// neither does what the cursor says. This reaches Big Picture in ar today, not only go-int.
|
||||
// Read per drag rather than cached: a host can flip dir between gestures (our own composites
|
||||
// set it on the custom-element host from the runtime locale), and a cached sign would be wrong
|
||||
// for the rest of the session.
|
||||
const rtl = (() => {
|
||||
try { return getComputedStyle(unref(shellRef)).direction === 'rtl' } catch (_) { return false }
|
||||
})()
|
||||
const xSign = rtl ? -1 : 1
|
||||
const handleEl = e.currentTarget
|
||||
// ── PAGE-SCROLL AXIS SWITCH (BUG-TABLE-RESIZE-HANDLE-DOES-NOTHING) ──────────────────────
|
||||
// In page-scroll mode (research-dashboard.vue's pageScroll / dashboard-plus-grid.vue) the
|
||||
|
|
@ -405,11 +428,11 @@ export function useDashboardLayout(options = {}) {
|
|||
pending = null
|
||||
if (axis === 'facet') {
|
||||
// left facet edge → dragging right widens the facet column
|
||||
facetW.value = Math.round(clamp(startFacet + (ev.clientX - startX), clamps.facetW))
|
||||
facetW.value = Math.round(clamp(startFacet + xSign * (ev.clientX - startX), clamps.facetW))
|
||||
customized.value = true
|
||||
} else if (axis === 'rail') {
|
||||
// right rail's LEFT edge → dragging left widens the rail column
|
||||
railW.value = Math.round(clamp(startRail - (ev.clientX - startX), clamps.railW))
|
||||
railW.value = Math.round(clamp(startRail - xSign * (ev.clientX - startX), clamps.railW))
|
||||
customized.value = true
|
||||
} else if (isTablePageScroll) {
|
||||
// page-scroll: the handle sits pinned to the MAP's own bottom edge (.rd-rz-table is
|
||||
|
|
@ -519,7 +542,16 @@ export function useDashboardLayout(options = {}) {
|
|||
const K = e.key
|
||||
let dir = 0
|
||||
if (axis === 'table' || axis === 'shell') { if (K === 'ArrowUp') dir = -1; else if (K === 'ArrowDown') dir = 1 }
|
||||
else { if (K === 'ArrowLeft') dir = -1; else if (K === 'ArrowRight') dir = 1 }
|
||||
else {
|
||||
// ★ AND THE ARROWS FOLLOW THE SAME SIGN (go-int). The pointer path now mirrors under rtl; if
|
||||
// the keyboard did not, the two input methods would disagree about which way widens — and a
|
||||
// keyboard user in Arabic would find the arrows inverted relative to the drag they just
|
||||
// watched. aria-valuenow reports the same ref for both, so a disagreement here would also
|
||||
// make the announced value move the wrong way.
|
||||
let kSign = 1
|
||||
try { if (getComputedStyle(unref(shellRef)).direction === 'rtl') kSign = -1 } catch (_) { /* no element */ }
|
||||
if (K === 'ArrowLeft') dir = -1 * kSign; else if (K === 'ArrowRight') dir = 1 * kSign
|
||||
}
|
||||
if (dir) { e.preventDefault(); stepResize(axis, dir); return }
|
||||
if (K === 'Home' || K === 'End') {
|
||||
e.preventDefault()
|
||||
|
|
|
|||
|
|
@ -81,6 +81,44 @@ if (!existsSync(ENGINE)) { console.log(`${TAG}: SKIP — no ${ENGINE.replace(ROO
|
|||
}
|
||||
}
|
||||
|
||||
// ── D. the drag mirrors under RTL, and the keyboard agrees with it ───────────────────────────
|
||||
// go-int, 2026-09-27: the x delta was computed once, in LTR. Under dir=rtl the grid mirrors — the
|
||||
// facet rail sits on the RIGHT — so dragging its handle toward the map moves clientX the other way,
|
||||
// and in Arabic the facet handle shrank the CHARTS rail. It reaches Big Picture in ar, not only a
|
||||
// new consumer. The keyboard path is asserted too: if only the pointer mirrored, the two input
|
||||
// methods would disagree about which way widens, and aria-valuenow would announce the wrong
|
||||
// direction to someone who cannot see the drag.
|
||||
{
|
||||
const src = readFileSync(ENGINE, 'utf8')
|
||||
const code = src.split('\n').filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n')
|
||||
if (!/direction === 'rtl'/.test(code)) {
|
||||
fails.push('D useDashboardLayout never reads the computed direction, so the drag cannot mirror: in ar the facet handle moves the wrong rail.')
|
||||
} else {
|
||||
if (!/startFacet \+ xSign \* \(ev\.clientX - startX\)/.test(code) || !/startRail - xSign \* \(ev\.clientX - startX\)/.test(code)) {
|
||||
fails.push('D the pointer drag does not apply the direction sign to BOTH widths — mirroring one and not the other is worse than mirroring neither, because the two handles then disagree.')
|
||||
}
|
||||
if (!/kSign/.test(code)) {
|
||||
fails.push('D the arrow keys do not follow the direction sign, so keyboard and pointer disagree about which way widens under rtl.')
|
||||
}
|
||||
if (!fails.some((f) => f.startsWith('D'))) notes.push('drag: mirrors under rtl, pointer and arrows share one sign')
|
||||
}
|
||||
}
|
||||
|
||||
// ── E. the legacy read-through applies ONLY to the default key ───────────────────────────────
|
||||
// My own regression from kit landing 2: I added the read-through so namespacing would not discard a
|
||||
// saved layout, and applied it unconditionally — so a caller passing its OWN key, which is the whole
|
||||
// point of namespacing, inherited Big Picture's widths. The migration meant to protect one
|
||||
// dashboard's layout was handing it to every other dashboard.
|
||||
{
|
||||
const src = readFileSync(ENGINE, 'utf8')
|
||||
const code = src.split('\n').filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n')
|
||||
if (/getItem\(storageKey\) \|\| localStorage\.getItem\(LEGACY_SHARED_KEY\)/.test(code)) {
|
||||
fails.push('E the legacy key is read even when the caller passed its own storageKey, so a second dashboard inherits the first one\'s layout. Gate it on storageKey === DEFAULT_STORAGE_KEY.')
|
||||
} else if (!/storageKey === DEFAULT_STORAGE_KEY/.test(code)) {
|
||||
fails.push('E the legacy read-through is not gated on the default key — either it is gone (a reader loses their layout) or it is gated on something this gate cannot see.')
|
||||
} else notes.push('legacy read-through: only for the default key')
|
||||
}
|
||||
|
||||
for (const n of notes) console.log(' · ' + n)
|
||||
if (fails.length) {
|
||||
console.error(`\n${TAG}: FAIL`)
|
||||
|
|
|
|||
Loading…
Reference in a new issue