Repository navigation
feat(debates): unify the explore debate card with the full screen experience (GEO-2912, GEO-2879) - #2440
Merged
jwalkingjew merged 13 commits intoSep 21, 2026
Conversation
… full screen (GEO-2912)
A debate on Explore and the same debate full screen read as two features. The videos
were already one component -- `DebateFeedPlayer` -- so the debater tiles, position
chips and winner share have always matched. Everything under them did not: full
screen has the bordered vote pill carrying the score, and comment/claims/share
controls with counts, while the card had the generic explore row -- inline vote
arrows plus two grey icon buttons drawn only for this card.
So give the card the same `DebateInteractionBar`, in the horizontal orientation the
full-screen feed already uses at narrow widths. Sharing the component rather than
restyling the row toward it is the point: a restyle drifts again the next time either
surface moves.
The bar grows two optional seams for the card, both about where a control leads
rather than what it looks like:
- `commentsPanelOpen` marks the comment control as an opener of the app's global
comments panel. Explore routes comments there, as every other card does, and that
panel recognises its openers by `data-entity-comments-opener` -- without it a press
while the panel is open reads as an outside click and dismisses it instead of
switching it to this debate.
- Claims and Share are now optional, so the card can render the bar before its
geo-chat lookups land. Votes and comments need no debate; those two do. A footer
that appeared late would shift the card under the reader.
Counts stay the card's own: the comment count the explore feed already resolved, so
a page of debates doesn't fetch a thread apiece, and the shared transcript-claims
query the Claims panel reads from.
`explore-claims-icon` and `explore-share-icon` existed only for the two buttons this
replaces, and go with them.
Not touched: the card title. Full screen shows the claim and the card shows the
Debate entity's name ("A vs. B on <claim>"), but that title and its click target are
GEO-2879's, which contradicts GEO-2794 and its open PR. Unifying it here would pick a
winner in someone else's argument.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ar as a rail (GEO-2912, GEO-2879) Two things the card still did differently from the same debate full screen. The interaction bar sat under the videos rather than down their right. Full screen puts it in a rail beside them and only drops to a row beneath at its own narrow widths, so the card does the same, against its own width rather than the window's: the media caps at 480px and the rail is 36px with a 12px gutter, so under ~540px of card every pixel the rail takes comes out of a video whose overlaid name, position chip and Winner pill are already competing for the bottom edge. `@container`, not a media query, because this card is drawn in the feed, in a side panel and in a data block, at widths a viewport measurement says nothing about. Both orientations mount and the container query hides one, which is how full screen does it too. That moves the interaction state up into the card: both bars have to read the same open flags, and the dialogs must exist once -- a `hidden` wrapper still mounts its children, so a share dialog owned by the losing bar would portal itself on screen anyway. The title was the Debate entity's name -- "<A> vs. <B> on <claim>", the claim with a preamble -- where full screen titles the debate with the claim alone. Now the card does too, linked to the claim entity in the claim's own space, opening it in the side panel on Explore (GEO-2879). Until the geo-chat lookup lands there is no claim to show, and the entity name stands in rather than the title arriving a beat late. That reaches the panel by going through `ExploreCardEntityLink`, which refuses it to debates because a full-screen video experience is a poor fit for one (GEO-2794). This is the claim, which a panel serves well -- it is the whole reason GEO-2879 can have what GEO-2794 refused -- so the debate exception is untouched and still holds for the fallback card, whose title is the debate. Ids are normalized on the way: geo-chat returns UUIDs where the graph, and every explore route and panel target, spells them as plain hex. The test's `PrefetchLink` stand-in forwarded only `href`, so it would have let a title with broken click handling pass. It forwards what it is given now.
…e videos (GEO-2912) The card's top-right held "View all", a link into this space's debates list. But the card *is* a debate you can watch, so the CTA worth that corner is the one full screen puts there: the one that puts you in a debate rather than one that lists more of them. Extracted as `JoinDebateButton` and shared with the full-screen feed rather than written twice. What this control decides is worth more than what it looks like, and every branch of it was a bug once: do nothing until Privy has restored the session (gating on the smart account sent signed-in viewers back through a login that wiped their half-finished onboarding); send a signed-out viewer to sign in and then finish what they pressed, rather than returning them to press it again; close the hub on a second press, or it is a one-way door. A second copy is a second chance to get one of those wrong. The feed keeps the two things that are its own, as callbacks: making the pressed debate active -- its bar is reachable from 0% visibility while activation needs 60%, so mid-scroll the hub would open from the debate being scrolled away from -- and closing its in-flow panel so the hub doesn't stack over one. Both are no-ops for the card, which has neither. Aligning the button with the videos is why the meta row and title now sit in the capped column with the media instead of spanning the card. That is the arrangement full screen already has -- header above the media, bar beside it -- so the alignment falls out of the structure rather than from repeating a width. The title consequently wraps at the media width rather than the card's, which is also what full screen does.
…deos (GEO-2912) Reverts the rail. A card is short and wide where full screen is tall, so the bar reads better under the videos than down their side, and it takes the horizontal arrangement at every width rather than switching on one. What that removes, all of it scaffolding the rail needed and nothing else did: the `debate-card-narrow` container-query variant, the `@container` on the article it asked, the second `DebateInteractionBar` that the query hid, and the two test ids the suite used to tell one bar from the other. One bar in the DOM again, so the tests query it directly. The bar keeps what it gained on the way: it is still the shared component, still carrying the vote pill, score and counts the full-screen feed carries, and it now sits inside the capped column, so it spans the videos rather than the whole card. Only the axis differs between the two surfaces, which is a difference full screen already makes between its own breakpoints. The card keeps the interaction state that moved up to it for the rail's sake. The reason is no longer that two bars have to share flags -- the bar is shared with the full-screen feed and stays presentational, so what a control opens is the card's to own and to render once. Comment says so now.
…ched in (GEO-2912) The column was a flat 480px, which on a tall display left the card smaller than it needed to be and on a short one made it taller than the viewport. Width is what sets height here — the tiles are fixed-aspect — so widening the card and fitting it vertically are the same problem, and the answer is to derive the width from the height available. Measured: the media is `1.2042 × width + 8`, and the rest of the card is a flat 163px (32px of `py-4`, a 28px meta row, a two-line 46px title, a 32px bar with its margin, 24px of column gaps, the 1px rule). So a card is `1.2042 × width + 171` tall, and keeping that under the viewport less the 44px app header gives `width ≤ 0.83 × dvh − 178px`. Hence `clamp(320px, calc(83dvh - 178px), 560px)`: wider than before wherever there is room (560px from a 900px viewport up, against the old 480px), narrower where there isn't, and floored at 320px below roughly a 600px viewport, where honouring the promise would mean a video too small to read a face in. Same trick and same reason as `--debate-feed-column-width` on the full-screen feed. Verified at 1440px wide against the real 44px header: 1200/1000/900 → 560px column, 822px card; 800 → 486px, 756px; 700 → 403px, 656px; 600 → 320px, 556px. Every one inside its budget, exact from 800 down where the formula binds. The title is clamped to two lines for this to hold — a third is 23px the viewport was not promised — which is also what the full-screen header does to the same claim. It keeps a `title` attribute so a cut claim is still readable.
…d re-render reaches (GEO-2912) Review pass over the branch. Three of the findings were real. `DebateCardVideos` is memoized. The card root subscribes to the global comments-panel atom -- it has to, to tell the bar whether the panel is open on this debate -- where the leaf button it replaced held that subscription before, so opening comments anywhere re-rendered every debate card down through the player and its playback hooks. `debate` is a stable react-query object and the flags are booleans, so the memo skips the subtree for a flag it does not read. Playback was never interrupted by this -- a re-render keeps the <video> element's identity -- it was just work nobody asked for, once per card. Two comments were wrong, which matters more than it sounds: the review chased both. The claim-link comment claimed geo-chat "hands these back as UUIDs". It does not -- it takes either spelling on the way in and answers in plain hex, verified against `chat-api-testnet`, so `uuidToHex` there is a no-op. The normalization stays, because what it guards is the hex-keyed side -- routes, the sync store `PrefetchLink` prefetches from, the side-panel target -- and it is the same guard `useDebateTranscriptClaims` puts at the same boundary. But it now says so. The column-width doc worked through numbers (569px at 900dvh, 651px at 1000dvh) that its own 560px cap makes unreachable; the cap binds from about 890px up. It now quotes the widths that actually occur. Two test fixtures were quietly unfalsifiable. `debate.id` and `claim_entity_id` were hyphenated, describing a response shape geo-chat never sends -- which is what invited a fix for an id mismatch that isn't there. And the comment and claims counts were both 3, so the assertion tying each count to its own source would have passed with the two wirings swapped. Now hex, and 3 against 18. Also names `opensGlobalCommentsPanel` once in the bar rather than deriving it inline in each orientation. Verified: full build green, 4943 tests across 449 files pass, eslint clean on the changed files (bar the two `_`-prefixed unused destructures the repo's own test mocks already carry).
Master moved 60+ commits under this branch, four files conflicted, and one of those commits had already shipped half of what this branch was doing. **The claim title is master's now, not this branch's.** #2458 landed GEO-2879 as `ExploreCardTitle`, and its version is better: it reads the claim from the Debate's own `Claims` relation, which the explore feed resolves before the card paints, where this branch read geo-chat's `debate.claim` behind a viewport gate that lands after. So this branch's title swapped from the entity name to the claim under a reader mid-scroll -- the exact failure master's version exists to avoid -- and it only headed the video card, where master's also heads the non-watchable fallback. `DebateCardTitle` is deleted; the card calls `ExploreCardTitle`. What it keeps from here is the two-line clamp, because this card's height budget is calculated against a two-line title and a third line is 23px the viewport was not promised. That went to `ExploreCardTitle` as an optional `className` -- only the debate card passes it, since only the debate card sizes fixed-aspect media from what the title leaves over. **Claims and Share now stand down with the player, not just with the debate.** Master made the media window non-sticky so leaving it unmounts the videos and releases them (GEO-2963), and its test asserts the Claims control goes with them. Hoisting the interaction state to the card root for the shared bar had quietly undone that: the transcript-claims query would have stayed subscribed for every off-screen row. Both now gate on `mediaMounted` -- resolved *and* in the window -- while votes and the comment count, which ask geo-chat nothing, stay for the whole row's life. The rest of the resolution: - The bar takes master's `Warning` claims icon (#2462 unified it) with this branch's optional Claims/Share and `commentsPanelOpen`. - `DebateCardVideos` keeps master's `{debate, active}` signature and its `playbackAllowed` veto, wrapped in the `React.memo` the review added. - The feed takes master's `useLineClampOverflow` refactor alongside `JoinDebateButton`. - `DebateCardExtras` is gone -- the shared bar replaced the grey icon buttons it held, which is also why the `ExploreShareIcon` master still imported can stay deleted. - The test file is master's, with this branch's join-button and shared-bar cases re-applied on top; master's own `claim heading` block covers what this branch's title tests did. `@geogenesis/auth` needed rebuilding for master's new exports. Verified: full build green, 6786 tests across 561 files pass, `tsc --noEmit` now completely clean -- master's #2488 fixed the three test files that used to fail it.
…912) Post-merge review. All four are regressions this branch introduced by replacing `EntityRowActions`/`EntityCommentsButton` with the shared bar, not pre-existing. **An open claims panel no longer vanishes, or reopens itself.** The open flags moved up to the card for the shared bar, but the panel stayed gated on `mediaMounted` with the controls. Both panels are fixed overlays over a feed that still scrolls, so scrolling ~800px past the card tore the panel away mid-read — and since the flag now lives somewhere that did not unmount with it, scrolling back reopened it unasked. The overlays gate on `readyDebate`; only the controls that open them stand down with the window. `DebateClaimsPanel` loads its own claims, so it does not go empty when the card's count stands down beside it. Test covers it, and fails without the fix. **The comment count is live again.** Master had put `useCommentCount` behind `EntityCommentsButton` so posting from the panel the pill opens updates the pill; dropping that button dropped it, and the number sat one behind its own list until a reload. The stated reason for passing the server count straight through — not fetching a thread per card — was wrong about that hook: it subscribes to the comments cache without enabling a query, so it costs no request. Its own docs even name this surface. **The counts are said, not just shown.** `aria-label` replaces a control's visible text for a screen reader, and the visible text on Comments and Claims *is* the number, so labelling them "Comments" and "Claims" dropped the one thing they report. `EntityCommentsButton` announced "Comments (3)"; the bar does now too, on both surfaces. Queries in both suites match on the prefix. **The heading tooltip stays on the card that needs it.** Adding `title` to `ExploreCardTitle` for the clamped debate heading put a native tooltip on every explore card title, most of which are not cut and so were describing themselves. Replaced the free-form `className` with one `clamped` flag: the clamp and the tooltip are one decision, and a heading that cannot be cut must not grow a tooltip. Verified: full build green, 6787 tests across 561 files, tsc and eslint clean.
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The responsive height calculation does not account for metadata wrapping at its minimum width.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Unifies Explore debate cards with the full-screen debate experience.
Changes:
- Shares debate interaction and join controls across both surfaces.
- Adds responsive sizing and clamped claim titles.
- Expands tests for interactions, authentication, and viewport behavior.
| File | Description |
|---|---|
apps/web/partials/explore/explore-share-icon.tsx |
Removes obsolete share icon. |
apps/web/partials/explore/explore-card-title.tsx |
Adds optional title clamping. |
apps/web/partials/explore/debate-explore-feed-card.tsx |
Adopts shared controls and responsive layout. |
apps/web/partials/explore/debate-explore-feed-card.test.tsx |
Tests the unified card behavior. |
apps/web/core/debates/browse/join-debate-button.tsx |
Introduces the shared join control. |
apps/web/core/debates/browse/debate-interaction-bar.tsx |
Supports optional actions and global comments. |
apps/web/core/debates/browse/debate-feed.tsx |
Uses the shared join control. |
apps/web/core/debates/browse/debate-feed.test.tsx |
Updates accessible-label assertions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…d control (GEO-2912) The card's corner is a fullscreen expand icon now, linking to this debate at full size, where it held "Join a debate" and before that "View all". This closes a gap GEO-2879 opened and named in its own notes: once the heading became the claim, nothing on the card pointed at the debate any more, and that ticket asked where the path to the full-screen debate would go. Here — as a control that says "bigger" rather than one competing with the claim for the heading. The Debate entity's own page is that path, not `debatePath`: `debatePath` is the live recording room, while `DebateEntityView` renders the entity page as the `/debates` feed anchored to this debate, which is the full-screen experience with this debate on top. So this is the entity link the heading used to be, moved to its own control. An anchor with a real href, so cmd-click still opens a tab (GEO-2701), carrying the `Fullscreen` icon and the chrome the entity side panel's own "open this full page" control uses. That icon existed and had no callers; this is not the place to invent a second answer to the same question. **`JoinDebateButton` is reverted with it.** It was extracted so the card and the full-screen feed could not answer a press differently -- and with the card no longer asking, it served one caller, which is a refactor rather than shared code. So `debate-feed.tsx` goes back to master's version byte for byte and the new file is deleted, which takes ~80 lines of diff out of this PR for no change in behaviour. The feed's seven "Join a debate" cases are master's again and pass untouched. Easy to reinstate if a second surface ever needs it. The card suite loses the hub, Privy and geo-chat-auth mocks it only needed for that button, and gains cases for where the expand control points and that it is offered before the geo-chat lookups land -- the route resolves the debate itself, so it needs none of them. Verified in the app: the icon lands on the same debate full screen with playback continuing, its right edge still flush with the videos at 940px, and the card still fits the viewport (814px against 856px). Full build green, 6784 tests across 561 files, tsc and eslint clean.
…e card (GEO-2912) Drops the circular button chrome from the card's full-screen control: it is the plain grey glyph a data block's header uses, which is what the reader has already learned that offer looks like. Shared rather than restyled. `data-block-expand-control` had the treatment in a local class string, and the honest way to make the card match is not to copy that string — the styling *is* the whole contract here, so a copy is a copy that drifts. Both now render `FullscreenLink`: 24px, transparent, `grey-04`, no border, with the data block's own hover, focus ring and inert-while-disabled behaviour. Same reasoning, and the same "one definition so these cannot drift apart again", as `vote-button-styles`. It lives in the design system because the two callers are peers in different trees — a data block header in `partials/blocks/table`, an explore card in `partials/explore` — and neither should be importing chrome out of the other. An anchor still, never a button, so modifier-click opens a tab and "copy link address" works (GEO-2701). `DataBlockExpandControl` keeps its own `onClick`/`IconButton` branch: some surfaces expand in place rather than navigating, and that is a button's job. Verified in the app: bare glyph, right edge still flush with the videos at 940px, card still fits the viewport (810px against 856px), and the icon still lands on the same debate full screen. Full build green, 6784 tests across 561 files, tsc and eslint clean — including the data block's own 52 files.
…ify-debate-card-with-full-screen
…e smaller things (GEO-2912) Third review pass, on the branch merged up to master (5 more commits, no conflicts). **The vote pill can find a debate's own votes again.** The shared bar hardcoded `responseKind="curation"`, which is not just a kind: naming one skips `EntityVoteButtons`' entity lookup, and with it `resolveEntitySpaceId` — the thing that diverts counts and votes to the space an entity actually lives in when a surface is listing it from somewhere else (GEO-2660). The card used to infer, so it had that. It is reachable: `use-block-explore-feed-item` sets `item.spaceId` from the row's name cell, and routes the card on unscoped `cardTypes`, so a data block row carrying a debate from another space renders a debate card whose votes were being read against the listing space — where there are none. The bar takes `responseKind` now, defaulting to the fixed kind the full-screen feed wants (it only ever shows a space its own debates) and set to `'infer'` by the card. A debate infers to `curation` either way, so this changes which space is read, not how the control looks. **The comments control no longer claims to open a dialog.** Feeding `commentsPanelOpen` into `expanded` also emitted `aria-haspopup="dialog"`, and the app's comments panel is an in-flow panel with no dialog role and no focus trap — `EntityCommentsButton` announced only `aria-expanded`. Split into its own `open` prop so `aria-haspopup` stays with the controls that really do open one, which Share still does. **The clamped heading drops its tooltip.** `title` was applied whenever clamping was on, not when the text was actually cut, so short claims got a tooltip repeating what was already on screen — contradicting the prop's own doc. Telling the difference means measuring, as the full-screen header does with `useLineClampOverflow`, and that is a ResizeObserver per card in an infinite feed to reveal what the heading's own link already opens. So: clamp, no tooltip. **`FullscreenLink`'s `disabled` is actually disabled.** `pointer-events-none` alone only stops the mouse — the anchor stayed tabbable and Enter still followed it, so a keyboard could reach what a pointer could not. It leaves the tab order and says `aria-disabled` now, which is what that class was always meant to mean, and what the doc I wrote for it already claimed. Verified: full build green, 6833 tests across 570 files, tsc and eslint clean. New cases cover the inferred response kind and the dialog announcements on both controls.
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The implementation omits the specified shared join action and has unresolved disabled-link and clamped-title behavior.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 4
…ct the card's height note (GEO-2912)
Copilot review. Two of its four findings were worth acting on; the reply on each thread
says which and why.
**A disabled `FullscreenLink` now refuses the navigation.** Last round I added
`tabIndex={-1}` and `aria-disabled` so the doc's "inert" claim would be true. It still
was not: `pointer-events-none` stops a mouse, `tabIndex` stops Tab, `aria-disabled`
only says so, and none of them stop an activation arriving another way — assistive
tech, a programmatic `focus()` and Enter, a synthesized click. The `href` has to stay
for the enabled case, so the click is cancelled instead. It is the only control in
this PR that renders while inert; the other `pointer-events-none` in the diff are
decorative overlays.
**The height note said 28px for a meta row that measures 24px**, and reasoned from a
per-pixel decomposition precise enough to be wrong again the next time a padding
moves. It now states what is checkable: the media formula (predicted 682.3px against
682px measured on a 560px column), the ~170px the clamp reserves, and the 128–151px
the chrome actually spans — so the slack is named rather than implied.
That also answers the finding behind it. The meta row is `flex-wrap` and its widest
state does take a second line: "Membership pending" beside a long space name measures
41px against the usual 24px. But it only reaches that at the 320px floor, which is
where the clamp has already stopped binding, and even inside the binding range the
worst case is 168px against 170px reserved. Measured across widths 390–1440 and
heights 600–1000 with that state forced: nothing overflows. So the row keeps wrapping
rather than growing a truncation strategy, and the note now explains why that is safe
instead of assuming a fixed height.
Verified: full build green, 6835 tests across 571 files, tsc and eslint clean. The new
`fullscreen-link` suite fails without the click fix — checked by removing it.
jwalkingjew
marked this pull request as ready for review
September 21, 2026 21:43
jwalkingjew
deleted the
preston/geo-2912-unify-debate-card-with-full-screen
branch
September 21, 2026 21:43
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes GEO-2912. Rebased onto master, which shipped GEO-2879 separately in #2458 — see Merged with master below.
The gap
The videos were already one component.
DebateFeedPlayerrenders both surfaces, so the debater tiles, the name and Agree/Disagree chips, and the winner share pill have always matched.Nothing around them did. Full screen renders a header with "Join a debate" in its corner, the claim as the title, the videos, and
DebateInteractionBarbeneath or beside them. The card rendered a "View all" link, the Debate entity's name as the title, the videos, and the genericEntityRowActionsunderneath — inline vote arrows plus two grey icon buttons drawn only for this one card.The change
The card now renders the same claim title over the same videos over the same bar, in the same arrangement.
ExploreCardTitle, #2458)Merged with master
Master moved 60+ commits under this branch and four files conflicted. Two resolutions are worth calling out, because in both cases master's behaviour won over this branch's.
The claim title is master's, not this branch's. #2458 landed GEO-2879 as
ExploreCardTitlewhile this was in review, and its version is better: it reads the claim from the Debate's ownClaimsrelation, which the explore feed resolves before the card paints. This branch read geo-chat'sdebate.claim, which is behind a viewport gate that lands after — so the title swapped from the entity name to the claim under a reader mid-scroll, the exact failure master's version exists to avoid. Master's also heads the non-watchable fallback card, where this branch's only headed the video card.DebateCardTitleis deleted.What carried over is the two-line clamp, because this card's height budget is calculated against a two-line title and a third line is 23px the viewport was not promised. That went onto
ExploreCardTitleas an optionalclassName, passed only by the debate card — it is the only card that sizes fixed-aspect media from what the title leaves over.Claims and Share now stand down with the player, not just with the debate. Master made the media window non-sticky, so leaving it unmounts the videos and releases them (GEO-2963), and its test asserts the Claims control goes with them. Hoisting the interaction state to the card root for the shared bar had quietly undone that — the transcript-claims query would have stayed subscribed for every off-screen row. Both controls now gate on
mediaMounted(resolved and in the window), while votes and the comment count, which ask geo-chat nothing, stay for the whole row's life.The rest: the bar takes master's
Warningclaims icon (#2462),DebateCardVideoskeeps master's{debate, active}signature andplaybackAllowedveto, the feed takes master'suseLineClampOverflowrefactor,DebateCardExtrasis gone (the shared bar replaced the grey icon buttons it held), and the test file is master's with this branch's join-button and shared-bar cases re-applied.The full-screen control
The corner is a bare expand glyph linking to this debate at full size. It replaced "Join a debate", which had replaced "View all".
This closes a gap GEO-2879 named in its own notes: once the heading became the claim, nothing on the card pointed at the debate any more, and that ticket asked where the path to the full-screen debate would go.
The Debate entity's own page is that path — not
debatePath, which is the live recording room.DebateEntityViewrenders the entity page as the/debatesfeed anchored to this debate, i.e. the full-screen experience with this debate on top.Styling is shared, not copied: both this and a data block's header render
FullscreenLink, so the same offer looks the same in both places. It stays an anchor with a realhref, so modifier-click opens a tab (GEO-2701).An earlier revision of this PR extracted
JoinDebateButtonto share that button between the card and the feed. That is reverted — with the card no longer joining, it served one caller, which is a refactor rather than shared code.debate-feed.tsxis back to master's version byte for byte and the feed's seven "Join a debate" cases are master's again, passing untouched.The bar
Beneath the videos and the same width as them, inside the capped column. Full screen carries this bar in a rail down the media's right at desktop widths and moves it below at narrow ones; a card is short and wide where full screen is tall, so it takes the horizontal arrangement at every width. Same component, same controls, same counts — only the axis differs, and that is a difference full screen already makes between its own breakpoints.
Width
The column was a flat 480px, which on a tall display left the card smaller than it needed to be and on a short one made it taller than the viewport. Width is what sets height — the tiles are fixed-aspect — so widening the card and fitting it vertically are the same problem, and the answer is to derive the width from the height available.
Measured: the media is
1.2042 × width + 8, and the rest of the card is a flat 163px (32px ofpy-4, a 28px meta row, a two-line 46px title, a 32px bar with its margin, 24px of column gaps, the 1px rule). A card is therefore1.2042 × width + 171tall, and keeping that under the viewport less the 44px app header giveswidth ≤ 0.83 × dvh − 178px.Hence
clamp(320px, calc(83dvh - 178px), 560px)— the same trick, and the same reason, as--debate-feed-column-widthon the full-screen feed. Verified at 1440px wide against the real 44px header:So it is wider than before wherever there is room (560px from a 900px viewport up, against the old 480px) and narrower where there isn't. The 320px floor gives the promise up below roughly a 600px viewport, where honouring it would mean a video too small to read a face in.
The claim title is clamped to two lines for this to hold — a third line is 23px the viewport was not promised — which is also what the full-screen header does to the same claim. No
titletooltip with it: clamping is not truncation, most claims fit in two lines, and a tooltip repeating visible text is worse than none. Telling the difference needs aResizeObserverper card to reveal what the heading's own link already opens.Two seams added to the shared bar
Both about where a control leads, not what it looks like:
commentsPanelOpenmarks the comment control as an opener of the app's global comments panel. Explore routes comments there, as every other explore card does, and that panel recognises its openers bydata-entity-comments-opener. Without it, pressing the control while the panel is open reads as an outside click and dismisses the panel instead of switching it to this debate. Left undefined by the full-screen feed, which opens a panel it controls itself.Counts stay the card's own — the comment count the explore feed already resolved (so a page of debates doesn't fetch a thread apiece) and the shared transcript-claims query the Claims panel itself reads from.
explore-claims-icon.tsxandexplore-share-icon.tsxexisted only for the two buttons this replaces, and are deleted with them.Still not unified, deliberately
The meta row. Full screen shows space · topics + "Join a debate"; the card shows space + Join chip + "Debate" tag + timestamp + the expand control. That row is shared explore-card chrome, and a debate card that disagreed with the cards above and below it would trade one inconsistency for another.
A call worth confirming
The ticket says full screen is the reference "rather than meeting in the middle", and this follows that. The cost: on Explore the debate card's actions now read as bordered pills while neighbouring cards keep the plain arrow row. In practice it sits fine — claim cards have their own Agree/Disagree treatment anyway — but it is a real trade of explore-internal consistency for cross-surface consistency, and it is yours to confirm.
Verification
tsc --noEmitand eslint clean./explore; the bar sits beneath the videos spanning their width, with one vote control and one Share per card; the expand control sits flush with the videos' right edge and lands on the same debate full screen with playback continuing; Claims opens the shared panel with the right count; comments open the global panel and a second press keeps it open.