new landscape styling & scripting for gameroom #id_tray apparatus, & some overall scripting & styling like wobble on click-to-close; new --undUser & --duoUser rootvars universally the table felt values; many new Jasmine tests to handle tray functionality
This commit is contained in:
@@ -266,7 +266,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// Container: fill centre, compensate for fixed sidebars on both sides
|
||||
// Container: fill center, compensate for fixed sidebars on both sides
|
||||
body .container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -284,7 +284,7 @@ body {
|
||||
margin: 0 0 0.25rem;
|
||||
letter-spacing: 0.4em;
|
||||
text-align: center;
|
||||
text-align-last: center;
|
||||
text-align-last: left;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,26 +363,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// @media (min-width: 1024px) and (max-height: 700px) {
|
||||
// body .container .navbar {
|
||||
// padding: 0.5rem 0;
|
||||
|
||||
// .navbar-brand h1 {
|
||||
// font-size: 1.4rem;
|
||||
// }
|
||||
// }
|
||||
|
||||
// #id_footer {
|
||||
// height: 3.5rem;
|
||||
// padding: 0.7rem 1rem;
|
||||
// gap: 0.35rem;
|
||||
|
||||
// #id_footer_nav a {
|
||||
// font-size: 1.2rem;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#id_footer {
|
||||
flex-shrink: 0;
|
||||
height: 6rem;
|
||||
|
||||
@@ -371,9 +371,9 @@ $seat-r-y: round($seat-r * 0.5); // 65px
|
||||
width: 160px;
|
||||
height: 185px;
|
||||
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
||||
background: rgba(var(--priUser), 0.8);
|
||||
background: rgba(var(--duoUser), 1);
|
||||
// box-shadow is clipped by clip-path; use filter instead
|
||||
filter: drop-shadow(0 0 8px rgba(var(--terUser), 0.25));
|
||||
filter: drop-shadow(0 0 8px rgba(var(--duoUser), 1));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -678,6 +678,7 @@ $inv-strip: 30px; // visible height of each stacked card after the first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ─── Significator deck (SIG_SELECT phase) ──────────────────────────────────
|
||||
@@ -781,7 +782,8 @@ $handle-r: 1rem;
|
||||
transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.tray-dragging { transition: none; }
|
||||
&.wobble { animation: tray-wobble 0.45s ease; }
|
||||
&.wobble { animation: tray-wobble 0.45s ease; }
|
||||
&.snap { animation: tray-snap 0.30s ease; }
|
||||
}
|
||||
|
||||
#id_tray_handle {
|
||||
@@ -865,6 +867,15 @@ $handle-r: 1rem;
|
||||
80% { transform: translateX(3px); }
|
||||
}
|
||||
|
||||
// Inverted wobble — handle overshoots past the wall on close, then bounces back.
|
||||
@keyframes tray-snap {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(8px); }
|
||||
40% { transform: translateX(-6px); }
|
||||
60% { transform: translateX(5px); }
|
||||
80% { transform: translateX(-3px); }
|
||||
}
|
||||
|
||||
#id_tray {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -872,10 +883,10 @@ $handle-r: 1rem;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
z-index: 1; // above #id_tray_grip pseudo-elements
|
||||
background: rgba(var(--secUser), 1);
|
||||
border-left:2.5rem solid rgba(var(--terUser), 1);
|
||||
border-top: 2.5rem solid rgba(var(--terUser), 1);
|
||||
border-bottom: 2.5rem solid rgba(var(--terUser), 1);
|
||||
background: rgba(var(--duoUser), 1);
|
||||
border-left:2.5rem solid rgba(var(--quaUser), 1);
|
||||
border-top: 2.5rem solid rgba(var(--quaUser), 1);
|
||||
border-bottom: 2.5rem solid rgba(var(--quaUser), 1);
|
||||
box-shadow:
|
||||
-0.25rem 0 0.5rem rgba(0, 0, 0, 0.75),
|
||||
inset 0 0 0 0.12rem rgba(255, 255, 255, 0.12), // bright bevel ring at wall edge
|
||||
@@ -884,6 +895,98 @@ $handle-r: 1rem;
|
||||
inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 0.3) // bottom wall depth
|
||||
;
|
||||
overflow-y: auto;
|
||||
max-height: 85vh; // cap on very tall portrait screens
|
||||
// scrollbar-width: thin;
|
||||
// scrollbar-color: rgba(var(--terUser), 0.3) transparent;
|
||||
}
|
||||
|
||||
// ─── Tray: landscape reorientation ─────────────────────────────────────────
|
||||
//
|
||||
// Must come AFTER the portrait tray rules above to win the cascade
|
||||
// (same specificity — later declaration wins).
|
||||
//
|
||||
// In landscape the tray slides DOWN from the top instead of in from the right.
|
||||
// Structure (column-reverse): tray panel above, handle below.
|
||||
// JS controls style.top for the Y-axis slide:
|
||||
// Closed: top = -(trayH) → only handle visible at y = 0
|
||||
// Open: top = gearBtnTop - wrapH → handle bottom at gear btn top
|
||||
//
|
||||
// The wrap fits horizontally between the fixed left-nav and right-footer sidebars.
|
||||
|
||||
@media (orientation: landscape) {
|
||||
$sidebar-w: 4rem;
|
||||
$tray-landscape-max-w: 960px; // cap tray width on very wide screens
|
||||
|
||||
#id_tray_wrap {
|
||||
flex-direction: column-reverse; // tray panel above, handle below
|
||||
left: $sidebar-w;
|
||||
right: $sidebar-w;
|
||||
top: auto; // JS controls style.top for the Y-axis slide
|
||||
bottom: auto;
|
||||
transition: top 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.tray-dragging { transition: none; }
|
||||
&.wobble { animation: tray-wobble-landscape 0.45s ease; }
|
||||
&.snap { animation: tray-snap-landscape 0.30s ease; }
|
||||
}
|
||||
|
||||
#id_tray_handle {
|
||||
width: auto; // full width of wrap
|
||||
height: 48px; // $handle-exposed — same exposed dimension as portrait
|
||||
}
|
||||
|
||||
#id_tray_grip {
|
||||
// Rotate 90°: centred horizontally, extends vertically.
|
||||
// bottom mirrors portrait's left: grip starts at handle centre and extends
|
||||
// toward the tray (upward in column-reverse layout).
|
||||
bottom: calc(48px / 2 - 0.125rem); // $handle-exposed / 2 from handle bottom
|
||||
top: auto;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 72px; // $handle-rect-h — narrow visible dimension
|
||||
height: 10000px; // $handle-rect-w — extends upward into tray area
|
||||
}
|
||||
|
||||
#id_tray {
|
||||
// Borders: left/right/bottom are visible walls; top edge is open.
|
||||
// Bottom faces the handle (same logic as portrait's left border facing handle).
|
||||
border-left: 2.5rem solid rgba(var(--quaUser), 1);
|
||||
border-right: 2.5rem solid rgba(var(--quaUser), 1);
|
||||
border-bottom: 2.5rem solid rgba(var(--quaUser), 1);
|
||||
border-top: none;
|
||||
|
||||
margin-left: 0; // portrait horizontal gap no longer needed
|
||||
margin-bottom: 0.5rem; // gap between tray bottom and handle top
|
||||
|
||||
// Cap width on ultra-wide screens; center within the handle shelf.
|
||||
width: 100%;
|
||||
max-width: $tray-landscape-max-w;
|
||||
align-self: center;
|
||||
|
||||
box-shadow:
|
||||
0 0.25rem 0.5rem rgba(0, 0, 0, 0.75), // outer shadow (downward, below tray toward handle)
|
||||
inset 0 0 0 0.12rem rgba(255, 255, 255, 0.12), // bright bevel ring
|
||||
inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 0.45), // bottom wall depth (inward from bottom border)
|
||||
inset 0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 0.3), // left wall depth
|
||||
inset -0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 0.3) // right wall depth
|
||||
;
|
||||
min-height: 2000px; // give tray real height so JS offsetHeight > 0
|
||||
}
|
||||
|
||||
@keyframes tray-wobble-landscape {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
20% { transform: translateY(-8px); }
|
||||
40% { transform: translateY(6px); }
|
||||
60% { transform: translateY(-5px); }
|
||||
80% { transform: translateY(3px); }
|
||||
}
|
||||
|
||||
// Inverted wobble — wrap overshoots upward on close, then bounces back.
|
||||
@keyframes tray-snap-landscape {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
20% { transform: translateY(8px); }
|
||||
40% { transform: translateY(-6px); }
|
||||
60% { transform: translateY(5px); }
|
||||
80% { transform: translateY(-3px); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,9 +199,9 @@
|
||||
--secPmm: 150, 120, 182;
|
||||
--terPmm: 112, 79, 146;
|
||||
// forest
|
||||
--priFor: 190, 209, 170;
|
||||
--secFor: 152, 182, 120;
|
||||
--terFor: 114, 146, 79;
|
||||
--priFor: 114, 146, 79;
|
||||
--secFor: 94, 124, 61;
|
||||
--terFor: 74, 102, 43;
|
||||
|
||||
/* Technoman Palette */
|
||||
// carbon steel
|
||||
@@ -301,7 +301,11 @@
|
||||
--octClh: 26, 51, 105;
|
||||
// • pure (rare)
|
||||
--ninClh: 192, 77, 1;
|
||||
--decClh: 255, 174, 0;
|
||||
--decClh: 255, 174, 0;
|
||||
|
||||
// Felt values
|
||||
--undUser: var(--priFor);
|
||||
--duoUser: var(--terFor);
|
||||
}
|
||||
|
||||
/* Default Earthman Palette */
|
||||
|
||||
@@ -39,6 +39,7 @@ describe("Tray", () => {
|
||||
document.body.appendChild(wrap);
|
||||
document.body.appendChild(tray);
|
||||
|
||||
Tray._testSetLandscape(false); // force portrait regardless of window size
|
||||
Tray.init();
|
||||
});
|
||||
|
||||
@@ -83,11 +84,26 @@ describe("Tray", () => {
|
||||
describe("close()", () => {
|
||||
beforeEach(() => Tray.open());
|
||||
|
||||
it("hides #id_tray", () => {
|
||||
it("hides #id_tray after slide + snap both complete", () => {
|
||||
Tray.close();
|
||||
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
|
||||
wrap.dispatchEvent(new Event("animationend"));
|
||||
expect(tray.style.display).toBe("none");
|
||||
});
|
||||
|
||||
it("adds .snap to wrap after slide transition completes", () => {
|
||||
Tray.close();
|
||||
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
|
||||
expect(wrap.classList.contains("snap")).toBe(true);
|
||||
});
|
||||
|
||||
it("removes .snap from wrap once animationend fires", () => {
|
||||
Tray.close();
|
||||
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
|
||||
wrap.dispatchEvent(new Event("animationend"));
|
||||
expect(wrap.classList.contains("snap")).toBe(false);
|
||||
});
|
||||
|
||||
it("removes .open from #id_tray_btn", () => {
|
||||
Tray.close();
|
||||
expect(btn.classList.contains("open")).toBe(false);
|
||||
@@ -203,4 +219,144 @@ describe("Tray", () => {
|
||||
expect(wrap.classList.contains("wobble")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------- //
|
||||
// Landscape mode — Y-axis drag, top-positioned wrap //
|
||||
// ---------------------------------------------------------------------- //
|
||||
|
||||
describe("landscape mode", () => {
|
||||
// Re-init in landscape after the portrait init from outer beforeEach.
|
||||
beforeEach(() => {
|
||||
Tray.reset();
|
||||
Tray._testSetLandscape(true);
|
||||
Tray.init();
|
||||
});
|
||||
|
||||
function simulateDragY(deltaY) {
|
||||
const startY = 50;
|
||||
btn.dispatchEvent(new PointerEvent("pointerdown", { clientY: startY, clientX: 0, bubbles: true }));
|
||||
btn.dispatchEvent(new PointerEvent("pointermove", { clientY: startY + deltaY, clientX: 0, bubbles: true }));
|
||||
btn.dispatchEvent(new PointerEvent("pointerup", { clientY: startY + deltaY, clientX: 0, bubbles: true }));
|
||||
}
|
||||
|
||||
// ── open() in landscape ─────────────────────────────────────────── //
|
||||
|
||||
describe("open()", () => {
|
||||
it("makes #id_tray visible", () => {
|
||||
Tray.open();
|
||||
expect(tray.style.display).not.toBe("none");
|
||||
});
|
||||
|
||||
it("adds .open to #id_tray_btn", () => {
|
||||
Tray.open();
|
||||
expect(btn.classList.contains("open")).toBe(true);
|
||||
});
|
||||
|
||||
it("positions wrap via style.top, not style.left", () => {
|
||||
Tray.open();
|
||||
expect(wrap.style.top).not.toBe("");
|
||||
expect(wrap.style.left).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
// ── close() in landscape ────────────────────────────────────────── //
|
||||
|
||||
describe("close()", () => {
|
||||
beforeEach(() => Tray.open());
|
||||
|
||||
it("closes the tray (display not toggled in landscape)", () => {
|
||||
Tray.close();
|
||||
expect(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("removes .open from #id_tray_btn", () => {
|
||||
Tray.close();
|
||||
expect(btn.classList.contains("open")).toBe(false);
|
||||
});
|
||||
|
||||
it("closed top is less than open top (wrap slides up to close)", () => {
|
||||
const openTop = parseInt(wrap.style.top, 10);
|
||||
Tray.close();
|
||||
const closedTop = parseInt(wrap.style.top, 10);
|
||||
expect(closedTop).toBeLessThan(openTop);
|
||||
});
|
||||
|
||||
it("adds .snap to wrap after top transition completes", () => {
|
||||
Tray.close();
|
||||
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "top" }));
|
||||
expect(wrap.classList.contains("snap")).toBe(true);
|
||||
});
|
||||
|
||||
it("removes .snap from wrap once animationend fires", () => {
|
||||
Tray.close();
|
||||
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "top" }));
|
||||
wrap.dispatchEvent(new Event("animationend"));
|
||||
expect(wrap.classList.contains("snap")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── drag — Y axis ──────────────────────────────────────────────── //
|
||||
|
||||
describe("drag interaction", () => {
|
||||
it("dragging down opens the tray", () => {
|
||||
simulateDragY(100);
|
||||
expect(Tray.isOpen()).toBe(true);
|
||||
});
|
||||
|
||||
it("dragging up does not open the tray", () => {
|
||||
simulateDragY(-100);
|
||||
expect(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("drag > 10px downward suppresses subsequent click", () => {
|
||||
simulateDragY(100);
|
||||
btn.click(); // should be swallowed — tray stays open
|
||||
expect(Tray.isOpen()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not set style.left (Y axis only)", () => {
|
||||
simulateDragY(100);
|
||||
expect(wrap.style.left).toBe("");
|
||||
});
|
||||
|
||||
it("does not add .wobble during drag", () => {
|
||||
simulateDragY(100);
|
||||
expect(wrap.classList.contains("wobble")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── click when closed — wobble, no open ───────────────────────── //
|
||||
|
||||
describe("clicking btn when closed", () => {
|
||||
it("adds .wobble to wrap", () => {
|
||||
btn.click();
|
||||
expect(wrap.classList.contains("wobble")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not open the tray", () => {
|
||||
btn.click();
|
||||
expect(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── click when open — close ────────────────────────────────────── //
|
||||
|
||||
describe("clicking btn when open", () => {
|
||||
beforeEach(() => Tray.open());
|
||||
|
||||
it("closes the tray", () => {
|
||||
btn.click();
|
||||
expect(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── init positions wrap at closed (top) ────────────────────────── //
|
||||
|
||||
it("init sets wrap to closed position (top < 0 or = maxTop)", () => {
|
||||
// After landscape init with no real elements, _maxTop = -(wrapH_fallback - handleH_fallback)
|
||||
// which will be negative. Wrap starts off-screen above.
|
||||
const top = parseInt(wrap.style.top, 10);
|
||||
expect(top).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user