Skip to content

Page transitions, shared-element morph, skeletons and scroll reveal - #10

Merged
Thiritin merged 2 commits into
mainfrom
motion-polish
Aug 3, 2026
Merged

Page transitions, shared-element morph, skeletons and scroll reveal#10
Thiritin merged 2 commits into
mainfrom
motion-polish

Conversation

@Thiritin

@Thiritin Thiritin commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Builds on the motion tokens already on main (--dur-*, --ease-*, the tile
TransitionGroup, the reduced-motion gate) and adds the pieces that were left over:
page transitions, a shared-element morph from tile to player, thumbnail skeletons,
scroll reveal, and a hover lift.

Page transitions

resources/js/viewTransitions.js wires the View Transitions API into the Inertia
router. Inertia has no "about to swap" hook, so the transition is held open across
the request and closed on finish, by which point the new page is in the DOM. The
DOM still mutates normally while it is held; only painting is suppressed.

Three guards make that safe:

  • MAX_FREEZE_MS = 600. While a transition is pending the browser suppresses
    paint, and that includes Inertia's own progress bar. Past the cap the transition
    is released un-animated so a slow visit degrades to a plain swap instead of
    looking hung.
  • preserveState and partial visits are skipped. Components/Manage/useTableQuery.js
    fires a router.get on every filter change and every debounced keystroke. A
    snapshot over the page each time would make typing feel broken. Same reasoning
    for the archive search.
  • Non-GET is skipped, and the whole thing is a no-op without
    document.startViewTransition or under prefers-reduced-motion.

Root crossfade plus an 8px rise at --dur-base.

Tiles and archive collection cards now use Inertia prefetch. That is what makes
the transition worth having: the response is usually already cached when the click
lands, so the morph starts immediately rather than after a round trip spent frozen.

Shared element (tile to player)

view-transition-name has to be unique document-wide, and ShowPlayer renders
both a player and a sidebar of tiles, so ownership lives in one module
(composables/useMediaHero.js) where claiming always strips the name off whoever
held it last.

  • Tiles claim on pointerdown, before Inertia starts the visit.
  • Players claim through a v-media-hero directive on mounted and updated
    Inertia reuses the player component between two show pages, so mounted only
    fires once, and a sidebar tile takes the name away when it is clicked.

Worth noting for review: releasing the name from a router.on('finish') handler
does not work. It strips the name before the browser captures the new page, so the
morph silently does not happen. The directive's ownership model replaced that.

Thumbnail skeletons

.media-skeleton reuses the archive's existing pending-sweep gradient, so
"image still loading" and "recording still processing" read as one family rather
than two separate inventions. It shows while a lazy thumbnail has not decoded yet,
which is a real state now that tile images are lazy — previously those slots were
flat bg-primary-800 boxes. Applied in both tiles and the archive collection art.

Scroll reveal

CSS-only, via animation-timeline: view(). No IntersectionObserver, nothing
running on the scroll path. Behind @supports (Firefox has no view timeline yet,
where the rule simply never applies) and prefers-reduced-motion: no-preference.

Applied to schedule rows and archive collection cards, and deliberately not to
.stream-grid tiles: those already animate in through <TransitionGroup>, and two
systems animating one element fight each other.

Tile hover

.media-tile scales to 1.03 and takes position: relative; z-index: 20 so the
hovered card sits over its neighbours. :focus-within is included so keyboard
users get the same affordance. Pending archive tiles are excluded, since they are
not clickable.

Testing

vite build passes and the generated CSS was checked for the new rules. Not
runtime-verified
— the browser profile was in use, so the transitions and the
morph have not been driven in a real page yet. Worth an eyeball on:

  • browse grid to a show, and archive tile to a recording (the morph)
  • a manage table filter and the archive search (should not transition)
  • schedule scrolled top to bottom (reveal)

Summary by CodeRabbit

  • New Features
    • Added smoother page transitions and shared artwork animations when opening recordings, shows, and streams.
    • Added loading skeletons for thumbnails and collection artwork while images load.
    • Added scroll-based reveal animations for collections and schedule entries.
    • Added prefetching for eligible recordings, shows, and collections.
  • Accessibility
    • Added reduced-motion support to disable transition effects when requested.
  • Visual Improvements
    • Added hover and focus scaling for interactive media tiles.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Thiritin, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f400e28-e6f5-4ad0-9605-27df7522b380

📥 Commits

Reviewing files that changed from the base of the PR and between e2f9255 and 29524f7.

📒 Files selected for processing (3)
  • resources/js/Components/Recordings/RecordingTile.vue
  • resources/js/Components/Shows/ShowTile.vue
  • resources/js/Pages/Archive/Index.vue
📝 Walkthrough

Walkthrough

The change adds media tile hover states, loading skeletons, scroll reveals, and View Transition animations. It adds shared media hero ownership for recording and show navigation, plus Inertia handling for eligible full-page GET visits.

Changes

Media experience transitions

Layer / File(s) Summary
Visual transition foundations
resources/css/app.css
CSS adds media tile scaling, thumbnail skeletons, scroll reveals, page cross-fades, media hero morphing, and reduced-motion handling.
Media tile and collection states
resources/js/Components/Recordings/RecordingTile.vue, resources/js/Components/Shows/ShowTile.vue, resources/js/Pages/Archive/Index.vue, resources/js/Pages/Schedule.vue
Tiles claim shared media heroes and prefetch routes. Thumbnail and collection artwork loading states now display skeletons and fade in after loading. Schedule rows use scroll-reveal styling.
Transition runtime and player endpoints
resources/js/composables/useMediaHero.js, resources/js/viewTransitions.js, resources/js/app.js, resources/js/Pages/RecordingPlayer.vue, resources/js/Pages/ShowPlayer.vue
The app registers the media hero directive and installs Inertia View Transition handling. Player components expose shared media hero targets. Eligible full-page GET visits hold and release transitions safely.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant RecordingTile
  participant Inertia
  participant RecordingPlayer
  User->>RecordingTile: pointer interaction
  RecordingTile->>RecordingTile: claim media hero
  RecordingTile->>Inertia: prefetch and navigate
  Inertia->>RecordingPlayer: complete full-page GET visit
  RecordingPlayer->>RecordingPlayer: apply media hero transition
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: page transitions, shared-element morphing, loading skeletons, and scroll reveal effects.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch motion-polish

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@resources/js/Components/Recordings/RecordingTile.vue`:
- Line 9: Update the activation handlers in
resources/js/Components/Recordings/RecordingTile.vue at lines 9-9 and
resources/js/Components/Shows/ShowTile.vue at lines 14-14 to claim thumbnail on
Enter keyboard activation, while preserving the existing pointerdown behavior
and isPending guard.

In `@resources/js/Pages/Archive/Index.vue`:
- Around line 71-81: Update the collection-art image flow around loadedArt and
TilePlaceholder to handle load failures: track failed artwork by collection.year
with an `@error` handler, then render TilePlaceholder when the image errors so the
media-skeleton is not shown indefinitely. Preserve the existing successful-load
behavior.

In `@resources/js/Pages/RecordingPlayer.vue`:
- Around line 28-30: Update StreamPlayer.vue to render through a single native
root element so inherited attributes such as v-media-hero are applied correctly.
Ensure both direct root branches are contained within that element; the
VideoPlayer usage in resources/js/Pages/RecordingPlayer.vue:28-30 and
resources/js/Pages/ShowPlayer.vue:510-511 require no direct changes because they
are corrected by this root-wrapper fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 93e513b3-2b84-4bce-b955-872e78b4ff6c

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc6b0a and e2f9255.

📒 Files selected for processing (10)
  • resources/css/app.css
  • resources/js/Components/Recordings/RecordingTile.vue
  • resources/js/Components/Shows/ShowTile.vue
  • resources/js/Pages/Archive/Index.vue
  • resources/js/Pages/RecordingPlayer.vue
  • resources/js/Pages/Schedule.vue
  • resources/js/Pages/ShowPlayer.vue
  • resources/js/app.js
  • resources/js/composables/useMediaHero.js
  • resources/js/viewTransitions.js

Comment thread resources/js/Components/Recordings/RecordingTile.vue Outdated
Comment thread resources/js/Pages/Archive/Index.vue
Comment on lines 28 to +30
<VideoPlayer
:key="playerKey"
v-media-hero

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline resources/js/Components/Player/VideoPlayer.vue --items all
ast-grep outline resources/js/Components/Livestream/StreamPlayer.vue --items all

sed -n '/<template>/,/<\/template>/p' resources/js/Components/Player/VideoPlayer.vue
sed -n '/<template>/,/<\/template>/p' resources/js/Components/Livestream/StreamPlayer.vue

Repository: Thiritin/streaming

Length of output: 2456


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Vue versions ---'
rg -n '"vue"|"`@vue/compiler`|vue/compiler' package.json package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null || true

printf '%s\n' '--- Component template root structure ---'
python3 - <<'PY'
from pathlib import Path
import re

files = [
    Path("resources/js/Components/Player/VideoPlayer.vue"),
    Path("resources/js/Components/Livestream/StreamPlayer.vue"),
]

tag_re = re.compile(r"<(/?)([A-Za-z][\w.-]*)(?:\s[^<>]*?)?(/?)>")
void = {"area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"}

for path in files:
    text = path.read_text()
    template = re.search(r"<template>(.*?)</template>", text, re.S).group(1)
    roots = []
    depth = 0
    for m in tag_re.finditer(template):
        closing, name, self_close = m.groups()
        if name in {"template"}:
            continue
        if closing:
            depth -= 1
        elif depth == 0:
            roots.append((name, "self-closing" if self_close or name.lower() in void else "element"))
        if not closing and not self_close and name.lower() not in void:
            depth += 1
    print(f"{path}: top-level native nodes = {roots}")
PY

printf '%s\n' '--- Exact templates ---'
sed -n '/<template>/,/<\/template>/p' resources/js/Components/Player/VideoPlayer.vue
sed -n '/<template>/,/<\/template>/p' resources/js/Components/Livestream/StreamPlayer.vue

Repository: Thiritin/streaming

Length of output: 5247


🌐 Web query:

Vue 3 fallthrough attributes component root v-if v-else single root directive on component

💡 Result:

In Vue 3, the automatic application of fallthrough attributes depends on the component having a single root node [1][2]. When a component renders a single root element, Vue automatically applies any attributes or event listeners not declared as props or emits to that root element [1][3]. However, if a component contains multiple root nodes, automatic fallthrough is disabled, and Vue will issue a runtime warning because it cannot determine which element should inherit the attributes [1][2]. In such cases, you must explicitly bind the attributes using v-bind="$attrs" to a specific element [1][2]. Regarding the interaction with v-if and v-else: - Structure as a single root: If your component has a single container wrapping the v-if/v-else logic (e.g., a single div surrounding the conditional branches), Vue treats it as a single-root component, and fallthrough attributes will be applied to that container [1][4]. - Structure as multiple roots: If the v-if and v-else elements are the direct children of the component (making them multiple root nodes), automatic attribute inheritance will not occur, and you will receive a warning [1][5]. - Note on Comments/Nodes: In certain scenarios, injecting comments or text nodes between conditional branches at the root level can cause Vue to interpret the fragment incorrectly, potentially affecting how fallthrough attributes are identified [5][6]. It is generally recommended to use a single wrapper element if you rely on automatic fallthrough behavior or, if using multiple roots, to explicitly handle attributes using inheritAttrs: false and v-bind="$attrs" [1][5]. If you wish to apply attributes to a specific element within a single-root component rather than the root itself, you must set inheritAttrs: false in your component options to disable the default automatic inheritance, then apply the attributes manually using v-bind="$attrs" on the desired target [1][4][3]. Failure to disable inheritAttrs while also using v-bind="$attrs" will result in attributes being applied to both the root element and the specific element you targeted [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ -d node_modules/@vue/compiler-sfc ]; then
  node - <<'JS'
const { compileTemplate } = require('`@vue/compiler-sfc`')
for (const source of [
  '<template><div v-if="ok"></div><div v-else></div></template>',
  '<template><div v-if="ok"><span /></div><div v-else><p /></div></template>',
]) {
  const result = compileTemplate({ source, filename: 'Probe.vue' })
  console.log(result.code)
  console.error(result.errors)
}
JS
else
  echo 'node_modules/@vue/compiler-sfc is unavailable; compiler probe not run'
fi

printf '%s\n' '--- Vue compiler/runtime source references in tracked files ---'
rg -n 'fall.?through|single root|multiple root|root node|INSTANCE_ATTRS' resources package.json package-lock.json 2>/dev/null || true

Repository: Thiritin/streaming

Length of output: 418


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ -d node_modules/@vue/compiler-sfc ]; then
  node - <<'JS'
const { compileTemplate } = require('`@vue/compiler-sfc`')
for (const source of [
  '<template><div v-if="ok"></div><div v-else></div></template>',
  '<template><div v-if="ok"><span /></div><div v-else><p /></div></template>',
]) {
  const result = compileTemplate({ source, filename: 'Probe.vue' })
  console.log(result.code)
  console.error(result.errors)
}
JS
else
  echo 'node_modules/@vue/compiler-sfc is unavailable; compiler probe not run'
fi

Repository: Thiritin/streaming

Length of output: 226


Wrap StreamPlayer in one native root element. VideoPlayer.vue already has one root. StreamPlayer.vue has two direct root branches, so v-media-hero does not fall through to either branch.

📍 Affects 2 files
  • resources/js/Pages/RecordingPlayer.vue#L28-L30 (this comment)
  • resources/js/Pages/ShowPlayer.vue#L510-L511
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/Pages/RecordingPlayer.vue` around lines 28 - 30, Update
StreamPlayer.vue to render through a single native root element so inherited
attributes such as v-media-hero are applied correctly. Ensure both direct root
branches are contained within that element; the VideoPlayer usage in
resources/js/Pages/RecordingPlayer.vue:28-30 and
resources/js/Pages/ShowPlayer.vue:510-511 require no direct changes because they
are corrected by this root-wrapper fix.

@Thiritin
Thiritin merged commit 9073d59 into main Aug 3, 2026
1 of 2 checks passed
@Thiritin
Thiritin deleted the motion-polish branch August 3, 2026 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant