game-views: replace CHAT with YARN (.fa-route) between SCROLL & POST; ATLAS timestamps match their source views

CHAT conceptually overlapped POST, so it's gone — replaced by a distinct YARN view (fa-route) shifted one slot left, between SCROLL and POST. Reelhouse order is now ATLAS | SCROLL | YARN | POST | PULSE. YARN is a stub (the shared [Feature forthcoming] partial in its .applet-scroll, like PULSE).

Touched: the carousel + strip partials, the h2 reel words, the _base.scss data-active-view translateX reindex (yarn=-200%, post now -300%), room-views.js VIEW_ORDER, the Jasmine spec VIEWS, and the FT/IT order + stub assertions. The horizontal-wheel FT now expects scroll's neighbour to be YARN.

ATLAS timestamps: the merged rows carry the ORIGINAL <time> from their source (.drama-event-time from SCROLL, .post-line-time from POST), but those source rules are feed/thread-scoped so the atlas copies rendered full-size inline. Made .atlas-row a flex row and restated the shared small / dim / right-aligned look on both source time classes so each timestamp reads the same as in the view it came from.

Verified: 8 carousel FTs + carousel ITs + Jasmine (atlas merge + swipe machine) green.

[[project-room-game-views-carousel]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-02 14:45:25 -04:00
parent 6f5927083c
commit ced324081f
10 changed files with 53 additions and 32 deletions

View File

@@ -1,6 +1,6 @@
// Game-views carousel — the "reelhouse" (the fivefold applet-scroll carousel)
// in the table-hex aperture's 2nd vertical snap pane.
// Five horizontal views — ATLAS | SCROLL | POST | CHAT | PULSE — reached by
// Five horizontal views — ATLAS | SCROLL | YARN | POST | PULSE — reached by
// scrolling DOWN from the hex, landing on SCROLL (the 2nd). This module owns
// the HORIZONTAL axis; room-scroll.js still owns the vertical hex<->views
// `.is-scroll` title toggle + the feed gear/filter. Responsibilities:
@@ -18,7 +18,7 @@
(function () {
'use strict';
var VIEW_ORDER = ['atlas', 'scroll', 'post', 'chat', 'pulse'];
var VIEW_ORDER = ['atlas', 'scroll', 'yarn', 'post', 'pulse'];
var DEFAULT_VIEW = 'scroll';
// ── pure helpers (exposed for Jasmine) ─────────────────────────────────

View File

@@ -3438,20 +3438,21 @@ class RoomViewsCarouselTest(TestCase):
def test_renders_five_view_panes_in_order(self):
content = self.client.get(self.url).content.decode()
self.assertIn("id_room_views", content)
for view in ("atlas", "scroll", "post", "chat", "pulse"):
for view in ("atlas", "scroll", "yarn", "post", "pulse"):
self.assertIn(f'data-view="{view}"', content)
# Order: atlas precedes scroll precedes post precedes chat precedes pulse.
# Order: atlas precedes scroll precedes yarn precedes post precedes pulse.
positions = [content.index(f'room-view--{v}')
for v in ("atlas", "scroll", "post", "chat", "pulse")]
for v in ("atlas", "scroll", "yarn", "post", "pulse")]
self.assertEqual(positions, sorted(positions))
def test_renders_root_level_icon_strip(self):
content = self.client.get(self.url).content.decode()
self.assertIn("id_room_views_strip", content)
for view in ("atlas", "scroll", "post", "chat", "pulse"):
for view in ("atlas", "scroll", "yarn", "post", "pulse"):
self.assertIn(f'data-view="{view}"', content)
self.assertIn("fa-scroll", content)
self.assertIn("fa-book-atlas", content)
self.assertIn("fa-route", content)
def test_scroll_view_still_wraps_the_provenance_feed(self):
content = self.client.get(self.url).content.decode()
@@ -3472,7 +3473,7 @@ class RoomViewsCarouselTest(TestCase):
content = self.client.get(self.url).content.decode()
self.assertIn("opening move", content)
def test_chat_and_pulse_render_stubs(self):
def test_yarn_and_pulse_render_stubs(self):
content = self.client.get(self.url).content.decode()
self.assertIn("room-view-stub", content)

View File

@@ -32,7 +32,7 @@ from apps.drama.models import GameEvent
from apps.epic.models import Room
from apps.lyric.models import User
VIEW_ORDER = ["atlas", "scroll", "post", "chat", "pulse"]
VIEW_ORDER = ["atlas", "scroll", "yarn", "post", "pulse"]
class GameViewsCarouselTest(FunctionalTest):
@@ -91,7 +91,7 @@ class GameViewsCarouselTest(FunctionalTest):
# ── tests ────────────────────────────────────────────────────────────
def test_icon_strip_hidden_at_hex_shown_in_views_with_scroll_active(self):
"""At the table-hex the strip is hidden; scrolling down reveals it with
five icons in order [atlas, scroll, post, chat, pulse] and SCROLL (2nd)
five icons in order [atlas, scroll, yarn, post, pulse] and SCROLL (2nd)
active + glowing."""
self._open()
self.assertFalse(
@@ -134,11 +134,11 @@ class GameViewsCarouselTest(FunctionalTest):
self._open()
self._scroll_to_views()
self.assertEqual(self._active_view(), "scroll")
# Horizontal wheel right → next view (post).
# Horizontal wheel right → next view in line (yarn, between scroll & post).
self.browser.execute_script(
"document.querySelector('#id_room_views').dispatchEvent("
"new WheelEvent('wheel', {deltaX: 120, deltaY: 0, bubbles: true}));")
self.wait_for(lambda: self.assertEqual(self._active_view(), "post"))
self.wait_for(lambda: self.assertEqual(self._active_view(), "yarn"))
def test_text_btn_from_hex_swipes_down_to_the_reelhouse(self):
"""The burger fan's #id_text_btn (fa-keyboard) is active on the table;
@@ -213,8 +213,8 @@ class GameViewsCarouselTest(FunctionalTest):
self.assertTrue(atlas.find_elements(
By.CSS_SELECTOR, "[data-source='post']"))
def test_chat_and_pulse_render_as_stubs(self):
"""CHAT (fa-comments) + PULSE (fa-chart-pie) are stub views this sprint —
def test_yarn_and_pulse_render_as_stubs(self):
"""YARN (fa-route) + PULSE (fa-chart-pie) are stub views this sprint —
each renders a placeholder, no backing model yet. The watermark icon
rests ABOVE the [Feature forthcoming] label (the shared partial centres
itself absolutely, so without the stub override it would overlap the
@@ -226,7 +226,7 @@ class GameViewsCarouselTest(FunctionalTest):
return self.browser.execute_script(
"return document.querySelector(arguments[0]).getBoundingClientRect();", css)
for view in ("chat", "pulse"):
for view in ("yarn", "pulse"):
self._click_icon(view)
self.wait_for(lambda v=view: self.assertTrue(
self._in_viewport(f".room-view[data-view='{v}']")))

View File

@@ -79,7 +79,7 @@ describe("RoomViews atlas row rendering", () => {
// scrolling. (The descent beat itself + plain goToView are covered by the FT.)
describe("RoomViews swipe machine", () => {
let aperture, viewsEl, strip, textBtn;
const VIEWS = ["atlas", "scroll", "post", "chat", "pulse"];
const VIEWS = ["atlas", "scroll", "yarn", "post", "pulse"];
function tag(name, attrs) {
const n = document.createElement(name);

View File

@@ -410,12 +410,12 @@ body {
.gr-word--base { transform: translateY(-100%); } // up & out
.gr-views-reel { transform: translateY(0); } // rises in
}
// Horizontal cell (VIEW_ORDER atlas|scroll|post|chat|pulse) —
// Horizontal cell (VIEW_ORDER atlas|scroll|yarn|post|pulse) —
// keyed on the active view ALONE so it holds at the hex too.
&[data-active-view="atlas"] .gr-views-track { transform: translateX(0); }
&[data-active-view="scroll"] .gr-views-track { transform: translateX(-100%); }
&[data-active-view="post"] .gr-views-track { transform: translateX(-200%); }
&[data-active-view="chat"] .gr-views-track { transform: translateX(-300%); }
&[data-active-view="yarn"] .gr-views-track { transform: translateX(-200%); }
&[data-active-view="post"] .gr-views-track { transform: translateX(-300%); }
&[data-active-view="pulse"] .gr-views-track { transform: translateX(-400%); }
}
}

View File

@@ -238,13 +238,30 @@ html.sea-open #id_aperture_fill {
flex-direction: column;
.atlas-row {
display: flex;
align-items: baseline;
gap: 0.4rem;
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; }
.atlas-row-who { font-weight: bold; color: rgba(var(--quaUser), 1); flex-shrink: 0; }
.atlas-row-body { flex: 1; min-width: 0; overflow-wrap: anywhere; }
// The merged rows carry the ORIGINAL <time> from their source row
// (.drama-event-time from SCROLL, .post-line-time from POST). Those
// source rules are scoped to the feed/thread, so restate the shared
// small / dim / right-aligned look here so each timestamp reads the
// same as it does in the view it came from.
.drama-event-time,
.post-line-time {
flex-shrink: 0;
margin-inline-start: auto;
font-size: 0.75rem;
opacity: 0.5;
text-align: right;
white-space: nowrap;
}
}
// Distinct per-source accent (border tint) — the styling hook the
// contract reserves for end-of-sprint polish.

View File

@@ -79,7 +79,7 @@ describe("RoomViews atlas row rendering", () => {
// scrolling. (The descent beat itself + plain goToView are covered by the FT.)
describe("RoomViews swipe machine", () => {
let aperture, viewsEl, strip, textBtn;
const VIEWS = ["atlas", "scroll", "post", "chat", "pulse"];
const VIEWS = ["atlas", "scroll", "yarn", "post", "pulse"];
function tag(name, attrs) {
const n = document.createElement(name);

View File

@@ -25,6 +25,15 @@
{% include "apps/gameboard/_partials/_room_scroll.html" %}
</div>
{# YARN — stub view this sprint (no backing model yet). The route icon + #}
{# the shared [Feature forthcoming] partial stand in. #}
<div class="room-view room-view--yarn" data-view="yarn">
<div class="applet-scroll room-view-card">
<h2>{{ room.name }}</h2>
<div class="room-view-stub"><i class="fa-solid fa-route"></i>{% include "core/_partials/_forthcoming.html" %}</div>
</div>
</div>
{# POST — the room-scoped game-table thread. Same #id_post_table + composer #}
{# markup as post.html (shared _post_line.html), but the composer appends #}
{# via the epic:room_post AJAX endpoint (room-views.js) so the carousel #}
@@ -53,14 +62,8 @@
</div>
</div>
{# CHAT + PULSE — stub views this sprint (no backing model yet). The #}
{# identity icon + the shared [Feature forthcoming] partial stand in. #}
<div class="room-view room-view--chat" data-view="chat">
<div class="applet-scroll room-view-card">
<h2>{{ room.name }}</h2>
<div class="room-view-stub"><i class="fa-solid fa-comments"></i>{% include "core/_partials/_forthcoming.html" %}</div>
</div>
</div>
{# PULSE — stub view this sprint (no backing model yet). The chart icon + #}
{# the shared [Feature forthcoming] partial stand in. #}
<div class="room-view room-view--pulse" data-view="pulse">
<div class="applet-scroll room-view-card">
<h2>{{ room.name }}</h2>

View File

@@ -3,11 +3,11 @@
{# the same reason the position strip + tooltip portals live at room-page root. #}
{# Hidden at the hex; room-views.js adds `.is-visible` while the views pane is #}
{# on screen, mirrors the active view's glow, and snaps the carousel on click. #}
{# Five icons L→R: ATLAS · SCROLL (default) · POST · CHAT · PULSE. #}
{# Five icons L→R: ATLAS · SCROLL (default) · YARN · POST · PULSE. #}
<div id="id_room_views_strip" class="room-views-strip" aria-hidden="true">
<button type="button" class="room-view-icon" data-view="atlas" aria-label="Atlas"><i class="fa-solid fa-book-atlas"></i></button>
<button type="button" class="room-view-icon is-active" data-view="scroll" aria-label="Scroll"><i class="fa-solid fa-scroll"></i></button>
<button type="button" class="room-view-icon" data-view="yarn" aria-label="Yarn"><i class="fa-solid fa-route"></i></button>
<button type="button" class="room-view-icon" data-view="post" aria-label="Post"><i class="fa-solid fa-keyboard"></i></button>
<button type="button" class="room-view-icon" data-view="chat" aria-label="Chat"><i class="fa-solid fa-comments"></i></button>
<button type="button" class="room-view-icon" data-view="pulse" aria-label="Pulse"><i class="fa-solid fa-chart-pie"></i></button>
</div>

View File

@@ -18,7 +18,7 @@
all times, including at the hex. Result: hex⇄views is a pure vertical reel
landing on wherever you left off; lateral nav is a pure horizontal slide
(old word out one side, new in from the other). base.html splits `.gr-word`.
{% endcomment %}{% block header_text %}<span>Game</span>{% if room.table_status %}<span class="gr-swap" data-letters-split="1"><span class="gr-word gr-word--base">room</span><span class="gr-views-reel"><span class="gr-views-track"><span class="gr-word gr-word--atlas">atlas</span><span class="gr-word gr-word--scroll">scroll</span><span class="gr-word gr-word--post">post</span><span class="gr-word gr-word--chat">chat</span><span class="gr-word gr-word--pulse">pulse</span></span></span></span>{% else %}<span>room</span>{% endif %}{% endblock header_text %}
{% endcomment %}{% block header_text %}<span>Game</span>{% if room.table_status %}<span class="gr-swap" data-letters-split="1"><span class="gr-word gr-word--base">room</span><span class="gr-views-reel"><span class="gr-views-track"><span class="gr-word gr-word--atlas">atlas</span><span class="gr-word gr-word--scroll">scroll</span><span class="gr-word gr-word--yarn">yarn</span><span class="gr-word gr-word--post">post</span><span class="gr-word gr-word--pulse">pulse</span></span></span></span>{% else %}<span>room</span>{% endif %}{% endblock header_text %}
{% block content %}
<div class="room-page" data-room-id="{{ room.id }}"