Compare commits
4 Commits
1ccb045889
...
79706e817a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79706e817a | ||
|
|
8066ac289f | ||
|
|
7165974905 | ||
|
|
fbe6c12ded |
@@ -1,5 +1,5 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var SCENE_W = 360, SCENE_H = 300;
|
var SCENE_W = 360, SCENE_H = 320;
|
||||||
|
|
||||||
function scaleTable() {
|
function scaleTable() {
|
||||||
var scene = document.querySelector('.room-table-scene');
|
var scene = document.querySelector('.room-table-scene');
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ var SigSelect = (function () {
|
|||||||
var _reservedFloats = {}; // key: role → portal <i> element (thumbs-up, frozen)
|
var _reservedFloats = {}; // key: role → portal <i> element (thumbs-up, frozen)
|
||||||
var _cursorPortal = null;
|
var _cursorPortal = null;
|
||||||
|
|
||||||
|
// CAST SKY click → page reload. Reassignable via setReload for tests so
|
||||||
|
// they can spy without actually reloading the Jasmine runner.
|
||||||
|
var _reload = function () { window.location.reload(); };
|
||||||
|
|
||||||
function getCsrf() {
|
function getCsrf() {
|
||||||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||||
return m ? m[1] : '';
|
return m ? m[1] : '';
|
||||||
@@ -625,12 +629,16 @@ var SigSelect = (function () {
|
|||||||
userRole = overlay.dataset.userRole;
|
userRole = overlay.dataset.userRole;
|
||||||
userPolarity= overlay.dataset.polarity;
|
userPolarity= overlay.dataset.polarity;
|
||||||
|
|
||||||
// CAST SKY btn is rendered hidden during SIG_SELECT; reveal on pick_sky_available
|
// CAST SKY btn is rendered hidden during SIG_SELECT; revealed by
|
||||||
|
// room:pick_sky_available once both polarity rooms are done. Clicking
|
||||||
|
// it must reload the page — the _sky_overlay.html partial is only
|
||||||
|
// included server-side once room.table_status == "SKY_SELECT", so
|
||||||
|
// reloading is the only path that brings the modal + its openSky
|
||||||
|
// handler into the DOM. (Tray.placeSig already played during the
|
||||||
|
// polarity_room_done sequence; do NOT re-open the tray here.)
|
||||||
var pickSkyBtn = document.getElementById('id_pick_sky_btn');
|
var pickSkyBtn = document.getElementById('id_pick_sky_btn');
|
||||||
if (pickSkyBtn) {
|
if (pickSkyBtn) {
|
||||||
pickSkyBtn.addEventListener('click', function () {
|
pickSkyBtn.addEventListener('click', function () { _reload(); });
|
||||||
if (typeof Tray !== 'undefined') Tray.open();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore reservations from server-rendered JSON (page-load state).
|
// Restore reservations from server-rendered JSON (page-load state).
|
||||||
@@ -738,5 +746,6 @@ var SigSelect = (function () {
|
|||||||
},
|
},
|
||||||
_setFrozen: function (v) { _stageFrozen = v; },
|
_setFrozen: function (v) { _stageFrozen = v; },
|
||||||
_setReservedCardId: function (id) { _reservedCardId = id; },
|
_setReservedCardId: function (id) { _reservedCardId = id; },
|
||||||
|
setReload: function (fn) { _reload = fn; },
|
||||||
};
|
};
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -894,4 +894,44 @@ describe("SigSelect", () => {
|
|||||||
expect(document.getElementById("id_hex_waiting_msg")).toBe(null);
|
expect(document.getElementById("id_hex_waiting_msg")).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── CAST SKY click (post pick_sky_available reveal) ──────────────────── //
|
||||||
|
//
|
||||||
|
// After room:pick_sky_available reveals the hidden #id_pick_sky_btn, a
|
||||||
|
// click on CAST SKY must reload the page — the _sky_overlay.html partial
|
||||||
|
// is only rendered server-side once room.table_status == "SKY_SELECT", so
|
||||||
|
// reloading is the only way to bring its modal + openSky handler into the
|
||||||
|
// DOM. The handler must NOT call Tray.open: the tray was already played
|
||||||
|
// during the polarity_room_done sequence (Tray.placeSig) and re-opening it
|
||||||
|
// here would swap Sky Select for the tray.
|
||||||
|
|
||||||
|
describe("CAST SKY click (post pick_sky_available)", () => {
|
||||||
|
let pickSkyBtn, reloadSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pickSkyBtn = document.createElement("button");
|
||||||
|
pickSkyBtn.id = "id_pick_sky_btn";
|
||||||
|
pickSkyBtn.style.display = "none";
|
||||||
|
document.body.appendChild(pickSkyBtn);
|
||||||
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
|
reloadSpy = jasmine.createSpy("reload");
|
||||||
|
SigSelect.setReload(reloadSpy);
|
||||||
|
spyOn(Tray, "open");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (pickSkyBtn) pickSkyBtn.remove();
|
||||||
|
SigSelect.setReload(function () { window.location.reload(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reloads the page so the sky overlay partial renders", () => {
|
||||||
|
pickSkyBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(reloadSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT call Tray.open (tray was already played by Tray.placeSig)", () => {
|
||||||
|
pickSkyBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(Tray.open).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -159,7 +159,11 @@ body {
|
|||||||
&.form-control-lg {
|
&.form-control-lg {
|
||||||
--_pad-v: 0.75rem;
|
--_pad-v: 0.75rem;
|
||||||
padding: var(--_pad-v) 1rem;
|
padding: var(--_pad-v) 1rem;
|
||||||
font-size: 1.125rem;
|
// 1.125rem at rem=14 (small portrait clamp floor) is 15.75px
|
||||||
|
// — just under iOS Safari's 16px auto-zoom threshold. Floor
|
||||||
|
// at 16px to prevent the focus-zoom; native CSS max() handles
|
||||||
|
// the unit mix Sass can't reconcile at compile time.
|
||||||
|
font-size: unquote("max(16px, 1.125rem)");
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-invalid ~ .invalid-feedback {
|
&.is-invalid ~ .invalid-feedback {
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ html.sea-open #id_aperture_fill {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 80vh;
|
height: 100%;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,24 +345,22 @@ html.sea-open #id_aperture_fill {
|
|||||||
//
|
//
|
||||||
// .table-hex: regular pointy-top hexagon.
|
// .table-hex: regular pointy-top hexagon.
|
||||||
// clip-path polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)
|
// clip-path polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)
|
||||||
// on a 160×185 container gives equal-length sides (height = width × 2/√3).
|
// on a 200×231 container gives equal-length sides (height = width × 2/√3).
|
||||||
//
|
//
|
||||||
// Seats use absolute positioning from the .room-table centre.
|
// Seats use absolute positioning from the .room-table centre.
|
||||||
// $seat-r = 130px — radius to seat centroid
|
|
||||||
// $seat-r-x = round(130px × sin60°) = 113px — horizontal component
|
|
||||||
// $seat-r-y = round(130px × cos60°) = 65px — vertical component
|
|
||||||
//
|
|
||||||
// Clockwise from top: slots 1→2→3→4→5→6.
|
// Clockwise from top: slots 1→2→3→4→5→6.
|
||||||
|
|
||||||
$seat-r: 130px;
|
$seat-r: 140px;
|
||||||
$seat-r-x: round($seat-r * 0.866); // 113px
|
$seat-r-x: round($seat-r * 0.866); // 121px
|
||||||
$seat-r-y: round($seat-r * 0.5); // 65px
|
$seat-r-y: round($seat-r * 0.5); // 70px
|
||||||
|
|
||||||
// Seat edge-midpoint geometry (pointy-top hex).
|
// Seat edge-midpoint geometry (pointy-top hex).
|
||||||
// Apothem ≈ 80px + 30px clearance = 110px total push from centre.
|
// 200×231 hex → apothem = 100px; $pos-d = 140 leaves 40px design-units of
|
||||||
$pos-d: 110px;
|
// chair clearance radially. $pos-d-x / $pos-d-y are the x/y components for
|
||||||
$pos-d-x: round($pos-d * 0.5); // 55px
|
// diagonal seats (cos/sin of 60° from horizontal).
|
||||||
$pos-d-y: round($pos-d * 0.866); // 95px
|
$pos-d: 140px;
|
||||||
|
$pos-d-x: round($pos-d * 0.5); // 70px
|
||||||
|
$pos-d-y: round($pos-d * 0.866); // 121px
|
||||||
|
|
||||||
// ─── Position strip ────────────────────────────────────────────────────────
|
// ─── Position strip ────────────────────────────────────────────────────────
|
||||||
// Numbered gate-slot circles sit above the gate backdrop/overlay (z 130 > 120
|
// Numbered gate-slot circles sit above the gate backdrop/overlay (z 130 > 120
|
||||||
@@ -482,10 +480,12 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fixed design-size scene; JS scales it to fill .room-table via transform: scale().
|
// Fixed design-size scene; JS scales it to fill .room-table via transform: scale().
|
||||||
// Design dims: seat reach is ±110px H / ±95px V from centre + seat element size.
|
// Design dims: seat reach is ±140px H / ±121px V from centre + seat element size.
|
||||||
|
// scene H of 320 leaves vertical headroom at large landscape so the rem-scaled
|
||||||
|
// chair icons + labels don't clip the aperture top/bottom edges.
|
||||||
.room-table-scene {
|
.room-table-scene {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
height: 300px;
|
height: 320px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -496,8 +496,8 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
|
|||||||
// Hex border: clip-path clips CSS borders, so the ring is a wrapper with the
|
// Hex border: clip-path clips CSS borders, so the ring is a wrapper with the
|
||||||
// same hex polygon at a slightly larger size. 0.25rem each side — subtle only.
|
// same hex polygon at a slightly larger size. 0.25rem each side — subtle only.
|
||||||
.table-hex-border {
|
.table-hex-border {
|
||||||
width: calc(160px + 0.5rem);
|
width: calc(200px + 0.5rem);
|
||||||
height: calc(185px + 0.5rem);
|
height: calc(231px + 0.5rem);
|
||||||
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
||||||
background: rgba(var(--quaUser), 1);
|
background: rgba(var(--quaUser), 1);
|
||||||
filter: drop-shadow(0 0 6px rgba(var(--quaUser), 0.5));
|
filter: drop-shadow(0 0 6px rgba(var(--quaUser), 0.5));
|
||||||
@@ -507,8 +507,8 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-hex {
|
.table-hex {
|
||||||
width: 160px;
|
width: 200px;
|
||||||
height: 185px;
|
height: 231px;
|
||||||
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
||||||
// Six gradients — one per hex face — each perpendicular to that face so the
|
// Six gradients — one per hex face — each perpendicular to that face so the
|
||||||
// shadows follow the hex geometry rather than the rectangular bounding box.
|
// shadows follow the hex geometry rather than the rectangular bounding box.
|
||||||
|
|||||||
@@ -23,4 +23,11 @@ select,
|
|||||||
[contenteditable] {
|
[contenteditable] {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
touch-action: auto;
|
touch-action: auto;
|
||||||
|
// iOS Safari auto-zooms when focusing a form field whose computed font-size
|
||||||
|
// is < 16px. At rem=14 (small portrait viewports w. clamp(14,2.4vmin,22))
|
||||||
|
// a 1rem input is 14px → triggers zoom. `max(16px, 1em)` enforces the 16px
|
||||||
|
// floor while still inheriting larger sizes from parent contexts when set.
|
||||||
|
// unquote() keeps Sass from trying to evaluate the px/em compare at compile
|
||||||
|
// time (it can't reconcile units); CSS max() handles it natively at runtime.
|
||||||
|
font-size: unquote("max(16px, 1em)");
|
||||||
}
|
}
|
||||||
@@ -894,4 +894,44 @@ describe("SigSelect", () => {
|
|||||||
expect(document.getElementById("id_hex_waiting_msg")).toBe(null);
|
expect(document.getElementById("id_hex_waiting_msg")).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── CAST SKY click (post pick_sky_available reveal) ──────────────────── //
|
||||||
|
//
|
||||||
|
// After room:pick_sky_available reveals the hidden #id_pick_sky_btn, a
|
||||||
|
// click on CAST SKY must reload the page — the _sky_overlay.html partial
|
||||||
|
// is only rendered server-side once room.table_status == "SKY_SELECT", so
|
||||||
|
// reloading is the only way to bring its modal + openSky handler into the
|
||||||
|
// DOM. The handler must NOT call Tray.open: the tray was already played
|
||||||
|
// during the polarity_room_done sequence (Tray.placeSig) and re-opening it
|
||||||
|
// here would swap Sky Select for the tray.
|
||||||
|
|
||||||
|
describe("CAST SKY click (post pick_sky_available)", () => {
|
||||||
|
let pickSkyBtn, reloadSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pickSkyBtn = document.createElement("button");
|
||||||
|
pickSkyBtn.id = "id_pick_sky_btn";
|
||||||
|
pickSkyBtn.style.display = "none";
|
||||||
|
document.body.appendChild(pickSkyBtn);
|
||||||
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
|
reloadSpy = jasmine.createSpy("reload");
|
||||||
|
SigSelect.setReload(reloadSpy);
|
||||||
|
spyOn(Tray, "open");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (pickSkyBtn) pickSkyBtn.remove();
|
||||||
|
SigSelect.setReload(function () { window.location.reload(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reloads the page so the sky overlay partial renders", () => {
|
||||||
|
pickSkyBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(reloadSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT call Tray.open (tray was already played by Tray.placeSig)", () => {
|
||||||
|
pickSkyBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(Tray.open).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,6 +98,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}());
|
}());
|
||||||
|
|
||||||
|
// iOS Safari auto-zooms when focusing an <input>/<textarea>/<select>
|
||||||
|
// whose font-size is < 16px, and does NOT auto-zoom back out on blur.
|
||||||
|
// Belt-and-suspenders: primary prevention is the global `font-size:
|
||||||
|
// max(16px, 1em)` rule in core.scss; this JS is a fallback for inputs
|
||||||
|
// that slip through (custom controls, shadow DOM, etc.). Modern iOS
|
||||||
|
// doesn't reliably react to a simple `setAttribute('content', ...)`
|
||||||
|
// tweak on the existing meta — removing and re-appending the meta
|
||||||
|
// element entirely is more dependable.
|
||||||
|
(function () {
|
||||||
|
var origMeta = document.querySelector('meta[name="viewport"]');
|
||||||
|
if (!origMeta) return;
|
||||||
|
var baseContent = origMeta.getAttribute('content');
|
||||||
|
document.addEventListener('focusout', function (e) {
|
||||||
|
if (!e.target.matches || !e.target.matches('input, textarea, select')) return;
|
||||||
|
var oldMeta = document.querySelector('meta[name="viewport"]');
|
||||||
|
if (oldMeta) oldMeta.remove();
|
||||||
|
var snapMeta = document.createElement('meta');
|
||||||
|
snapMeta.setAttribute('name', 'viewport');
|
||||||
|
snapMeta.setAttribute('content', baseContent + ', maximum-scale=1.0, user-scalable=no');
|
||||||
|
document.head.appendChild(snapMeta);
|
||||||
|
setTimeout(function () {
|
||||||
|
snapMeta.remove();
|
||||||
|
var revertMeta = document.createElement('meta');
|
||||||
|
revertMeta.setAttribute('name', 'viewport');
|
||||||
|
revertMeta.setAttribute('content', baseContent);
|
||||||
|
document.head.appendChild(revertMeta);
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}());
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user