commit 2b9b20cc23d7d77056542d7cf46dbf18ec89997b
parent d336209bb68118450b4680d77f39dd6da3b2310b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sun, 24 May 2026 21:43:53 -0400
collapsed embedded video view in channel view
Diffstat:
3 files changed, 147 insertions(+), 6 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **The embedded single-video view on the channel page is now collapsed by default**, keeping the channel view compact. Clicking a video in the list expands the panel and scrolls to it; navigating away from a video (no video selected) collapses it again. A manual collapse/expand toggle is available on the panel, mirroring the video list's existing collapse.
- **Unlisted videos now get their own clickable list in the channel availability diagnostics**, alongside Deleted/Private/Members-only/Needs-auth/Error. Previously unlisted videos were only shown as a count, so there was no way to jump to the specific videos.
- **Cutting a release now clears `## [Unreleased]` entirely** instead of leaving an empty heading sitting above the new dated section. The `/changelog` and `/deploy` pages stop showing a blank Unreleased block after a release; the heading reappears the next time somebody hand-adds a pending bullet to the file.
diff --git a/editor/app/channels/[slug]/components/VideoListPaneSection.tsx b/editor/app/channels/[slug]/components/VideoListPaneSection.tsx
@@ -1,9 +1,19 @@
"use client";
-import { useEffect, useState, type ReactNode } from "react";
+import { useEffect, useRef, useState, type ReactNode } from "react";
+import { useSearchParams } from "next/navigation";
const STORAGE_KEY = "vlp:collapsed";
+// Resolve the lg+ grid columns for every left/right collapse combination. When
+// the right (embedded single-video) pane is expanded it is the primary content
+// area; when collapsed it shrinks to a thin strip and the list takes the slack.
+function gridTemplate(leftCollapsed: boolean, rightCollapsed: boolean): string {
+ const left = leftCollapsed ? "2.25rem" : "24rem";
+ if (!rightCollapsed) return `${left} minmax(0,1fr)`;
+ return "minmax(0,1fr) 2.25rem";
+}
+
export function VideoListPaneSection({
left,
right,
@@ -11,8 +21,14 @@ export function VideoListPaneSection({
left: ReactNode;
right: ReactNode;
}) {
+ const searchParams = useSearchParams();
+ const videoParam = searchParams.get("video");
const [collapsed, setCollapsed] = useState(false);
+ // The embedded single-video pane is collapsed by default and its open state
+ // is derived from the selection (see effect below), not persisted.
+ const [rightCollapsed, setRightCollapsed] = useState(true);
const [hydrated, setHydrated] = useState(false);
+ const rightRef = useRef<HTMLDivElement>(null);
useEffect(() => {
try {
@@ -32,14 +48,34 @@ export function VideoListPaneSection({
}
}, [collapsed, hydrated]);
- const gridCols = collapsed
- ? "lg:grid-cols-[2.25rem_minmax(0,1fr)]"
- : "lg:grid-cols-[24rem_minmax(0,1fr)]";
+ // Selection-driven embedded pane: collapse when no video is selected, expand
+ // and jump to it when one is (this overrides any manual collapse). Keyed on
+ // videoParam so prev/next navigation inside the panel re-scrolls too.
+ useEffect(() => {
+ if (!hydrated) return;
+ if (!videoParam) {
+ setRightCollapsed(true);
+ return;
+ }
+ setRightCollapsed(false);
+ const el = rightRef.current;
+ if (!el) return;
+ // Defer two frames so the grid column resizes and the panel mounts before
+ // we scroll, otherwise the target hasn't reached its final position yet.
+ requestAnimationFrame(() =>
+ requestAnimationFrame(() =>
+ el.scrollIntoView({ block: "start", behavior: "smooth" }),
+ ),
+ );
+ }, [videoParam, hydrated]);
return (
<section
aria-label="videos"
- className={`grid ${gridCols} gap-4 lg:gap-6 min-h-[calc(100vh-22rem)]`}
+ style={
+ { "--vlp-cols": gridTemplate(collapsed, rightCollapsed) } as React.CSSProperties
+ }
+ className="grid grid-cols-1 lg:grid-cols-[var(--vlp-cols)] gap-4 lg:gap-6 min-h-[calc(100vh-22rem)]"
>
<div className="flex flex-col min-h-0 lg:sticky lg:self-start lg:top-28 lg:max-h-[80vh]">
{collapsed ? (
@@ -70,7 +106,38 @@ export function VideoListPaneSection({
</>
)}
</div>
- <div className="min-w-0">{right}</div>
+ <div ref={rightRef} className="min-w-0 scroll-mt-28">
+ {rightCollapsed ? (
+ <>
+ <button
+ type="button"
+ onClick={() => setRightCollapsed(false)}
+ aria-expanded={false}
+ aria-label="expand video"
+ className="hidden lg:flex items-center justify-center w-full h-12 rounded border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 hover:bg-zinc-100 dark:hover:bg-zinc-800"
+ title="Expand video"
+ >
+ <span aria-hidden="true" className="text-sm">‹</span>
+ </button>
+ <div className="lg:hidden">{right}</div>
+ </>
+ ) : (
+ <>
+ <button
+ type="button"
+ onClick={() => setRightCollapsed(true)}
+ aria-expanded={true}
+ aria-label="collapse video"
+ className="hidden lg:flex self-start items-center gap-1 text-xs text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 mb-1"
+ title="Collapse video"
+ >
+ <span aria-hidden="true">›</span>
+ <span>Collapse</span>
+ </button>
+ {right}
+ </>
+ )}
+ </div>
</section>
);
}
diff --git a/editor/e2e/channel-embedded-video.spec.ts b/editor/e2e/channel-embedded-video.spec.ts
@@ -0,0 +1,73 @@
+import { test, expect } from "@playwright/test";
+import { resetData } from "./helpers";
+
+// The embedded single-video view on the channel page is collapsed by default and
+// driven by the ?video= selection: clicking a video expands it and scrolls to it,
+// clearing the selection collapses it again.
+
+test("embedded video view is collapsed by default and expands on selection", async ({
+ page,
+}) => {
+ await resetData("one-transcribe-channel-with-audio");
+ await page.goto("/channels/test-transcribe");
+
+ // No video selected: the thin-strip expand button is shown, the panel is not.
+ await expect(page.getByLabel("expand video", { exact: true })).toBeVisible();
+ await expect(page.getByLabel("collapse video", { exact: true })).toHaveCount(0);
+ await expect(page.getByLabel("pipeline status")).toBeHidden();
+
+ // Click a video in the list -> URL updates, panel expands and scrolls in.
+ await page.getByLabel("open vidA").click();
+ await expect(page).toHaveURL(/[?&]video=vidA\b/);
+ await expect(page.getByLabel("expand video", { exact: true })).toHaveCount(0);
+ await expect(page.getByLabel("pipeline status")).toBeVisible();
+ // The page jumped to the panel: its top (the collapse toggle) is in view.
+ await expect(page.getByLabel("collapse video", { exact: true })).toBeInViewport();
+
+ // Selecting a different video keeps the panel expanded.
+ await page.getByLabel("open vidB").click();
+ await expect(page).toHaveURL(/[?&]video=vidB\b/);
+ await expect(page.getByLabel("collapse video", { exact: true })).toBeVisible();
+ await expect(page.getByLabel("files for vidB")).toBeVisible();
+});
+
+test("embedded video view auto-collapses when the selection is cleared", async ({
+ page,
+}) => {
+ await resetData("one-transcribe-channel-with-audio");
+ await page.goto("/channels/test-transcribe?video=vidA");
+
+ // Deep-linked with a selection: starts expanded.
+ await expect(page.getByLabel("collapse video", { exact: true })).toBeVisible();
+ await expect(page.getByLabel("files for vidA")).toBeVisible();
+
+ // Manually collapse via the toggle.
+ await page.getByLabel("collapse video", { exact: true }).click();
+ await expect(page.getByLabel("expand video", { exact: true })).toBeVisible();
+
+ // Selecting another video re-expands (manual collapse is overridden by a
+ // change in selection).
+ await page.getByLabel("open vidB").click();
+ await expect(page.getByLabel("collapse video", { exact: true })).toBeVisible();
+ await expect(page.getByLabel("files for vidB")).toBeVisible();
+
+ // Clearing the selection auto-collapses.
+ await page.goto("/channels/test-transcribe");
+ await expect(page.getByLabel("expand video", { exact: true })).toBeVisible();
+ await expect(page.getByLabel("collapse video", { exact: true })).toHaveCount(0);
+});
+
+test("on mobile the panel stacks below the list without a collapse strip", async ({
+ page,
+}) => {
+ await page.setViewportSize({ width: 600, height: 900 });
+ await resetData("one-transcribe-channel-with-audio");
+ await page.goto("/channels/test-transcribe");
+
+ // The desktop-only collapse affordances are hidden; the placeholder shows.
+ await expect(page.getByLabel("expand video", { exact: true })).toBeHidden();
+ await expect(page.getByLabel("collapse video", { exact: true })).toHaveCount(0);
+ await expect(
+ page.getByText("Select a video from the list to view actions."),
+ ).toBeVisible();
+});