feat: add configuration option for enabling browser-native view transitions

This commit is contained in:
Jairus Joer 2025-10-25 15:42:09 +02:00
parent 82abc95d36
commit de33ab6dda
14 changed files with 33 additions and 8 deletions

View File

@ -11,6 +11,7 @@ const config: QuartzConfig = {
pageTitle: "Quartz 4", pageTitle: "Quartz 4",
pageTitleSuffix: "", pageTitleSuffix: "",
enableSPA: true, enableSPA: true,
enableViewTransitions: true,
enablePopovers: true, enablePopovers: true,
analytics: { analytics: {
provider: "plausible", provider: "plausible",

View File

@ -56,6 +56,12 @@ export interface GlobalConfiguration {
pageTitleSuffix?: string pageTitleSuffix?: string
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
enableSPA: boolean enableSPA: boolean
/**
* `enableSPA: true` required; Whether to enable browser-native view transitions for single-page-app style rendering.
*
* Customizing your animations: https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#customizing_your_animations.
*/
enableViewTransitions: boolean
/** Whether to display Wikipedia-style popovers when hovering over links */ /** Whether to display Wikipedia-style popovers when hovering over links */
enablePopovers: boolean enablePopovers: boolean
/** Analytics mode */ /** Analytics mode */

View File

@ -235,7 +235,7 @@ export function renderPage(
const doc = ( const doc = (
<html lang={lang} dir={direction}> <html lang={lang} dir={direction}>
<Head {...componentData} /> <Head {...componentData} />
<body data-slug={slug}> <body data-slug={slug} data-view-transitions={cfg.enableViewTransitions}>
<div id="quartz-root" class="page"> <div id="quartz-root" class="page">
<Body {...componentData}> <Body {...componentData}>
{LeftComponent} {LeftComponent}

View File

@ -128,9 +128,8 @@ async function _navigate(url: URL, isBack: boolean = false) {
} }
notifyNav(getFullSlug(window)) notifyNav(getFullSlug(window))
delete announcer.dataset.persist
}) })
delete announcer.dataset.persist
} }
async function navigate(url: URL, isBack: boolean = false) { async function navigate(url: URL, isBack: boolean = false) {

View File

@ -46,10 +46,15 @@ export async function fetchCanonical(url: URL): Promise<Response> {
} }
/** /**
* Wraps a DOM update in a View Transition if supported by the browser. * Wraps a DOM update in a View Transition if supported by the browser and enabled in config.
* Falls back to immediate execution if the API is unavailable. * Falls back to immediate execution if the API is unavailable or disabled.
* @param callback - The function containing DOM updates to animate
*/ */
export function startViewTransition(callback: () => void): void { export function startViewTransition(callback: () => void): void {
document.startViewTransition?.(() => callback()) ?? callback() const enableViewTransitions = document.body.dataset.viewTransitions === "true"
if (enableViewTransitions && document.startViewTransition) {
document.startViewTransition(() => callback())
} else {
callback()
}
} }

View File

@ -2,6 +2,7 @@
.backlinks { .backlinks {
flex-direction: column; flex-direction: column;
view-transition-name: backlinks;
& > h3 { & > h3 {
font-size: 1rem; font-size: 1rem;

View File

@ -0,0 +1,3 @@
.giscus {
view-transition-name: giscus;
}

View File

@ -9,6 +9,7 @@
margin: 0; margin: 0;
text-align: inherit; text-align: inherit;
flex-shrink: 0; flex-shrink: 0;
view-transition-name: darkmode;
& svg { & svg {
position: absolute; position: absolute;

View File

@ -30,9 +30,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: hidden; overflow-y: hidden;
min-height: 1.2rem; min-height: 1.2rem;
flex: 0 1 auto; flex: 0 1 auto;
view-transition-name: explorer;
&.collapsed { &.collapsed {
flex: 0 1 1.2rem; flex: 0 1 1.2rem;
& .fold { & .fold {

View File

@ -2,6 +2,7 @@ footer {
text-align: left; text-align: left;
margin-bottom: 4rem; margin-bottom: 4rem;
opacity: 0.7; opacity: 0.7;
view-transition-name: footer;
& ul { & ul {
list-style: none; list-style: none;

View File

@ -1,6 +1,8 @@
@use "../../styles/variables.scss" as *; @use "../../styles/variables.scss" as *;
.graph { .graph {
view-transition-name: graph;
& > h3 { & > h3 {
font-size: 1rem; font-size: 1rem;
margin: 0; margin: 0;

View File

@ -9,6 +9,7 @@
margin: 0; margin: 0;
text-align: inherit; text-align: inherit;
flex-shrink: 0; flex-shrink: 0;
view-transition-name: readermode;
& svg { & svg {
position: absolute; position: absolute;

View File

@ -1,4 +1,6 @@
.recent-notes { .recent-notes {
view-transition-name: recent-notes;
& > h3 { & > h3 {
margin: 0.5rem 0 0 0; margin: 0.5rem 0 0 0;
font-size: 1rem; font-size: 1rem;

View File

@ -6,6 +6,8 @@
overflow-y: hidden; overflow-y: hidden;
min-height: 1.4rem; min-height: 1.4rem;
flex: 0 0.5 auto; flex: 0 0.5 auto;
view-transition-name: toc;
&:has(button.toc-header.collapsed) { &:has(button.toc-header.collapsed) {
flex: 0 1 1.4rem; flex: 0 1 1.4rem;
} }