expanded margin of position spots on gatekeeper; cleaned up #id_tray scripts & styles
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Disco DeDisco
2026-03-29 15:22:00 -04:00
parent 39db59c71a
commit 6c91ec0385
5 changed files with 184 additions and 38 deletions

View File

@@ -71,29 +71,36 @@ var Tray = (function () {
if (_isLandscape()) { if (_isLandscape()) {
// Landscape: the wrap slides on the Y axis. // Landscape: the wrap slides on the Y axis.
// Structure (column-reverse): tray above, handle below. // Structure (column-reverse): tray above, handle below.
// Tray has an explicit CSS height (80vh) so offsetHeight is real. // Wrap height is fixed to gearBtnTop so the handle bottom always
// Closed: wrap top = -(trayH) so tray is above viewport, handle at y=0. // meets the gear button when open. Tray is flex:1 and fills the rest.
// Open: wrap top = gearBtnTop - wrapH so handle bottom = gear btn top. // Open: wrap top = 0 (pinned to viewport top).
// Closed: wrap top = -(gearBtnTop - handleH) = tray fully above viewport.
var gearBtn = document.getElementById('id_gear_btn'); var gearBtn = document.getElementById('id_gear_btn');
var gearBtnTop = window.innerHeight; var gearBtnTop = window.innerHeight;
if (gearBtn) { if (gearBtn) {
gearBtnTop = Math.round(gearBtn.getBoundingClientRect().top); gearBtnTop = Math.round(gearBtn.getBoundingClientRect().top);
} }
var handleH = (_btn && _btn.offsetHeight) || 48; var handleH = (_btn && _btn.offsetHeight) || 48;
var wrapH = (_wrap && _wrap.offsetHeight) || (handleH + 280);
// Pin wrap height so handle bottom = gear btn top when open.
if (_wrap) _wrap.style.height = gearBtnTop + 'px';
// Open: wrap pinned to viewport top.
_minTop = 0;
// Closed: tray hidden above viewport, handle visible at y=0. // Closed: tray hidden above viewport, handle visible at y=0.
_maxTop = -(wrapH - handleH); _maxTop = -(gearBtnTop - handleH);
// Open: handle bottom at gear btn top.
_minTop = gearBtnTop - wrapH;
} else { } else {
// Portrait: slide on X axis. // Portrait: slide on X axis.
var rightPx = parseInt(getComputedStyle(_wrap).right, 10); // Wrap width is pinned to viewportW (JS) so its right edge only
if (isNaN(rightPx)) rightPx = 0; // reaches the viewport boundary when left = 0 (fully open).
// This mirrors landscape: the open edge appears only at the last moment.
// Open: left = 0 → wrap right = viewportW exactly.
// Closed: left = viewportW - handleW → tray fully off-screen right.
var handleW = _btn.offsetWidth || 48; var handleW = _btn.offsetWidth || 48;
if (_wrap) _wrap.style.width = window.innerWidth + 'px';
_minLeft = 0; _minLeft = 0;
_maxLeft = window.innerWidth - rightPx - handleW; _maxLeft = window.innerWidth - handleW;
} }
} }
@@ -322,21 +329,39 @@ var Tray = (function () {
_btn.addEventListener('click', _onBtnClick); _btn.addEventListener('click', _onBtnClick);
window.addEventListener('resize', function () { window.addEventListener('resize', function () {
// Always close on resize: bounds change invalidates current position.
// Cancel any in-flight close animation, then force-close state.
_cancelPendingHide();
_open = false;
if (_btn) _btn.classList.remove('open');
if (_wrap) _wrap.classList.remove('wobble', 'snap', 'tray-dragging');
if (_isLandscape()) { if (_isLandscape()) {
// Ensure tray is visible before measuring bounds. // Ensure tray is visible before measuring bounds.
if (_tray) _tray.style.display = 'grid'; if (_tray) _tray.style.display = 'grid';
if (_wrap) { _wrap.style.left = ''; _wrap.style.bottom = ''; } if (_wrap) { _wrap.style.left = ''; _wrap.style.bottom = ''; _wrap.style.width = ''; }
_computeBounds(); _computeBounds();
_computeCellSize(); _computeCellSize();
if (!_open && _wrap) _wrap.style.top = _maxTop + 'px'; // Snap to closed without transition (resize fires continuously).
if (_wrap) {
_wrap.classList.add('tray-dragging');
_wrap.style.top = _maxTop + 'px';
void _wrap.offsetWidth; // flush reflow so position lands before transition restored
_wrap.classList.remove('tray-dragging');
}
} else { } else {
// Switching to portrait: hide tray if closed. if (_tray) _tray.style.display = 'none';
if (!_open && _tray) _tray.style.display = 'none'; if (_wrap) { _wrap.style.top = ''; _wrap.style.height = ''; }
if (_wrap) _wrap.style.top = '';
_computeBounds(); _computeBounds();
_applyVerticalBounds(); _applyVerticalBounds();
_computeCellSize(); _computeCellSize();
if (!_open && _wrap) _wrap.style.left = _maxLeft + 'px'; // Snap to closed without transition.
if (_wrap) {
_wrap.classList.add('tray-dragging');
_wrap.style.left = _maxLeft + 'px';
void _wrap.offsetWidth; // flush reflow
_wrap.classList.remove('tray-dragging');
}
} }
}); });
} }
@@ -358,8 +383,10 @@ var Tray = (function () {
if (_btn) _btn.classList.remove('open'); if (_btn) _btn.classList.remove('open');
if (_wrap) { if (_wrap) {
_wrap.classList.remove('wobble', 'snap', 'tray-dragging'); _wrap.classList.remove('wobble', 'snap', 'tray-dragging');
_wrap.style.left = ''; _wrap.style.left = '';
_wrap.style.top = ''; _wrap.style.top = '';
_wrap.style.height = '';
_wrap.style.width = '';
} }
if (_onDocMove) { if (_onDocMove) {
document.removeEventListener('pointermove', _onDocMove); document.removeEventListener('pointermove', _onDocMove);

View File

@@ -358,5 +358,68 @@ describe("Tray", () => {
const top = parseInt(wrap.style.top, 10); const top = parseInt(wrap.style.top, 10);
expect(top).toBeLessThan(0); expect(top).toBeLessThan(0);
}); });
// ── resize closes landscape tray ─────────────────────────────── //
describe("resize closes the tray", () => {
it("closes when landscape tray is open", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
it("removes .open from btn on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(btn.classList.contains("open")).toBe(false);
});
it("resets wrap to closed top position on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(parseInt(wrap.style.top, 10)).toBeLessThan(0);
});
it("does not re-open a closed tray on resize", () => {
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
});
});
// ---------------------------------------------------------------------- //
// window resize — portrait //
// ---------------------------------------------------------------------- //
describe("window resize (portrait)", () => {
it("closes the tray when open", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
it("removes .open from btn on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(btn.classList.contains("open")).toBe(false);
});
it("hides the tray panel on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(tray.style.display).toBe("none");
});
it("resets wrap to closed left position on resize", () => {
Tray.open();
expect(wrap.style.left).toBe("0px");
window.dispatchEvent(new Event("resize"));
expect(parseInt(wrap.style.left, 10)).toBeGreaterThan(0);
});
it("does not re-open a closed tray on resize", () => {
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
}); });
}); });

View File

@@ -296,6 +296,10 @@ html:has(.gate-overlay) {
} }
} }
} }
.form-container {
margin-top: 1rem;
}
} }
// Narrow viewport — scale down, 2×3 slot grid (portrait mobile + narrow desktop) // Narrow viewport — scale down, 2×3 slot grid (portrait mobile + narrow desktop)
@@ -698,7 +702,8 @@ $inv-strip: 30px; // visible height of each stacked card after the first
} }
.form-container { .form-container {
h3 { font-size: 0.85rem; margin: 0.25rem 0; } margin-top: 0.75rem;
h3 { font-size: 0.85rem; margin: 0.5rem 0; }
form { gap: 0.35rem; } form { gap: 0.35rem; }

View File

@@ -26,10 +26,11 @@ $handle-r: 1rem;
#id_tray_wrap { #id_tray_wrap {
position: fixed; position: fixed;
// left set by JS: closed = vw - handle; open = 0 // left set by JS: closed = vw - handleW; open = vw - wrapW
// top/bottom set by JS from nav/footer measurements // top/bottom set by JS from nav/footer measurements
// right intentionally absent — wrap has fixed CSS width (handle + tray)
// so the open edge only reaches the viewport boundary when fully open.
top: 0; top: 0;
right: 0;
bottom: 0; bottom: 0;
z-index: 310; z-index: 310;
pointer-events: none; pointer-events: none;
@@ -201,19 +202,6 @@ $handle-r: 1rem;
&.wobble { animation: tray-wobble-landscape 0.45s ease; } &.wobble { animation: tray-wobble-landscape 0.45s ease; }
&.snap { animation: tray-snap-landscape 0.30s ease; } &.snap { animation: tray-snap-landscape 0.30s ease; }
// Landscape: extend upward instead of rightward
&::before {
top: -9999px;
bottom: auto;
right: auto;
left: 0;
width: 100%;
height: 9999px;
border-top: none;
border-bottom: none;
border-left: 2.5rem solid rgba(var(--quaUser), 1);
border-right: 2.5rem solid rgba(var(--quaUser), 1);
}
} }
#id_tray_handle { #id_tray_handle {
@@ -259,8 +247,8 @@ $handle-r: 1rem;
inset -0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 1), // right wall depth inset -0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 1), // right wall depth
inset -0.6rem 0 1.5rem -0.5rem rgba(var(--quaUser), 0.5) // right wall depth (hue) inset -0.6rem 0 1.5rem -0.5rem rgba(var(--quaUser), 0.5) // right wall depth (hue)
; ;
flex: none; // override portrait's flex:1 so height applies flex: 1; // fill wrap height (JS sets wrap height = gearBtnTop)
height: 80vh; // gives JS a real offsetHeight to compute bounds from height: auto;
min-height: unset; min-height: unset;
overflow: hidden; // clip #id_tray_grid to the felt interior overflow: hidden; // clip #id_tray_grid to the felt interior
} }

View File

@@ -358,5 +358,68 @@ describe("Tray", () => {
const top = parseInt(wrap.style.top, 10); const top = parseInt(wrap.style.top, 10);
expect(top).toBeLessThan(0); expect(top).toBeLessThan(0);
}); });
// ── resize closes landscape tray ─────────────────────────────── //
describe("resize closes the tray", () => {
it("closes when landscape tray is open", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
it("removes .open from btn on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(btn.classList.contains("open")).toBe(false);
});
it("resets wrap to closed top position on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(parseInt(wrap.style.top, 10)).toBeLessThan(0);
});
it("does not re-open a closed tray on resize", () => {
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
});
});
// ---------------------------------------------------------------------- //
// window resize — portrait //
// ---------------------------------------------------------------------- //
describe("window resize (portrait)", () => {
it("closes the tray when open", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
it("removes .open from btn on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(btn.classList.contains("open")).toBe(false);
});
it("hides the tray panel on resize", () => {
Tray.open();
window.dispatchEvent(new Event("resize"));
expect(tray.style.display).toBe("none");
});
it("resets wrap to closed left position on resize", () => {
Tray.open();
expect(wrap.style.left).toBe("0px");
window.dispatchEvent(new Event("resize"));
expect(parseInt(wrap.style.left, 10)).toBeGreaterThan(0);
});
it("does not re-open a closed tray on resize", () => {
window.dispatchEvent(new Event("resize"));
expect(Tray.isOpen()).toBe(false);
});
}); });
}); });