styles related to #id_tray & apparatus separated out into _tray.scss; new tray.js computes the cell size of the tray grid for item organization; room.html now sports the grid as a separate div so as not to interfere w. tray styling or size; new tests in FTs.test_room_tray

This commit is contained in:
Disco DeDisco
2026-03-29 13:36:44 -04:00
parent 5f643350c5
commit 39db59c71a
6 changed files with 455 additions and 256 deletions

View File

@@ -779,250 +779,4 @@ $inv-strip: 30px; // visible height of each stacked card after the first
}
}
// ─── Seat tray ──────────────────────────────────────────────────────────────
//
// Structure:
// #id_tray_wrap — fixed right edge, flex row, slides on :has(.open)
// #id_tray_handle — $handle-exposed wide; contains grip + button
// #id_tray_grip — position:absolute; ::before/::after = concentric rects
// #id_tray_btn — circle button (z-index:1, paints above grip)
// #id_tray — 280px panel; covers grip's rightward extension when open
//
// Closed: wrap translateX($tray-w) → only button circle visible at right edge.
// Open: translateX(0) → full tray panel slides in; grip rects visible as handle.
$tray-w: 280px;
$handle-rect-w: 10000px;
$handle-rect-h: 72px;
$handle-exposed: 48px;
$handle-r: 1rem;
#id_tray_wrap {
position: fixed;
// left set by JS: closed = vw - handle; open = 0
// top/bottom set by JS from nav/footer measurements
top: 0;
right: 0;
bottom: 0;
z-index: 310;
pointer-events: none;
display: flex;
flex-direction: row;
align-items: stretch;
transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1);
&.tray-dragging { transition: none; }
&.wobble { animation: tray-wobble 0.45s ease; }
&.snap { animation: tray-snap 0.30s ease; }
}
#id_tray_handle {
flex-shrink: 0;
position: relative;
width: $handle-exposed;
display: flex;
align-items: center;
justify-content: center;
}
#id_tray_grip {
position: absolute;
top: 50%;
left: calc(#{$handle-exposed} / 2 - 0.125rem);
transform: translateY(-50%);
width: $handle-rect-w;
height: $handle-rect-h;
pointer-events: none;
// Border + overflow:hidden on the grip itself clips ::before's shadow with correct radius
border-radius: $handle-r;
border: 0.15rem solid rgba(var(--secUser), 1);
overflow: hidden;
// Inset inner window: box-shadow spills outward to fill the opaque frame area,
// clipped to grip's rounded edge by overflow:hidden. background:transparent = see-through hole.
&::before {
content: '';
position: absolute;
inset: 0.4rem;
border-radius: calc(#{$handle-r} - 0.35rem);
border: 0.15rem solid rgba(var(--secUser), 1);
background: transparent;
box-shadow: 0 0 0 200px rgba(var(--priUser), 1);
}
&::after {
content: none;
}
}
#id_tray_btn {
pointer-events: auto;
position: relative;
z-index: 1; // above #id_tray_grip
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 1);
cursor: grab;
display: inline-flex;
align-items: center;
justify-content: center;
i {
font-size: 1.75rem;
color: rgba(var(--secUser), 1);
pointer-events: none;
}
&:active { cursor: grabbing; }
&.open {
cursor: pointer;
border-color: rgba(var(--quaUser), 1);
i { color: rgba(var(--quaUser), 1); }
}
}
// Grip borders → --quaUser when tray is open (btn.open precedes grip in DOM so :has() needed)
#id_tray_wrap:has(#id_tray_btn.open) #id_tray_grip {
border-color: rgba(var(--quaUser), 1);
&::before { border-color: rgba(var(--quaUser), 1); }
}
@keyframes tray-wobble {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(6px); }
60% { transform: translateX(-5px); }
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;
margin-left: 0.5rem; // small gap so tray appears slightly off-screen on drag start
pointer-events: auto;
position: relative;
z-index: 1; // above #id_tray_grip pseudo-elements
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.55),
inset 0 0 0 0.3rem rgba(var(--quiUser), 0.45), // prominent bevel ring at wall edge
inset 0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 1), // left wall depth
inset 0.6rem 0 1.5rem -0.5rem rgba(var(--quaUser), 0.5), // left wall depth (hue)
inset 0 0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 1), // top wall depth
inset 0 0.6rem 1.5rem -0.5rem rgba(var(--quaUser), 0.5), // top wall depth (hue)
inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 1), // bottom wall depth
inset 0 -0.6rem 1.5rem -0.5rem rgba(var(--quaUser), 0.5) // bottom wall depth (hue)
;
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.55),
inset 0 0 0 0.3rem rgba(var(--quiUser), 0.45), // prominent bevel ring
inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 1), // bottom wall depth
inset 0 -0.6rem 1.5rem -0.5rem rgba(var(--quaUser), 0.5), // bottom wall depth (hue)
inset 0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 1), // left wall depth
inset 0.6rem 0 1.5rem -0.5rem rgba(var(--quaUser), 0.5), // left wall depth (hue)
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)
;
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); }
}
}
// ─── Seat tray — see _tray.scss ─────────────────────────────────────────────

View File

@@ -0,0 +1,305 @@
// ─── Seat tray ──────────────────────────────────────────────────────────────
//
// Structure:
// #id_tray_wrap — fixed right edge, flex row, slides on :has(.open)
// #id_tray_handle — $handle-exposed wide; contains grip + button
// #id_tray_grip — position:absolute; ::before/::after = concentric rects
// #id_tray_btn — circle button (z-index:1, paints above grip)
// #id_tray — 280px panel; covers grip's rightward extension when open
//
// Closed: wrap translateX($tray-w) → only button circle visible at right edge.
// Open: translateX(0) → full tray panel slides in; grip rects visible as handle.
//
// Grid layout (portrait):
// 8 explicit rows; columns auto-added as items arrive (grid-auto-flow: column).
// --tray-cell-size set by JS from tray.clientHeight / 8 → always square cells.
//
// Grid layout (landscape):
// 8 explicit columns; rows auto-added as items arrive (grid-auto-flow: row).
// --tray-cell-size set by JS from tray.clientWidth / 8 → always square cells.
$tray-w: 280px;
$handle-rect-w: 10000px;
$handle-rect-h: 72px;
$handle-exposed: 48px;
$handle-r: 1rem;
#id_tray_wrap {
position: fixed;
// left set by JS: closed = vw - handle; open = 0
// top/bottom set by JS from nav/footer measurements
top: 0;
right: 0;
bottom: 0;
z-index: 310;
pointer-events: none;
display: flex;
flex-direction: row;
align-items: stretch;
transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1);
&.tray-dragging { transition: none; }
&.wobble { animation: tray-wobble 0.45s ease; }
&.snap { animation: tray-snap 0.30s ease; }
}
#id_tray_handle {
flex-shrink: 0;
position: relative;
width: $handle-exposed;
display: flex;
align-items: center;
justify-content: center;
}
#id_tray_grip {
position: absolute;
top: 50%;
left: calc(#{$handle-exposed} / 2 - 0.125rem);
transform: translateY(-50%);
width: $handle-rect-w;
height: $handle-rect-h;
pointer-events: none;
// Border + overflow:hidden on the grip itself clips ::before's shadow with correct radius
border-radius: $handle-r;
border: 0.15rem solid rgba(var(--secUser), 1);
overflow: hidden;
// Inset inner window: box-shadow spills outward to fill the opaque frame area,
// clipped to grip's rounded edge by overflow:hidden. background:transparent = see-through hole.
&::before {
content: '';
position: absolute;
inset: 0.4rem;
border-radius: calc(#{$handle-r} - 0.35rem);
border: 0.15rem solid rgba(var(--secUser), 1);
background: transparent;
box-shadow: 0 0 0 200px rgba(var(--priUser), 1);
}
&::after {
content: none;
}
}
#id_tray_btn {
pointer-events: auto;
position: relative;
z-index: 1; // above #id_tray_grip
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 1);
cursor: grab;
display: inline-flex;
align-items: center;
justify-content: center;
i {
font-size: 1.75rem;
color: rgba(var(--secUser), 1);
pointer-events: none;
}
&:active { cursor: grabbing; }
&.open {
cursor: pointer;
border-color: rgba(var(--quaUser), 1);
i { color: rgba(var(--quaUser), 1); }
}
}
// Grip borders → --quaUser when tray is open (btn.open precedes grip in DOM so :has() needed)
#id_tray_wrap:has(#id_tray_btn.open) #id_tray_grip {
border-color: rgba(var(--quaUser), 1);
&::before { border-color: rgba(var(--quaUser), 1); }
}
@keyframes tray-wobble {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(6px); }
60% { transform: translateX(-5px); }
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;
margin-left: 0.5rem; // small gap so tray appears slightly off-screen on drag start
pointer-events: auto;
position: relative;
z-index: 1; // above #id_tray_grip pseudo-elements
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.55),
inset 0 0 0 0.3rem rgba(var(--quiUser), 0.45), // prominent bevel ring at wall edge
inset 0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 1), // left wall depth
inset 0.6rem 0 1.5rem -0.5rem rgba(var(--quiUser), 0.5), // left wall depth (hue)
inset 0 0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 1), // top wall depth
inset 0 0.6rem 1.5rem -0.5rem rgba(var(--quiUser), 0.5), // top wall depth (hue)
inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 1), // bottom wall depth
inset 0 -0.6rem 1.5rem -0.5rem rgba(var(--quiUser), 0.5) // bottom wall depth (hue)
;
overflow: hidden; // clip #id_tray_grid to the felt interior
}
#id_tray_grid {
display: grid;
// Portrait: 8 explicit rows; columns auto-added as items arrive.
// --tray-cell-size set by JS from tray.clientHeight / 8 → always square cells.
grid-template-rows: repeat(8, var(--tray-cell-size, 48px));
grid-auto-flow: column;
grid-auto-columns: var(--tray-cell-size, 48px);
}
.tray-cell {
border-right: 2px dotted rgba(var(--quaUser), 0.35);
border-bottom: 2px dotted rgba(var(--quaUser), 0.35);
position: relative;
}
// ─── 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; }
// 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 {
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.55),
inset 0 0 0 0.3rem rgba(var(--quiUser), 0.45), // prominent bevel ring
inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 1), // bottom wall depth
inset 0 -0.6rem 1.5rem -0.5rem rgba(var(--quaUser), 0.5), // bottom wall depth (hue)
inset 0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 1), // left wall depth
inset 0.6rem 0 1.5rem -0.5rem rgba(var(--quaUser), 0.5), // left wall depth (hue)
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)
;
flex: none; // override portrait's flex:1 so height applies
height: 80vh; // gives JS a real offsetHeight to compute bounds from
min-height: unset;
overflow: hidden; // clip #id_tray_grid to the felt interior
}
#id_tray_grid {
// Landscape: 8 explicit columns; rows auto-added as items arrive.
// --tray-cell-size set by JS from tray.clientWidth / 8 → always square cells.
grid-template-columns: repeat(8, var(--tray-cell-size, 48px));
grid-template-rows: none; // clear portrait's 8-row template
grid-auto-flow: row;
grid-auto-rows: var(--tray-cell-size, 48px);
// Anchor grid to the handle-side (bottom) of the tray so the first row
// is visible when partially open; additional rows grow upward.
position: absolute;
bottom: 0;
left: 0;
}
// In landscape the first row sits at the bottom; border-top divides it from
// the felt above. border-bottom would face the wall — swap it out.
.tray-cell {
border-top: 2px dotted rgba(var(--quaUser), 0.35);
border-bottom: none;
}
@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); }
}
}

View File

@@ -6,6 +6,7 @@
@import 'gameboard';
@import 'palette-picker';
@import 'room';
@import 'tray';
@import 'billboard';
@import 'game-kit';
@import 'wallet-tokens';