diff --git a/web/src/app.css b/web/src/app.css index ce44df43e..99b02762b 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -3,6 +3,7 @@ @import "./markup.css"; @import "./pierre.css"; @import "./logs.css"; +@import "./listView.css"; @import "./theme.css"; @import "./fonts.css"; diff --git a/web/src/lib/components/profile/ListViewSwitch.svelte b/web/src/lib/components/profile/ListViewSwitch.svelte new file mode 100644 index 000000000..81b028f17 --- /dev/null +++ b/web/src/lib/components/profile/ListViewSwitch.svelte @@ -0,0 +1,26 @@ + + + + showListView("cards")} + /> + showListView("list")} + /> + diff --git a/web/src/lib/components/profile/PagedList.svelte b/web/src/lib/components/profile/PagedList.svelte index ba6a89ee8..881167ece 100644 --- a/web/src/lib/components/profile/PagedList.svelte +++ b/web/src/lib/components/profile/PagedList.svelte @@ -1,6 +1,11 @@ - +{#snippet actions()} + +{/snippet} + + {#if loading} 1} - + +{#snippet actions()} + +{/snippet} + - + diff --git a/web/src/lib/components/profile/StringCard.svelte b/web/src/lib/components/profile/StringCard.svelte index 9bd04c535..7adb220a9 100644 --- a/web/src/lib/components/profile/StringCard.svelte +++ b/web/src/lib/components/profile/StringCard.svelte @@ -5,15 +5,17 @@ let { entry, + layout = "card", border = true, background = true }: { entry: StringCardData; + layout?: "card" | "row"; border?: CardVariants["border"]; background?: CardVariants["background"]; } = $props(); - + diff --git a/web/src/lib/components/profile/StringCardContent.svelte b/web/src/lib/components/profile/StringCardContent.svelte index 0daac0e97..6f9a7fc8b 100644 --- a/web/src/lib/components/profile/StringCardContent.svelte +++ b/web/src/lib/components/profile/StringCardContent.svelte @@ -4,13 +4,18 @@ import { compactRelativeTime } from "$lib/format"; import type { StringCardData } from "$lib/components/profile/types"; - let { entry }: { entry: StringCardData } = $props(); + let { entry, layout = "card" }: { entry: StringCardData; layout?: "card" | "row" } = $props(); - - + + - {#if entry.description} - + {#if layout === "card" && entry.description} + {entry.description} {/if} @@ -29,6 +37,7 @@ {#if entry.sizeLabel} {entry.sizeLabel} diff --git a/web/src/lib/components/profile/listView.svelte.ts b/web/src/lib/components/profile/listView.svelte.ts new file mode 100644 index 000000000..bdb2187cd --- /dev/null +++ b/web/src/lib/components/profile/listView.svelte.ts @@ -0,0 +1,28 @@ +import { tick } from "svelte"; + +export type ListView = "cards" | "list"; + +export const listView = $state<{ current: ListView }>({ current: "cards" }); + +let active: ViewTransition | undefined; + +export function showListView(next: ListView) { + if (next === listView.current) return; + if (!("startViewTransition" in document)) { + listView.current = next; + return; + } + active?.skipTransition(); + const root = document.documentElement; + root.dataset.listSwitch = ""; + const transition = document.startViewTransition(async () => { + listView.current = next; + await tick(); + }); + active = transition; + transition.finished.finally(() => { + if (active !== transition) return; + active = undefined; + delete root.dataset.listSwitch; + }); +} diff --git a/web/src/lib/components/profile/tabs/RepoListTab.svelte b/web/src/lib/components/profile/tabs/RepoListTab.svelte index 3762c1b2d..ed3733c46 100644 --- a/web/src/lib/components/profile/tabs/RepoListTab.svelte +++ b/web/src/lib/components/profile/tabs/RepoListTab.svelte @@ -5,6 +5,7 @@ import { createBobbinClient } from "$lib/api/client"; import RepoCard from "$lib/components/repo/RepoCard.svelte"; import PagedList from "$lib/components/profile/PagedList.svelte"; + import { listView } from "$lib/components/profile/listView.svelte"; import RepoSearchForm from "$lib/components/profile/RepoSearchForm.svelte"; import { createOffsetPager, @@ -55,6 +56,7 @@ {pages} total={listed} title="Repositories" + switchable rowCapture={repoRepocardWithStarButton} key={repoKey} emptyMessage={searchQuery @@ -65,6 +67,11 @@ {/snippet} {#snippet row(repo)} - + {/snippet} diff --git a/web/src/lib/components/profile/tabs/StarredTab.svelte b/web/src/lib/components/profile/tabs/StarredTab.svelte index 308813972..4bb6a8799 100644 --- a/web/src/lib/components/profile/tabs/StarredTab.svelte +++ b/web/src/lib/components/profile/tabs/StarredTab.svelte @@ -6,6 +6,7 @@ import RepoCard from "$lib/components/repo/RepoCard.svelte"; import Card from "$lib/components/ui/Card.svelte"; import PagedList from "$lib/components/profile/PagedList.svelte"; + import { listView } from "$lib/components/profile/listView.svelte"; import { createOffsetPager, pageCount } from "$lib/components/profile/pagination.svelte"; import { PROFILE_PAGE_LIMIT, fetchStarredPage } from "$lib/components/profile/pages"; import repoRepocardWithStarButton from "$lib/bones/repo-repocard--with-star-button.bones.json"; @@ -41,17 +42,23 @@ {pages} {total} title="Starred" + switchable emptyMessage="No stars yet." rowCapture={repoRepocardWithStarButton} key={(star) => star.uri} > {#snippet row(star)} {#if star.kind === "repo"} - + {:else} {star.ownerHandle}/{star.rkey} diff --git a/web/src/lib/components/profile/tabs/StringListTab.svelte b/web/src/lib/components/profile/tabs/StringListTab.svelte index 1029ee694..4eb3ea242 100644 --- a/web/src/lib/components/profile/tabs/StringListTab.svelte +++ b/web/src/lib/components/profile/tabs/StringListTab.svelte @@ -4,6 +4,7 @@ import { createBobbinClient } from "$lib/api/client"; import StringCard from "$lib/components/profile/StringCard.svelte"; import PagedList from "$lib/components/profile/PagedList.svelte"; + import { listView } from "$lib/components/profile/listView.svelte"; import { createOffsetPager, pageCount } from "$lib/components/profile/pagination.svelte"; import { PROFILE_PAGE_LIMIT, fetchStringsPage } from "$lib/components/profile/pages"; import profileStringcardDefault from "$lib/bones/profile-stringcard--default.bones.json"; @@ -36,11 +37,12 @@ {pages} {total} title="Strings" + switchable emptyMessage="No strings yet." rowCapture={profileStringcardDefault} key={(entry) => entry.rkey} > {#snippet row(entry)} - + {/snippet} diff --git a/web/src/lib/components/repo/RepoCard.svelte b/web/src/lib/components/repo/RepoCard.svelte index 47cab4eec..ccfbcc842 100644 --- a/web/src/lib/components/repo/RepoCard.svelte +++ b/web/src/lib/components/repo/RepoCard.svelte @@ -10,6 +10,7 @@ starButton = true, showOwner = true, uniformHeight = false, + layout = "card", border = true, background = true, padding = "regular", @@ -22,6 +23,7 @@ starButton?: boolean; showOwner?: boolean; uniformHeight?: boolean; + layout?: "card" | "row"; border?: CardVariants["border"]; background?: CardVariants["background"]; padding?: CardVariants["padding"]; @@ -33,7 +35,7 @@ {#snippet body()} - + {/snippet} void; } = $props(); @@ -40,9 +42,25 @@ ); - - - + + + {#if starButton} - + - {#if repo.description || uniformHeight} + {#if layout === "card" && (repo.description || uniformHeight)} {/if} - {#if repo.language || stats.length > 0 || uniformHeight} + {#if repo.language || stats.length > 0 || (layout === "card" && uniformHeight)} {#if repo.language} diff --git a/web/src/listView.css b/web/src/listView.css new file mode 100644 index 000000000..d2ab99584 --- /dev/null +++ b/web/src/listView.css @@ -0,0 +1,100 @@ +@utility list-morph { + :root[data-list-switch] & > * { + view-transition-name: match-element; + view-transition-class: list-shell; + } +} + +:root[data-list-switch] { + view-transition-name: none; + + & [data-vt="move"] { + view-transition-name: match-element; + view-transition-class: list-move; + } + + & [data-vt="fade"] { + view-transition-name: match-element; + view-transition-class: list-fade; + } +} + +::view-transition { + pointer-events: none; +} + +::view-transition-group(.list-shell), +::view-transition-group(.list-move), +::view-transition-group(.list-fade) { + animation-duration: 420ms; + animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1); +} + +::view-transition-group(.list-shell) { + background: var(--color-background-default); + border-radius: var(--radius-md); + box-shadow: 0 0 0 1px var(--color-shadow-ring); +} + +::view-transition-old(.list-shell), +::view-transition-new(.list-shell) { + display: none; +} + +::view-transition-old(.list-move), +::view-transition-new(.list-move), +::view-transition-old(.list-fade), +::view-transition-new(.list-fade) { + height: 100%; + width: auto; + object-fit: none; + object-position: left top; +} + +::view-transition-old(.list-fade) { + animation: 150ms cubic-bezier(0.33, 1, 0.68, 1) both list-fade-out; +} + +::view-transition-new(.list-fade) { + animation: 210ms cubic-bezier(0.33, 1, 0.68, 1) 105ms both list-fade-in; +} + +@media (prefers-reduced-motion: reduce) { + ::view-transition-group(*), + ::view-transition-image-pair(*) { + animation-duration: 0s !important; + animation-delay: 0s !important; + } + ::view-transition-old(*) { + animation: 150ms cubic-bezier(0.33, 1, 0.68, 1) both list-dissolve-out !important; + } + ::view-transition-new(*) { + animation: 150ms cubic-bezier(0.33, 1, 0.68, 1) both list-dissolve-in !important; + } +} + +@keyframes list-fade-in { + from { + opacity: 0; + filter: blur(3px); + } +} + +@keyframes list-fade-out { + to { + opacity: 0; + filter: blur(3px); + } +} + +@keyframes list-dissolve-in { + from { + opacity: 0; + } +} + +@keyframes list-dissolve-out { + to { + opacity: 0; + } +} diff --git a/web/src/routes/[handle]/+page.svelte b/web/src/routes/[handle]/+page.svelte index 3978bce3a..fe987ed66 100644 --- a/web/src/routes/[handle]/+page.svelte +++ b/web/src/routes/[handle]/+page.svelte @@ -48,6 +48,7 @@ {#snippet skeleton()} @@ -70,6 +71,7 @@ {#snippet skeleton()} @@ -83,6 +85,7 @@ {#snippet skeleton()} diff --git a/web/src/routes/[handle]/[repo]/settings/pipelines/+page.svelte b/web/src/routes/[handle]/[repo]/settings/pipelines/+page.svelte index d2b19995b..fa600dcd3 100644 --- a/web/src/routes/[handle]/[repo]/settings/pipelines/+page.svelte +++ b/web/src/routes/[handle]/[repo]/settings/pipelines/+page.svelte @@ -10,6 +10,7 @@ import { spindleTargetOf } from "$lib/api/spindlewrite"; import { listSecrets, removeSecret, type Secret } from "$lib/api/secrets"; import { getAuth } from "$lib/auth.svelte"; + import { guardUnsavedChanges } from "$lib/unsavedChanges"; import Avatar from "$lib/components/ui/Avatar.svelte"; import Button from "$lib/components/ui/Button.svelte"; import ErrorAlert from "$lib/components/ui/Error.svelte"; @@ -46,6 +47,7 @@ let picked = $state(); const chosen = $derived(picked ?? current); const dirty = $derived(chosen !== current); + guardUnsavedChanges(() => dirty); const reset = () => (picked = undefined); let confirming = $state(false); @@ -91,7 +93,6 @@ await removeSecret(agent, spindle, target, key); secrets.update((rows) => (rows ? rows.filter((row) => row.key !== key) : rows)); }); - diff --git a/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte b/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte index 37c953efc..f77034882 100644 --- a/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte +++ b/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte @@ -1,4 +1,5 @@
+ {#if layout === "card" && entry.description} +
{entry.description}
{/if} - {#if repo.language || stats.length > 0 || uniformHeight} + {#if repo.language || stats.length > 0 || (layout === "card" && uniformHeight)}