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,
}
}