Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Meditor/CHANGELOG.md | 1+
Meditor/app/channels/[slug]/components/VideoListPaneSection.tsx | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Aeditor/e2e/channel-embedded-video.spec.ts | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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(); +});