diff --git a/plugin-designs/research-dashboard/research-dashboard.vue b/plugin-designs/research-dashboard/research-dashboard.vue index f0f30fe25..3027aa9f6 100644 --- a/plugin-designs/research-dashboard/research-dashboard.vue +++ b/plugin-designs/research-dashboard/research-dashboard.vue @@ -166,7 +166,7 @@ const { shell, isNarrow, slimRail, slimFacet, mobileView, SEG, tableCollapsed, f // TopBar's "Align Dashboard" button via bus.emit('resetLayout')) snaps back to the exact defaults. // Defaults MUST match the CSS spine below (facet 248, rail 312, table 34%) so an untouched board is // pixel-identical and the responsive slim-rail classes keep working until the user drags. -const { startFacetResize, startRailResize, startTableResize, onResizeKeydown, facetW, railW, tableH, mapH } = useDashboardLayout({ +const { startFacetResize, startRailResize, startTableResize, startShellResize, onResizeKeydown, facetW, railW, tableH, mapH, shellH } = useDashboardLayout({ shellRef: shell, bus, isNarrow, @@ -180,7 +180,11 @@ const { startFacetResize, startRailResize, startTableResize, onResizeKeydown, fa // mapH mirrors the page-scroll CSS fallback 40svh (~360px at a 900px viewport — see // BUG-DASHBOARD-BAND-GROWS-ENORMOUS: 58svh left the default band taller than one viewport; // 40svh is the value that makes chrome(137px) + map(X) + table(X+32px bar) sum to <=900px). - defaults: { facetW: 296, railW: 264, tableH: '34%', mapH: 360 }, + // shellH default 720 = the map+table combined budget that mapH:360 used to imply on its own + // (360 map + 360 table-with-bar) — see useDashboardLayout.js's DEFAULTS comment for the exact + // accounting. Must mirror --rd-shell-h's CSS fallback below for the same reset()-vs-fresh-load + // reason as the other three. + defaults: { facetW: 296, railW: 264, tableH: '34%', mapH: 360, shellH: 720 }, }) // KPI strip — generic formatting per kind (mirrors the original inline strip). @@ -323,6 +327,18 @@ useShadowStyles(` .rd-rz-table{ grid-area:map; align-self:end; justify-self:stretch; height:8px; margin-bottom:-4px; cursor:row-resize; } .rd-rz-facet::after, .rd-rz-rail::after{ top:50%; left:50%; transform:translate(-50%,-50%); width:3px; height:34px; } .rd-rz-table::after{ top:50%; left:50%; transform:translate(-50%,-50%); width:34px; height:3px; } + /* ── SHELL HANDLE (page-scroll mode only) — the boundary between the WHOLE research-dashboard + element and whatever a host stacks below it (BUG-SHELL-HANDLE-BOUND-TO-WRONG-DIVIDER: this is + the control the Driver actually asked for — "resize the height of the page" — which a previous + pass mistakenly bound to .rd-rz-table above instead). Positioned OUTSIDE the grid (the shell is + already position:relative) rather than given a grid-area, because it must span all THREE + columns (facets + map/table + rail) regardless of their widths, and no single grid-area does + that without also claiming cells .rd-rz-table already owns. Pinned to the shell's own bottom + edge — not the table cell's — so it tracks whichever row happens to be last. Grid-mode has no + concept of "the element's own height" (the shell IS the viewport there), so this handle only + ever renders in .rd-page-scroll — see the template's v-show. */ + .rd-rz-shell{ position:absolute; left:0; right:0; bottom:0; height:10px; margin-bottom:-5px; cursor:row-resize; } + .rd-rz-shell::after{ top:50%; left:50%; transform:translate(-50%,-50%); width:48px; height:3px; } .rd-rz:hover::after{ background:var(--rd-accent,#e67e22); } .rd-rz:active{ background:color-mix(in srgb, var(--rd-accent,#e67e22) 18%, transparent); } .rd-rz:active::after{ background:var(--rd-accent,#e67e22); } @@ -357,7 +373,18 @@ useShadowStyles(` is the ORIGINAL defect the 88svh pin was introduced to avoid; giving the map an explicit height keeps it while freeing the table. The table's own overflow is released to visible in this mode, which is the actual fix: with - no internal scrollport under the cursor there is nothing to capture the wheel. */ + no internal scrollport under the cursor there is nothing to capture the wheel. + + ★ --rd-shell-h vs --rd-map-h (BUG-SHELL-HANDLE-BOUND-TO-WRONG-DIVIDER). Two INDEPENDENT + variables, answering two different questions, where there used to be only one: + --rd-shell-h = the ELEMENT's own total height (map + table combined) — the NEW bottom + handle (.rd-rz-shell) is the only thing that writes this. + --rd-map-h = the map's SHARE of that total — the existing map/table divider + (.rd-rz-table) writes this, redistributing within --rd-shell-h without + changing it (useDashboardLayout.js clamps mapH so it can never exceed the + shell budget). Every derived measurement below (table budget, facets/rail + span) now roots in --rd-shell-h so the total stays the single source of + truth and the two handles cannot fight over the same pixels. */ .rd-shell.rd-page-scroll{ height:auto; --table-h:auto; } .rd-shell.rd-page-scroll > [data-region="map"]{ height:var(--rd-map-h, 40svh); overflow:hidden; } .rd-shell.rd-page-scroll > [data-region="table"]{ overflow:visible; } @@ -394,9 +421,27 @@ useShadowStyles(` taller than where the gesture happens). 40svh (~360px at 900px) brings chrome + map(360) + table(392) to ~889px, clearing all three clauses with an 11px margin. The resize handle (useDashboardLayout's mapH drag) still lets a large-monitor user drag it back up afterward — - this only changes what a FIRST LOAD shows. */ + this only changes what a FIRST LOAD shows. + ★ RE-ROOTED IN --rd-shell-h (BUG-SHELL-HANDLE-BOUND-TO-WRONG-DIVIDER). The table's budget used + to be map-h minus the bar, which made the map/table divider ALSO the element's total-height + control (growing the map grew the total, since nothing capped it). Now the table's budget is + the SHELL's total minus whatever the map is currently taking minus the bar — so dragging the + map/table divider (which changes --rd-map-h, never --rd-shell-h) redistributes between the two + rows while their SUM stays exactly --rd-shell-h. Fallback 80svh mirrors useDashboardLayout.js's + shellH default (720px ≈ 2×40svh at 900px) — the map+table combined budget --rd-map-h alone used + to imply, so first paint is unchanged. + ★ ALSO SUBTRACTS --rd-table-foot-h NOW — MEASURED, a real gap this exposed. The table cell has + THREE stacked children (.rd-table-bar, .rd-grid, .rd-foot — the "Σ" summary row, + DashboardDataTable.vue's height:28px), but the old formula only ever budgeted for the bar. That + was invisible before this card: with nothing else depending on map-h + table-cell summing to an + exact number, the extra 28px just meant the table cell was quietly 28px taller than the map row + beside it. It became visible the moment facets/rail needed to span EXACTLY map+table (below) — + without this term they undershot by the footer's height, leaving them 28-30px short of the + table's real bottom edge, i.e. the sidebar grey-gap bug this card is fixing. --rd-table-foot-h + is a var (not a literal) for the same reason --rd-table-bar-h is: DashboardDataTable can correct + it if the footer's height ever changes, without this rule going stale. */ .rd-shell.rd-page-scroll .rd-grid{ - max-height:var(--rd-table-max-h, calc(var(--rd-map-h, 40svh) - var(--rd-table-bar-h, 32px))); } + max-height:var(--rd-table-max-h, calc(var(--rd-shell-h, 80svh) - var(--rd-map-h, 40svh) - var(--rd-table-bar-h, 32px) - var(--rd-table-foot-h, 28px))); } /* ── FACETS/RAIL COLUMNS BOUNDED IN PAGE-SCROLL MODE (BUG-DASHBOARD-BAND-GROWS-ENORMOUS) ────── THE ROOT CAUSE, one level up from the table rule above. .rd-facets/.rd-rail are authored height:100%, which is correct in the FIXED-VIEWPORT grid (rows sum to a real, bounded @@ -449,10 +494,16 @@ useShadowStyles(` already owns) rather than map alone — that is what actually matches "span the map AND the table". --rd-table-max-h already carries the table's real budget (line 399); reusing it here keeps the two in lockstep by construction instead of a second hardcoded number that could drift. - +1px absorbs the 1px grid gap between the two rows this element spans. */ + +1px absorbs the 1px grid gap between the two rows this element spans. + ★ RE-ROOTED IN --rd-shell-h, SAME CARD AS THE TABLE RULE ABOVE. map-h + table-cell(bar + + grid-max-h) now sums to EXACTLY --rd-shell-h by construction (table-max-h is defined as + shell-h - map-h - bar-h, so bar-h + table-max-h = shell-h - map-h, and adding map-h back + cancels it) — so the two-row span the sidebars must cover is just the shell's own total, not a + re-derived sum of its two parts. This also means the sidebars automatically track the NEW + shell handle (.rd-rz-shell) the same tick it moves, with no separate wiring. */ .rd-shell.rd-page-scroll > [data-region="facets"], .rd-shell.rd-page-scroll > [data-region="rail"]{ min-height:0; overflow:hidden; - max-height:calc(var(--rd-map-h, 40svh) + var(--rd-table-max-h, calc(var(--rd-map-h, 40svh) - var(--rd-table-bar-h, 32px))) + 1px); } + max-height:calc(var(--rd-shell-h, 80svh) + 1px); } .rd-shell.rd-narrow{ grid-template-columns:1fr; grid-template-rows:52px auto var(--rd-stats-h,30px) minmax(0,1fr) 46px; grid-template-areas:"topbar" "filters" "stats" "body" "seg"; } @@ -543,16 +594,32 @@ useShadowStyles(` title="Drag or use arrow keys to resize the charts rail" aria-label="Resize charts rail" :aria-valuenow="railW" aria-valuemin="200" aria-valuemax="520" @pointerdown="startRailResize" @keydown="onResizeKeydown('rail', $event)"> - + + +
diff --git a/plugin-designs/research-dashboard/src/composables/useDashboardLayout.js b/plugin-designs/research-dashboard/src/composables/useDashboardLayout.js index 937ff9be7..bf7b13ebf 100644 --- a/plugin-designs/research-dashboard/src/composables/useDashboardLayout.js +++ b/plugin-designs/research-dashboard/src/composables/useDashboardLayout.js @@ -12,6 +12,25 @@ * properties, PERSISTS the chosen layout to localStorage on every drag-end, RESTORES it on init, * and offers reset() to clear the key and snap back to the exact defaults. * + * SHELL vs. TABLE axis (page-scroll mode only — BUG-SHELL-HANDLE-BOUND-TO-WRONG-DIVIDER): + * Two DIFFERENT questions look similar and must stay answered by two DIFFERENT handles. + * "table" axis = the MAP/TABLE SPLIT. In page-scroll mode it redistributes height BETWEEN + * the map and the table; the sum of the two (the element's total height) + * does NOT change. This is the EXISTING map/table divider + * (.rd-rz-table in research-dashboard.vue), unchanged in grid-mode. + * "shell" axis = the ELEMENT'S OWN HEIGHT (page-scroll mode only) — a bar at the shell's + * BOTTOM edge, the boundary between the whole research-dashboard element and + * whatever a host stacks below it (e.g. the people-groups grid in + * dashboard-plus-grid.vue). Dragging it changes --rd-shell-h, the total map+ + * table budget; the map/table SPLIT within that budget is untouched. + * --rd-shell-h is the new independent variable; --rd-map-h is now the map's share of it, and + * the table's share is derived as shellH - mapH (see the CSS in research-dashboard.vue). Before + * this, --rd-map-h alone was BOTH "the map/table split" AND "the element's total height" — one + * knob answering two questions, which is why dragging the map/table divider used to resize the + * whole element as a side effect. Driver: "you improperly bound that control to the separator + * line between the map and the table... that's not the request I was making to resize the + * height of the page." + * * COMPOSITION with the collapse-table fix (do NOT regress): * The shell's DEFAULT rows are `52px auto 42px 1fr var(--table-h,34%)` while the collapsed rule * `.rd-shell.rd-table-collapsed{ grid-template-rows:52px auto 42px 1fr 0 }` comes LATER in source @@ -39,17 +58,18 @@ * @param {import('vue').Ref|boolean=} options.pageScroll when true, the table axis drives * --rd-map-h (px) instead of --table-h — see the PAGE-SCROLL AXIS SWITCH note below. * @param {string=} options.storageKey localStorage key (default 'rd-dashboard-layout') - * @param {{facetW?:number, railW?:number, tableH?:string, mapH?:number}=} options.defaults default track sizes - * @param {{facetW?:[number,number], railW?:[number,number], tableH?:[number,number], mapH?:[number,number]}=} options.clamps - * min/max — facetW/railW/mapH in px, tableH in percent-of-shell-height + * @param {{facetW?:number, railW?:number, tableH?:string, mapH?:number, shellH?:number}=} options.defaults default track sizes + * @param {{facetW?:[number,number], railW?:[number,number], tableH?:[number,number], mapH?:[number,number], shellH?:[number,number]}=} options.clamps + * min/max — facetW/railW/mapH/shellH in px, tableH in percent-of-shell-height * * @returns {{ * facetW: import('vue').Ref, railW: import('vue').Ref, tableH: import('vue').Ref, - * mapH: import('vue').Ref, + * mapH: import('vue').Ref, shellH: import('vue').Ref, * startFacetResize:(e:PointerEvent)=>void, startRailResize:(e:PointerEvent)=>void, - * startTableResize:(e:PointerEvent)=>void, reset:()=>void, applyVars:()=>void, customized:import('vue').Ref, - * stepResize:(axis:'facet'|'rail'|'table', dir:-1|1, toExtreme?:boolean)=>void, - * onResizeKeydown:(axis:'facet'|'rail'|'table', e:KeyboardEvent)=>void + * startTableResize:(e:PointerEvent)=>void, startShellResize:(e:PointerEvent)=>void, + * reset:()=>void, applyVars:()=>void, customized:import('vue').Ref, + * stepResize:(axis:'facet'|'rail'|'table'|'shell', dir:-1|1, toExtreme?:boolean)=>void, + * onResizeKeydown:(axis:'facet'|'rail'|'table'|'shell', e:KeyboardEvent)=>void * }} */ import { ref, unref, onMounted, onBeforeUnmount } from 'vue' @@ -62,8 +82,18 @@ import { ref, unref, onMounted, onBeforeUnmount } from 'vue' // ★ 58svh (522px) was the ORIGINAL default and left the page-scroll band taller than one desktop // viewport (BUG-DASHBOARD-BAND-GROWS-ENORMOUS: chrome 137px + map 522px + table 552px = 1211px // against 900px). 40svh is the value that makes chrome + map(X) + table(X + bar-height) fit <=900px. -const DEFAULTS = { facetW: 296, railW: 264, tableH: '34%', mapH: 360 } -const CLAMPS = { facetW: [160, 420], railW: [200, 520], tableH: [12, 70], mapH: [220, 1400] } +// shellH default (720) is the SUM the old single-knob mapH(360) used to imply — map 360 + table +// budget 360 (328 grid + 32 bar) — so first paint is pixel-identical to before this axis split; +// only DRAGGING behaves differently now (see the SHELL vs. TABLE axis note above). +const DEFAULTS = { facetW: 296, railW: 264, tableH: '34%', mapH: 360, shellH: 720 } +const CLAMPS = { facetW: [160, 420], railW: [200, 520], tableH: [12, 70], mapH: [220, 1400], shellH: [400, 2600] } + +// Mirrors the CSS fallback --rd-table-bar-h (research-dashboard.vue's .rd-table-bar budget calc) — +// the table's header bar (count + Export CSV) sits ABOVE its scrollport and is not itself +// resizable, so the map/table split must always leave it at least this much room, or the divider +// could drag the table's usable grid to zero while its outer cell still looks non-empty. +const TABLE_BAR_H = 32 +const TABLE_MIN_H = 120 // grid's own floor below the bar — a table smaller than this is unusable const clamp = (v, [lo, hi]) => Math.min(hi, Math.max(lo, v)) const pctNum = (v, fallback) => { @@ -87,10 +117,14 @@ export function useDashboardLayout(options = {}) { const railW = ref(defaults.railW) // px const tableH = ref(defaults.tableH) // string, e.g. '34%' const mapH = ref(defaults.mapH) // px — only meaningful/applied in page-scroll mode + const shellH = ref(defaults.shellH) // px — page-scroll ELEMENT total height (map+table budget) const customized = ref(false) // has the user (or a restored layout) diverged from CSS defaults? const mapHCustomized = ref(false) // page-scroll's own inline var is written independently of // customized (below), since an untouched grid-mode drag // must never suddenly pin --rd-map-h and vice versa. + const shellHCustomized = ref(false) // same independence, one level up: the shell handle is a + // separate drag from the map/table split and must not fire + // just because the OTHER page-scroll var was touched. const narrow = () => !!unref(isNarrow) const inPageScroll = () => !!unref(pageScroll) @@ -111,6 +145,9 @@ export function useDashboardLayout(options = {}) { if (mapHCustomized.value) { el.style.setProperty('--rd-map-h', mapH.value + 'px') } + if (shellHCustomized.value) { + el.style.setProperty('--rd-shell-h', shellH.value + 'px') + } } function clearVars() { const el = shellRef && shellRef.value @@ -119,13 +156,14 @@ export function useDashboardLayout(options = {}) { el.style.removeProperty('--rail-w') el.style.removeProperty('--table-h') el.style.removeProperty('--rd-map-h') + el.style.removeProperty('--rd-shell-h') } // ── persistence ── function persist() { try { localStorage.setItem(storageKey, JSON.stringify({ - facetW: facetW.value, railW: railW.value, tableH: tableH.value, mapH: mapH.value, + facetW: facetW.value, railW: railW.value, tableH: tableH.value, mapH: mapH.value, shellH: shellH.value, })) } catch (_) { /* storage blocked / SSR — layout still works this session */ } } @@ -138,6 +176,9 @@ export function useDashboardLayout(options = {}) { if (Number.isFinite(s.railW)) railW.value = clamp(s.railW, clamps.railW) if (s.tableH != null) tableH.value = clamp(pctNum(s.tableH, pctNum(defaults.tableH, 34)), clamps.tableH) + '%' customized.value = true + // shellH restores BEFORE mapH so the mapH clamp below (which bounds map to <= shellH, see + // startResize's isTablePageScroll branch) has the right ceiling on first paint too. + if (Number.isFinite(s.shellH)) { shellH.value = clamp(s.shellH, clamps.shellH); shellHCustomized.value = true } if (Number.isFinite(s.mapH)) { mapH.value = clamp(s.mapH, clamps.mapH); mapHCustomized.value = true } return true } catch (_) { return false } @@ -150,8 +191,10 @@ export function useDashboardLayout(options = {}) { railW.value = defaults.railW tableH.value = defaults.tableH mapH.value = defaults.mapH + shellH.value = defaults.shellH customized.value = false mapHCustomized.value = false + shellHCustomized.value = false clearVars() } @@ -201,13 +244,23 @@ export function useDashboardLayout(options = {}) { // frame. The fix is to drive --rd-map-h directly, from the MAP ELEMENT's own top edge // (not the unbounded shell rect), so growing the map is a plain function of where the // pointer is relative to where the map starts. + // + // ★ THE map/table SPLIT vs. THE shell's OWN HEIGHT (BUG-SHELL-HANDLE-BOUND-TO-WRONG-DIVIDER) + // — this handle must move height BETWEEN map and table, never grow the total. mapH is now + // clamped to leave the table at least TABLE_BAR_H + TABLE_MIN_H of the FIXED shellH budget, + // so dragging this divider can only redistribute the two, exactly like resizing an internal + // pane split rather than the window itself. const isTablePageScroll = axis === 'table' && inPageScroll() + const isShellResize = axis === 'shell' && inPageScroll() let startMapTop = 0 + let mapHCeiling = clamps.mapH[1] if (isTablePageScroll) { const mapEl = el.querySelector('[data-region="map"]') const mapRect = mapEl ? mapEl.getBoundingClientRect() : null startMapTop = mapRect ? mapRect.top : (e.clientY - mapH.value) + mapHCeiling = Math.max(clamps.mapH[0], shellH.value - (TABLE_BAR_H + TABLE_MIN_H)) } + const startShellTop = isShellResize ? rect.top : 0 try { handleEl && handleEl.setPointerCapture && handleEl.setPointerCapture(e.pointerId) } catch (_) {} // ── A11Y / PHOTOSENSITIVITY (BUG-RESIZE-STROBE): mark the drag as LIVE ── @@ -244,13 +297,27 @@ export function useDashboardLayout(options = {}) { // distance from the map's (fixed) top to the cursor — a direct 1:1 follow, exactly like // dragging the corner of a window. Re-derived fresh every frame from startMapTop (not // summed from a delta), so it can never drift from the cursor across a long drag. - mapH.value = Math.round(clamp(ev.clientY - startMapTop, clamps.mapH)) + // Clamped to mapHCeiling (not the raw clamps.mapH max) — the ceiling reserves the table's + // bar + a usable minimum out of the FIXED shellH budget, so this drag only ever moves + // height between map and table and can never grow the element's total height. That is + // the shell axis's job (see isShellResize below), not this one's. + mapH.value = Math.round(clamp(ev.clientY - startMapTop, [clamps.mapH[0], mapHCeiling])) mapHCustomized.value = true } else if (axis === 'table') { // map/table divider → dragging up grows the table (height measured from shell bottom) const pct = ((rect.bottom - ev.clientY) / rect.height) * 100 tableH.value = Math.round(clamp(pct, clamps.tableH)) + '%' customized.value = true + } else if (isShellResize) { + // The NEW shell handle — spans the whole element's bottom edge (research-dashboard.vue's + // .rd-rz-shell), the boundary with whatever a host stacks below (e.g. the people-groups + // grid in dashboard-plus-grid.vue). Same fixed-top / 1:1-follow pattern as the map handle + // above, rooted at the SHELL's own top instead of the map's: the pointer's Y position IS + // the shell's new bottom edge. This is the ONLY axis that changes --rd-shell-h, i.e. the + // only handle that changes the element's total height — the map/table divider above must + // never do this (see its own comment). + shellH.value = Math.round(clamp(ev.clientY - startShellTop, clamps.shellH)) + shellHCustomized.value = true } applyVars() } @@ -277,13 +344,15 @@ export function useDashboardLayout(options = {}) { const startFacetResize = (e) => startResize('facet', e) const startRailResize = (e) => startResize('rail', e) const startTableResize = (e) => startResize('table', e) + const startShellResize = (e) => startResize('shell', e) // ── keyboard step-resize (a11y: role="separator" needs an operable equivalent to the drag) ── // Reuses the SAME refs/clamps/persist/applyVars as the pointer path — no parallel math. One // discrete step per keypress (no rAF coalescing needed; a keydown can't fire faster than a frame). // `table` axis mirrors startResize's own page-scroll branch: in page-scroll mode it steps mapH - // (the dashboard band height), otherwise it steps the grid-mode tableH percentage. - const STEP = { facet: 16, rail: 16, table: 24, mapH: 24 } // px per arrow-key press (tableH in %) + // (the map/table split within the FIXED shellH budget), otherwise it steps the grid-mode tableH + // percentage. `shell` steps shellH directly — the only axis that changes the element's total height. + const STEP = { facet: 16, rail: 16, table: 24, mapH: 24, shellH: 24 } // px per arrow-key press (tableH in %) // `toExtreme` bypasses the per-step delta entirely and clamps straight to lo/hi — Home/End jump, // not a long walk of individual steps (which would also mean 200 localStorage writes per keypress). function stepResize(axis, dir, toExtreme) { @@ -299,25 +368,33 @@ export function useDashboardLayout(options = {}) { : Math.round(clamp(railW.value - dir * STEP.rail, clamps.railW)) customized.value = true } else if (axis === 'table' && inPageScroll()) { - mapH.value = toExtreme ? (dir < 0 ? clamps.mapH[0] : clamps.mapH[1]) - : Math.round(clamp(mapH.value + dir * STEP.mapH, clamps.mapH)) + // Same ceiling as the pointer path (startResize's mapHCeiling): reserve the table's bar + + // usable minimum out of the CURRENT shellH so a keyboard drag can redistribute but never + // grow the total either. + const ceiling = Math.max(clamps.mapH[0], shellH.value - (TABLE_BAR_H + TABLE_MIN_H)) + mapH.value = toExtreme ? (dir < 0 ? clamps.mapH[0] : ceiling) + : Math.round(clamp(mapH.value + dir * STEP.mapH, [clamps.mapH[0], ceiling])) mapHCustomized.value = true } else if (axis === 'table') { const pct = toExtreme ? (dir < 0 ? clamps.tableH[0] : clamps.tableH[1]) : clamp(pctNum(tableH.value, pctNum(defaults.tableH, 34)) - dir * STEP.table / 4, clamps.tableH) tableH.value = Math.round(pct) + '%' customized.value = true + } else if (axis === 'shell' && inPageScroll()) { + shellH.value = toExtreme ? (dir < 0 ? clamps.shellH[0] : clamps.shellH[1]) + : Math.round(clamp(shellH.value + dir * STEP.shellH, clamps.shellH)) + shellHCustomized.value = true } applyVars() persist() } // ArrowUp/Left = grow toward the start edge, ArrowDown/Right = grow toward the end edge, matching - // each handle's drag SENSE (facet/rail are col-resize, table/map is row-resize). Home/End jump to - // the axis's clamp extremes in one write (not a walk of individual steps). + // each handle's drag SENSE (facet/rail are col-resize, table/map/shell are row-resize). Home/End + // jump to the axis's clamp extremes in one write (not a walk of individual steps). function onResizeKeydown(axis, e) { const K = e.key let dir = 0 - if (axis === 'table') { if (K === 'ArrowUp') dir = -1; else if (K === 'ArrowDown') dir = 1 } + 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 } if (dir) { e.preventDefault(); stepResize(axis, dir); return } if (K === 'Home' || K === 'End') { @@ -340,8 +417,8 @@ export function useDashboardLayout(options = {}) { }) return { - facetW, railW, tableH, mapH, customized, resizing, - startFacetResize, startRailResize, startTableResize, + facetW, railW, tableH, mapH, shellH, customized, resizing, + startFacetResize, startRailResize, startTableResize, startShellResize, startResize, reset, applyVars, stepResize, onResizeKeydown, } }