game-views carousel: ATLAS/SCROLL/POST/CHAT/PULSE views in the room scroll pane — TDD
Unskips the 8 RED FTs from the prior commit (test_game_room_views.py) and lands the feature beneath them — the room's 2nd vertical snap pane becomes a horizontal scroll-snap carousel of five views, landing on SCROLL (2nd). Carousel core: _room_views.html (5 .room-view panes) + _room_views_strip.html (root-level icon strip, outside the aperture so it clears the scroll card's fade mask + scroll clip); room-views.js owns the horizontal axis — goToView (authoritative active-state) + an IntersectionObserver backing native swipe; horizontal wheel (deltaX / shift+wheel) advances a view while vertical wheel stays for feed scroll; icon-click snaps; the strip shows only while the views pane is on screen (vertical IO mirrors room-scroll.js). SCROLL still wraps _room_scroll.html, so the existing binary y-snap + provenance feed + GAME ROOM ⇄ GAME SCROLL title reel behave unchanged. Title reel: the .gr-swap reel gains the four extra view words; the active word is driven by data-active-view on the h2 (set by room-views.js), gated by .is-scroll (room-scroll.js) so ROOM shows at the hex. POST view: a room-scoped game-table thread. New Post.room FK + KIND_ROOM_THREAD (mirrors Brief.room) + Room.get_thread_post(); epic:room_post AJAX endpoint appends a Line (seated-gamer-gated, dup-rejected) and returns the rendered line partial. _post_line.html extracted from post.html and shared by both surfaces + the endpoint. The composer appends OPTIMISTICALLY (synchronous line so the POST + ATLAS views reflect it the instant OK is clicked, no dependence on the round-trip), then reconciles with the server's authoritative @handle/timestamp render; a rejection rolls the optimistic line back. ATLAS view: a live client-side merge of the SCROLL provenance rows + the POST thread rows, time-ordered, each row tagged data-source=provenance|post for end-of-sprint per-type styling. Rebuilds from the live DOM on activation + on every composer append. CHAT/PULSE are .room-view-stub placeholders (no backing model yet). Burger Text sub-btn lights .active on the table (text_btn_active from epic.room_view, unset on every other _burger.html surface) → room-views.js binds its active click to the swipe machine: DOWN to the views pane, RIGHT to Post. Coverage: 8 carousel FTs green; Jasmine RoomViewsSpec (atlas merge order/stability + row data-source); epic ITs (Room.get_thread_post, carousel markup, room_post endpoint 200/403/400/GET); 1636 ITs/UTs + the existing scroll FT green (no regression). Gotcha logged: build FormData(form) BEFORE clearing the input on optimistic submit — clearing first captures an empty text field → 400 → the line silently rolls back. [[project-room-game-views-carousel]] [[project-room-scroll-of-events]] [[project-room-title-scroll-reel-jun02]] [[feedback-jsonfield-exclude-sqlite-null]] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -364,11 +364,27 @@ body {
|
||||
// the slot edges.
|
||||
&[data-letters="3"] { justify-content: space-around; }
|
||||
}
|
||||
// ROOM rests in view at the hex; every view word (SCROLL is
|
||||
// the default) parks one notch below in the bottom fade.
|
||||
.gr-word--base { transform: translateY(0); } // resting in view
|
||||
.gr-word--scroll { transform: translateY(100%); } // parked below the slot
|
||||
.gr-word--scroll,
|
||||
.gr-word--atlas,
|
||||
.gr-word--post,
|
||||
.gr-word--chat,
|
||||
.gr-word--pulse { transform: translateY(100%); } // parked below the slot
|
||||
// Scrolled to the views pane (room-scroll.js adds `.is-scroll`)
|
||||
// → ROOM slides up & out and the ACTIVE view's word rises in.
|
||||
// The active view is carried by `data-active-view` on the h2
|
||||
// (room-views.js, the horizontal carousel axis); before JS
|
||||
// sets it, SCROLL (the landing view) shows.
|
||||
&.is-scroll {
|
||||
.gr-word--base { transform: translateY(-100%); } // up & out the top
|
||||
.gr-word--scroll { transform: translateY(0); } // up into view
|
||||
.gr-word--base { transform: translateY(-100%); } // up & out the top
|
||||
&:not([data-active-view]) .gr-word--scroll,
|
||||
&[data-active-view="scroll"] .gr-word--scroll,
|
||||
&[data-active-view="atlas"] .gr-word--atlas,
|
||||
&[data-active-view="post"] .gr-word--post,
|
||||
&[data-active-view="chat"] .gr-word--chat,
|
||||
&[data-active-view="pulse"] .gr-word--pulse { transform: translateY(0); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,39 @@ html.sea-open #id_aperture_fill {
|
||||
.room-scroll-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem;
|
||||
|
||||
// ── Horizontal game-views carousel ────────────────────────────────────
|
||||
// The pane holds a row of 5 full-width views (ATLAS/SCROLL/POST/CHAT/
|
||||
// PULSE) on native x scroll-snap, nested inside the aperture's binary y
|
||||
// snap. room-views.js parks the initial scrollLeft on SCROLL (2nd view).
|
||||
// The horizontal scrollbar is hidden — the root icon strip is the
|
||||
// affordance. CRITICAL: no z-index/transform here (root stacking context).
|
||||
.room-views {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
overscroll-behavior-x: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { width: 0; height: 0; }
|
||||
}
|
||||
|
||||
.room-view {
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always; // hard stop each view — no coasting
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
// Same %applet-box card chrome + rotated room-name title (`> h2`) as the
|
||||
// Billscroll page's .applet-scroll. No --duoUser pane bg — the dark card
|
||||
@@ -136,6 +168,138 @@ html.sea-open #id_aperture_fill {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST view — the room-scoped game-table thread ─────────────────────
|
||||
// Reuses post.html's #id_post_table + composer markup (shared
|
||||
// _post_line.html). The post styling proper is .post-page-scoped, so the
|
||||
// essentials are restated here for the carousel card.
|
||||
.room-view--post {
|
||||
#id_post_table {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0.5rem 0 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end; // bottom-anchor short threads
|
||||
|
||||
.post-line {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(4rem, auto) 1fr minmax(3rem, auto);
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
.post-line-author {
|
||||
font-weight: bold;
|
||||
color: rgba(var(--quaUser), 1);
|
||||
white-space: nowrap;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.post-line-text { min-width: 0; overflow-wrap: anywhere; }
|
||||
.post-line-time {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.5;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.post-line-buffer { flex-shrink: 0; height: 0.25rem; }
|
||||
}
|
||||
|
||||
.post-line-form {
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
padding-top: 0.25rem;
|
||||
|
||||
.composer-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.composer-row input.form-control { flex: 1; min-width: 0; width: auto; }
|
||||
}
|
||||
}
|
||||
|
||||
// ── ATLAS view — provenance + post-thread merged feed ─────────────────
|
||||
// room-views.js rebuilds the body from the live SCROLL + POST DOM on
|
||||
// activation; rows carry data-source so the two streams read distinctly.
|
||||
.room-atlas-feed {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.atlas-row {
|
||||
padding: 0.3rem 0;
|
||||
font-size: 0.9rem;
|
||||
border-inline-start: 2px solid transparent;
|
||||
padding-inline-start: 0.5rem;
|
||||
|
||||
.atlas-row-time { font-size: 0.7rem; opacity: 0.5; margin-inline-start: 0.4rem; }
|
||||
.atlas-row-who { font-weight: bold; color: rgba(var(--quaUser), 1); margin-inline-end: 0.3rem; }
|
||||
}
|
||||
// Distinct per-source accent (border tint) — the styling hook the
|
||||
// contract reserves for end-of-sprint polish.
|
||||
.atlas-row[data-source="provenance"] { border-inline-start-color: rgba(var(--terUser), 0.6); }
|
||||
.atlas-row[data-source="post"] { border-inline-start-color: rgba(var(--quaUser), 0.6); }
|
||||
}
|
||||
|
||||
// ── CHAT / PULSE stubs ────────────────────────────────────────────────
|
||||
.room-view-stub {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
opacity: 0.5;
|
||||
text-align: center;
|
||||
|
||||
i { font-size: 2.4rem; }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Game-views icon strip ─────────────────────────────────────────────────
|
||||
// Root-level (sibling of the aperture) + position:fixed so it escapes the
|
||||
// aperture scroll clip and the scroll card's fade mask — the same root-
|
||||
// stacking trick as the tooltip portals. Hidden at the hex; room-views.js
|
||||
// adds `.is-visible` while the views pane is on screen. Sits centred at the
|
||||
// viewport bottom in BOTH orientations, so it always clears the mask + stays
|
||||
// within the viewport (landscape gutters are 5rem each → ample room).
|
||||
.room-views-strip {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 0.85rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 140; // above the position strip (130) + the card mask
|
||||
gap: 0.85rem;
|
||||
pointer-events: auto;
|
||||
|
||||
&.is-visible { display: flex; }
|
||||
|
||||
.room-view-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.2rem;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
font-size: 1.15rem;
|
||||
color: rgba(var(--secUser), 0.6);
|
||||
transition: color 0.3s ease, text-shadow 0.3s ease, transform 0.3s ease;
|
||||
|
||||
// Active view's icon carries the --ninUser glow (+ --terUser shadow),
|
||||
// half-again the size; the glow hands off as the carousel snaps.
|
||||
&.is-active {
|
||||
color: rgba(var(--ninUser), 1);
|
||||
text-shadow: 0 0 0.5rem rgba(var(--terUser), 0.9);
|
||||
transform: scale(1.35);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The gear menu's default pane (NVM/DEL/BYE) is `display:contents` so wrapping
|
||||
|
||||
Reference in New Issue
Block a user