From c630d1cef4fc1afdf70fee956646ae2d19e91609 Mon Sep 17 00:00:00 2001 From: eti Date: Fri, 9 Oct 2026 23:58:15 +0200 Subject: [PATCH] web/diff: animate diff file tree expand collapse Signed-off-by: eti --- .../repo/DiffFileTree.stories.svelte | 12 +++--- .../lib/components/repo/DiffFileTree.svelte | 41 ++++++++++++------- web/src/lib/components/repo/DiffView.svelte | 2 +- web/src/lib/motion/reveal.ts | 30 ++++++++++++++ web/src/lib/motion/revealWidth.ts | 13 ------ .../[version]/[[range]]/PullViewPage.svelte | 2 +- 6 files changed, 65 insertions(+), 35 deletions(-) create mode 100644 web/src/lib/motion/reveal.ts delete mode 100644 web/src/lib/motion/revealWidth.ts diff --git a/web/src/lib/components/repo/DiffFileTree.stories.svelte b/web/src/lib/components/repo/DiffFileTree.stories.svelte index 6b0eb5638..9c7bf98ae 100644 --- a/web/src/lib/components/repo/DiffFileTree.stories.svelte +++ b/web/src/lib/components/repo/DiffFileTree.stories.svelte @@ -25,16 +25,16 @@ "#file-package.json" ); await expect(canvas.getByText("repo")).toBeVisible(); - expect(canvasElement.querySelectorAll("details").length).toBe(5); + expect(canvasElement.querySelectorAll("[aria-expanded]").length).toBe(5); - const firstDir = canvasElement.querySelector("details")!; - expect(firstDir.open).toBe(true); - await userEvent.click(firstDir.querySelector("summary")!); - expect(firstDir.open).toBe(false); + const firstDir = canvas.getByRole("button", { name: "src" }); + await expect(firstDir).toHaveAttribute("aria-expanded", "true"); + await userEvent.click(firstDir); + await expect(firstDir).toHaveAttribute("aria-expanded", "false"); }; const flatList = async ({ canvas, canvasElement }: PlayContext) => { - expect(canvasElement.querySelector("details")).toBeNull(); + expect(canvasElement.querySelector("[aria-expanded]")).toBeNull(); await expect(canvas.getAllByRole("link")).toHaveLength(2); }; diff --git a/web/src/lib/components/repo/DiffFileTree.svelte b/web/src/lib/components/repo/DiffFileTree.svelte index 5d91576aa..27782f914 100644 --- a/web/src/lib/components/repo/DiffFileTree.svelte +++ b/web/src/lib/components/repo/DiffFileTree.svelte @@ -12,7 +12,8 @@ variants: { kind: { dir: { - row: "cursor-pointer list-none", + row: "cursor-pointer text-left", + icon: "transition-[rotate] duration-(--fold-duration) ease-(--fold-easing) group-aria-expanded:rotate-90", name: "text-foreground-muted group-hover:text-inherit" }, file: { row: "flex items-center gap-xs" } @@ -61,8 +62,10 @@ @@ -85,19 +91,26 @@ {#each list as node (`${node.type}:${node.path}`)} {#if node.type === "dir"} {@const open = !closed[node.path]} -
(closed[node.path] = !e.currentTarget.open)}> - - {#if open} - -
+ {@const id = `${uid}-${node.path}`} + + {#if open} +
{@render nodes(node.children)}
-
+ {/if} {:else} @@ -119,6 +132,6 @@ {/each} {/snippet} -
+
{@render nodes(tree)}
diff --git a/web/src/lib/components/repo/DiffView.svelte b/web/src/lib/components/repo/DiffView.svelte index f9a228e7b..7711307e4 100644 --- a/web/src/lib/components/repo/DiffView.svelte +++ b/web/src/lib/components/repo/DiffView.svelte @@ -13,7 +13,7 @@ import UnfoldVertical from "$icon/unfold-vertical"; import { prefersReducedMotion } from "svelte/motion"; import { RAIL_DURATION, railEasing } from "$lib/motion/spring"; - import { revealWidth } from "$lib/motion/revealWidth"; + import { revealWidth } from "$lib/motion/reveal"; import Button, { buttonIcon } from "$lib/components/ui/Button.svelte"; import EmptyState from "$lib/components/ui/EmptyState.svelte"; import Segment from "$lib/components/ui/Segment.svelte"; diff --git a/web/src/lib/motion/reveal.ts b/web/src/lib/motion/reveal.ts new file mode 100644 index 000000000..af268d5ce --- /dev/null +++ b/web/src/lib/motion/reveal.ts @@ -0,0 +1,30 @@ +import type { TransitionConfig } from "svelte/transition"; + +interface RevealParams { + duration: number; + easing: (t: number) => number; +} + +export const revealWidth = ( + node: Element, + { duration, easing }: RevealParams +): TransitionConfig => { + const width = node.getBoundingClientRect().width; + return { + duration, + easing, + css: (t) => `overflow: clip; width: ${t * width}px; min-width: 0` + }; +}; + +export const revealHeight = ( + node: Element, + { duration, easing }: RevealParams +): TransitionConfig => { + const height = node.getBoundingClientRect().height; + return { + duration, + easing, + css: (t) => `overflow: clip; height: ${t * height}px; min-height: 0; opacity: ${t}` + }; +}; diff --git a/web/src/lib/motion/revealWidth.ts b/web/src/lib/motion/revealWidth.ts deleted file mode 100644 index ed617da43..000000000 --- a/web/src/lib/motion/revealWidth.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { TransitionConfig } from "svelte/transition"; - -export const revealWidth = ( - node: Element, - { duration, easing }: { duration: number; easing: (t: number) => number } -): TransitionConfig => { - const width = node.getBoundingClientRect().width; - return { - duration, - easing, - css: (t) => `overflow: clip; width: ${t * width}px; min-width: 0` - }; -}; diff --git a/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte b/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte index 749fb5988..1bd1c7f7b 100644 --- a/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte +++ b/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte @@ -83,7 +83,7 @@ RAIL_EASING, railEasing } from "$lib/motion/spring"; - import { revealWidth } from "$lib/motion/revealWidth"; + import { revealWidth } from "$lib/motion/reveal"; import { plural } from "$lib/format"; type PullViewData = PageData & -- 2.51.2