From ace861209978ff4352c52f5edd0dca2daf7e5edd Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Sun, 17 May 2026 23:21:02 -0400 Subject: [PATCH] =?UTF-8?q?tray=20apparatus=20scales=20w.=20fluid=20rem;?= =?UTF-8?q?=20sig-select=209=C3=972=20middling=20breakpoint=20=E2=80=94=20?= =?UTF-8?q?`$handle-exposed`=20was=20`48px`=20fixed=20while=20`#id=5Ftray?= =?UTF-8?q?=5Fbtn`=20is=20`3rem`,=20so=20on=20big-rem=20viewports=20(`clam?= =?UTF-8?q?p(14px,=202.4vmin,=2022px)`=20=E2=86=92=20up=20to=2022px=20on?= =?UTF-8?q?=20tall=20screens,=20btn=3D66)=20the=20btn's=20flex=20parent=20?= =?UTF-8?q?(`#id=5Ftray=5Fhandle`)=20shrank=20the=20btn=20from=2066=C3=976?= =?UTF-8?q?6=20=E2=86=92=2048=C3=9766=20via=20default=20`flex-shrink:1`=20?= =?UTF-8?q?in=20portrait=20(elongated=20tall=20ellipse),=20and=20in=20land?= =?UTF-8?q?scape=20the=20btn=20overflowed=20the=2048px-tall=20handle=20ver?= =?UTF-8?q?tically=20(extending=209px=20past=20viewport=20top=20in=20close?= =?UTF-8?q?d=20state);=20fix:=20`$handle-exposed:=203rem`=20matches=20the?= =?UTF-8?q?=20btn=20so=20it=20fills=20the=20exposed=20area=20at=20every=20?= =?UTF-8?q?rem;=20`$handle-rect-h:=204.5rem`=20(was=20`72px`)=20gives=20th?= =?UTF-8?q?e=20visible=20rail=20thickness=20a=20touch=20of=20breathing=20r?= =?UTF-8?q?oom=20around=20the=20btn=20at=20every=20scale;=20landscape=20ru?= =?UTF-8?q?les=20in=20the=20same=20partial=20that=20hard-coded=20`48px`=20?= =?UTF-8?q?/=20`72px`=20(`#id=5Ftray=5Fhandle=20{=20height:=2048px=20}`,?= =?UTF-8?q?=20`#id=5Ftray=5Fgrip=20{=20bottom:=20calc(48px/2=20-=200.125re?= =?UTF-8?q?m);=20width:=2072px=20}`)=20now=20reference=20the=20variables?= =?UTF-8?q?=20so=20they=20track=20in=20sync=20=E2=80=94=20`tray.js=20=5Fco?= =?UTF-8?q?mputeBounds()`=20swapped=20from=20`=5Fbtn.offsetWidth/Height`?= =?UTF-8?q?=20=E2=86=92=20`=5Fhandle.offsetWidth/Height`=20for=20the=20sam?= =?UTF-8?q?e=20reason:=20even=20with=20the=20SCSS=20fix,=20measuring=20the?= =?UTF-8?q?=20btn=20would=20re-introduce=20the=20offset=20when=20btn=20and?= =?UTF-8?q?=20handle=20drift=20(which=20they=20shouldn't=20now,=20but=20th?= =?UTF-8?q?e=20handle=20is=20the=20layout-defining=20element=20so=20measur?= =?UTF-8?q?e=20it=20directly);=20`id=5Fkit=5Fbtn`=20added=20as=20fallback?= =?UTF-8?q?=20for=20`id=5Fgear=5Fbtn`=20(which=20no=20longer=20renders=20o?= =?UTF-8?q?n=20the=20room=20page)=20so=20the=20open-state=20landscape=20wr?= =?UTF-8?q?ap=20height=20anchors=20to=20the=20bottom-right=20kit=20btn=20i?= =?UTF-8?q?nstead=20of=20the=20full=20viewport=20=E2=80=94=20`id=5Ftray=5F?= =?UTF-8?q?handle`=20cached=20on=20the=20module=20via=20`=5Fhandle`=20ref?= =?UTF-8?q?=20alongside=20`=5Fbtn`=20and=20cleared=20in=20`reset()`=20;=20?= =?UTF-8?q?sig-select=20grid=20jumped=20straight=20from=206=20cols=20(narr?= =?UTF-8?q?ow=20landscape)=20=E2=86=92=2018=20cols=20=C3=97=203rem=20at=20?= =?UTF-8?q?`min-width:=20900px`,=20but=2018=C3=973rem=20+=207rem=20modal?= =?UTF-8?q?=20margins=20needs=20~1376px=20to=20clear=20at=20rem=3D22=20so?= =?UTF-8?q?=20the=20cards=20spilled=20off=20the=20sides=20on=20common=2012?= =?UTF-8?q?80-wide=20laptops=20+=20the=20previous-era=209=C3=972=20middlin?= =?UTF-8?q?g=20layout=20had=20simply=20been=20dropped;=20new=20cascade=20i?= =?UTF-8?q?n=20`=5Fcard-deck.scss`=20mirrors=20the=20comment's=20documente?= =?UTF-8?q?d=20intent:=206=20cols=20default=20landscape=20(row=20layout,?= =?UTF-8?q?=20stage=20beside=20grid)=20=E2=86=92=209=20cols=20=C3=97=203re?= =?UTF-8?q?m=20at=20`min-width:=20900px`=20(column=20layout,=20stage=20abo?= =?UTF-8?q?ve=20grid)=20=E2=86=92=2018=20cols=20=C3=97=203rem=20at=20`min-?= =?UTF-8?q?width:=201400px`=20=E2=86=92=2018=20=C3=97=205rem=20at=20`min-w?= =?UTF-8?q?idth:=201800px`=20(unchanged)=20=E2=80=94=20verified=20in=20Cla?= =?UTF-8?q?udezilla=20across=20iphone-14=20portrait=20(rem=3D14,=20btn=3D4?= =?UTF-8?q?2=20square,=20handle=20right=20edge=20at=20viewport=20right),?= =?UTF-8?q?=20816=C3=97826=20portrait=20near-landscape=20(rem=3D19.6,=20bt?= =?UTF-8?q?n=3D58.75=20square=20no=20longer=20elongated),=201149=C3=97751?= =?UTF-8?q?=20landscape=20mid=20(rem=3D18,=20btn=3D54=20square=20at=20view?= =?UTF-8?q?port=20top,=209=C3=972=20grid),=201789=C3=971111=20desktop=20XL?= =?UTF-8?q?=20(rem=3D22,=20btn=3D66=20square=20at=20viewport=20top,=2018?= =?UTF-8?q?=C3=971=20grid)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- src/apps/epic/static/apps/epic/tray.js | 45 +++++++++++++++++--------- src/static_src/scss/_card-deck.scss | 22 +++++++++---- src/static_src/scss/_tray.scss | 15 +++++---- 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/apps/epic/static/apps/epic/tray.js b/src/apps/epic/static/apps/epic/tray.js index ec0bb26..2d3a2ad 100644 --- a/src/apps/epic/static/apps/epic/tray.js +++ b/src/apps/epic/static/apps/epic/tray.js @@ -9,10 +9,11 @@ var Tray = (function () { var _dragStartTop = null; var _dragHandled = false; - var _wrap = null; - var _btn = null; - var _tray = null; - var _grid = null; + var _wrap = null; + var _handle = null; + var _btn = null; + var _tray = null; + var _grid = null; // Role code → scrawl SVG name mapping for tray card display. var _ROLE_SCRAWL = { @@ -99,12 +100,20 @@ var Tray = (function () { // meets the gear button when open. Tray is flex:1 and fills the rest. // Open: wrap top = 0 (pinned to viewport top). // Closed: wrap top = -(gearBtnTop - handleH) = tray fully above viewport. - var gearBtn = document.getElementById('id_gear_btn'); + // Anchor: id_gear_btn historically; id_kit_btn is the live fallback so + // the open-state handle bottom lands at the bottom-right anchor instead + // of overlapping it (no id_gear_btn renders on the room page today). + var gearBtn = document.getElementById('id_gear_btn') + || document.getElementById('id_kit_btn'); var gearBtnTop = window.innerHeight; if (gearBtn) { gearBtnTop = Math.round(gearBtn.getBoundingClientRect().top); } - var handleH = (_btn && _btn.offsetHeight) || 48; + // handleH = #id_tray_handle's height (48px CSS), NOT _btn.offsetHeight. + // The 3rem btn can be larger than the 48px handle on big-rem viewports + // (clamp(14px, 2.4vmin, 22px) → btn=66 at rem=22); using btn here would + // push the handle's bottom that far below y=0 in closed state. + var handleH = (_handle && _handle.offsetHeight) || 48; // Pin wrap height so handle bottom = gear btn top when open. if (_wrap) _wrap.style.height = gearBtnTop + 'px'; @@ -116,7 +125,11 @@ var Tray = (function () { _maxTop = -(gearBtnTop - handleH); } else { // Portrait: wrap width = full viewport; handle parks at right edge. - var handleW = _btn.offsetWidth || 48; + // handleW = #id_tray_handle's width (48px CSS), NOT _btn.offsetWidth — + // same rationale as landscape: btn can be wider than handle on big-rem + // viewports, which would leave a visible gap between the handle's right + // edge and the viewport right edge in closed state. + var handleW = (_handle && _handle.offsetWidth) || 48; if (_wrap) _wrap.style.width = window.innerWidth + 'px'; _minLeft = 0; _maxLeft = window.innerWidth - handleW; @@ -398,10 +411,11 @@ var Tray = (function () { } function init() { - _wrap = document.getElementById('id_tray_wrap'); - _btn = document.getElementById('id_tray_btn'); - _tray = document.getElementById('id_tray'); - _grid = document.getElementById('id_tray_grid'); + _wrap = document.getElementById('id_tray_wrap'); + _handle = document.getElementById('id_tray_handle'); + _btn = document.getElementById('id_tray_btn'); + _tray = document.getElementById('id_tray'); + _grid = document.getElementById('id_tray_grid'); _roleIconsUrl = (_grid && _grid.dataset.roleIconsUrl) || null; if (!_btn) return; @@ -572,10 +586,11 @@ var Tray = (function () { delete el.dataset.role; }); } - _wrap = null; - _btn = null; - _tray = null; - _grid = null; + _wrap = null; + _handle = null; + _btn = null; + _tray = null; + _grid = null; } if (document.readyState === 'loading') { diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 3ef4db2..0c3905c 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -868,10 +868,12 @@ html:has(.sig-backdrop) { } // ─── Sig select: landscape overrides ───────────────────────────────────────── -// Landscape base: 9×2 grid of 3rem cards. At ≥992px (wide enough for 18 cards -// at 3rem + 4rem left + ~4rem right): collapse to a single 18×1 row so the -// stage preview gets maximum vertical real-estate. -// padding-left clears the fixed left navbar (JS sets right/bottom but not left). +// Cascade (each step is a SUPERSET of the prior): +// narrow landscape → 6 cols × 2.5rem, row layout (stage beside grid) +// ≥ 900px → 9×2 grid of 3rem cards, column layout (stage above) +// ≥ 1400px → 18×1 row of 3rem cards (wide enough that 18×3rem +// + ~7rem modal margins clears even at rem=22) +// ≥ 1800px → 18×1 row of 5rem cards + doubled sidebar padding // Grid margins reset to 0 — overlay padding handles all edge clearance. @media (orientation: landscape) { @@ -892,7 +894,7 @@ html:has(.sig-backdrop) { } @media (orientation: landscape) and (min-width: 900px) { - // Wide landscape: revert to stacked layout (stage top, 18-card row grid bottom). + // Middling landscape: stacked layout (stage top, 9×2 grid bottom). .sig-modal { flex-direction: column; align-items: stretch; @@ -903,11 +905,19 @@ html:has(.sig-backdrop) { margin-left: 3rem; } .sig-deck-grid { - grid-template-columns: repeat(18, 3rem); + grid-template-columns: repeat(9, 3rem); align-self: center; } } +@media (orientation: landscape) and (min-width: 1400px) { + // Wide landscape: 18-card single-row grid. 18×3rem + ~7rem modal margins + // clears the viewport here even at the fluid-rem ceiling (rem=22 → ~1376px). + .sig-deck-grid { + grid-template-columns: repeat(18, 3rem); + } +} + @media (orientation: landscape) and (min-width: 1800px) { // Sig overlay: clear doubled sidebars (8rem each instead of 4rem/6rem) .sig-overlay { padding-left: 8rem; padding-right: 8rem; } diff --git a/src/static_src/scss/_tray.scss b/src/static_src/scss/_tray.scss index a1655e3..9bf345b 100644 --- a/src/static_src/scss/_tray.scss +++ b/src/static_src/scss/_tray.scss @@ -20,8 +20,9 @@ $tray-w: 280px; $handle-rect-w: 10000px; -$handle-rect-h: 72px; -$handle-exposed: 48px; +$handle-rect-h: 4.5rem; // visible rail thickness — scales w. rem; ≈ btn (3rem) + breathing room +$handle-exposed: 3rem; // matches #id_tray_btn so the btn fills the exposed handle area + // (previously 48px → flex-shrink squished the btn on rem > 16px viewports) $handle-r: 1rem; $tray-bevel: 0.3rem; // inner bevel ring; grid must sit inside this @@ -304,20 +305,20 @@ $tray-bevel: 0.3rem; // inner bevel ring; grid must sit inside this } #id_tray_handle { - width: auto; // full width of wrap - height: 48px; // $handle-exposed — same exposed dimension as portrait + width: auto; // full width of wrap + height: $handle-exposed; // same exposed dimension as portrait — scales w. rem } #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 + bottom: calc(#{$handle-exposed} / 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 + width: $handle-rect-h; // narrow visible dimension — scales w. rem + height: $handle-rect-w; // extends upward into tray area } #id_tray {