From 90f1ae8fbde4b6b4ad5952bffedef0bfe0d7ea7b Mon Sep 17 00:00:00 2001 From: Chris Zuber Date: Thu, 30 Apr 2026 11:11:25 -0700 Subject: [PATCH] Implement basics of View Transitions --- CHANGELOG.md | 5 +++ http.config.js | 7 +++- package-lock.json | 86 ++++++++++++++++++++++++++--------------------- package.json | 22 ++++++------ router.js | 64 +++++++++++++++++++---------------- test/index.js | 20 +++++------ 6 files changed, 115 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4512e14..ad96f21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v1.2.0] - 2026-04-30 + +### Added +- Implement basics of View Transitions + ## [v1.1.15] - 2026-02-27 ### Added diff --git a/http.config.js b/http.config.js index fce3edb..b28ed97 100644 --- a/http.config.js +++ b/http.config.js @@ -3,6 +3,7 @@ import { useDefaultCSP, addScriptSrc, addStyleSrc, addConnectSrc, addImageSrc, a addScriptSrc( imports['@shgysk8zer0/polyfills'], + imports['@shgysk8zer0/signals/'], 'https://unpkg.com/@aegisjsproject/', 'https://unpkg.com/@lit/', imports.marked, @@ -10,7 +11,11 @@ addScriptSrc( imports['@highlightjs/cdn-assets/'], ); addConnectSrc('https://api.github.com/users/', 'https://baconipsum.com/api/'); -addStyleSrc(imports['@shgysk8zer0/core-css/'], imports['@highlightjs/cdn-assets/es/styles/']); +addStyleSrc( + imports['@shgysk8zer0/core-css/'], + imports['@aegisjsproject/styles/'] + 'css/', + imports['@highlightjs/cdn-assets/es/styles/'], +); addImageSrc('https://avatars.githubusercontent.com/u/', 'https://images.unsplash.com/', 'blob:'); addTrustedTypePolicy('aegis-router#html', 'aegis-sanitizer#html', 'aegis-escape#html', 'lit-html', 'default'); lockCSP(); diff --git a/package-lock.json b/package-lock.json index 2dedb0c..4a35e94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aegisjsproject/router", - "version": "1.1.15", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@aegisjsproject/router", - "version": "1.1.15", + "version": "1.2.0", "funding": [ { "type": "librepay", @@ -19,21 +19,21 @@ ], "license": "MIT", "dependencies": { - "@aegisjsproject/state": "^1.0.5" + "@aegisjsproject/state": "^1.0.7" }, "devDependencies": { - "@aegisjsproject/component": "^0.1.6", - "@aegisjsproject/core": "^0.2.27", - "@aegisjsproject/dev-server": "^1.0.5", + "@aegisjsproject/component": "^0.1.8", + "@aegisjsproject/core": "^0.2.34", + "@aegisjsproject/dev-server": "^1.0.6", "@aegisjsproject/http-utils": "^1.0.4", "@aegisjsproject/url": "^1.0.3", - "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", - "@shgysk8zer0/eslint-config": "^1.0.4", - "@shgysk8zer0/http-server": "^1.1.1", - "@shgysk8zer0/importmap": "^1.7.8", - "eslint": "^10.0.0", - "rollup": "^4.38.0" + "@shgysk8zer0/eslint-config": "^1.0.9", + "@shgysk8zer0/http-server": "^1.1.2", + "@shgysk8zer0/importmap": "^1.9.12", + "eslint": "^10.2.1", + "rollup": "^4.60.2" }, "engines": { "node": ">=18.0.0" @@ -1040,10 +1040,11 @@ } }, "node_modules/@shgysk8zer0/importmap": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@shgysk8zer0/importmap/-/importmap-1.9.10.tgz", - "integrity": "sha512-V6x1WsJXEhMQuRUS3SWUViIeMiLzasV8qPQ3IJZzaxlq8JO0H93BcktRddKSbnHJGcIqD+2DIeVxGFcjouA39w==", + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@shgysk8zer0/importmap/-/importmap-1.9.12.tgz", + "integrity": "sha512-B04gR8fFQyyFasuO6vg6WzOShKqVBjWZuGX8MEhXO4lQwRNurUZBlXBQZPZWmnZvKkFgynPOuikP68Qw4/eWFg==", "dev": true, + "license": "MIT", "dependencies": { "@shgysk8zer0/npm-utils": "^1.1.6", "@shgysk8zer0/polyfills": "^0.6.3", @@ -1151,6 +1152,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1286,6 +1288,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -1491,10 +1494,11 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -1786,10 +1790,11 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1837,6 +1842,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -1877,10 +1883,11 @@ } }, "node_modules/serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=20.0.0" } @@ -2539,9 +2546,9 @@ } }, "@shgysk8zer0/importmap": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@shgysk8zer0/importmap/-/importmap-1.9.10.tgz", - "integrity": "sha512-V6x1WsJXEhMQuRUS3SWUViIeMiLzasV8qPQ3IJZzaxlq8JO0H93BcktRddKSbnHJGcIqD+2DIeVxGFcjouA39w==", + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@shgysk8zer0/importmap/-/importmap-1.9.12.tgz", + "integrity": "sha512-B04gR8fFQyyFasuO6vg6WzOShKqVBjWZuGX8MEhXO4lQwRNurUZBlXBQZPZWmnZvKkFgynPOuikP68Qw4/eWFg==", "dev": true, "requires": { "@shgysk8zer0/npm-utils": "^1.1.6", @@ -2612,7 +2619,8 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -2709,6 +2717,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2855,9 +2864,9 @@ } }, "flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "fsevents": { @@ -3072,9 +3081,9 @@ "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "prelude-ls": { @@ -3105,6 +3114,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, + "peer": true, "requires": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", @@ -3136,9 +3146,9 @@ } }, "serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true }, "shebang-command": { diff --git a/package.json b/package.json index 9dfbe47..78f7f79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aegisjsproject/router", - "version": "1.1.15", + "version": "1.2.0", "description": "A simple but powerful router module", "keywords": [ "router", @@ -95,20 +95,20 @@ }, "homepage": "https://github.com/AegisJSProject/router#readme", "dependencies": { - "@aegisjsproject/state": "^1.0.5" + "@aegisjsproject/state": "^1.0.7" }, "devDependencies": { - "@aegisjsproject/component": "^0.1.6", - "@aegisjsproject/core": "^0.2.27", - "@aegisjsproject/dev-server": "^1.0.5", + "@aegisjsproject/component": "^0.1.8", + "@aegisjsproject/core": "^0.2.34", + "@aegisjsproject/dev-server": "^1.0.6", "@aegisjsproject/http-utils": "^1.0.4", "@aegisjsproject/url": "^1.0.3", - "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", - "@shgysk8zer0/eslint-config": "^1.0.4", - "@shgysk8zer0/http-server": "^1.1.1", - "@shgysk8zer0/importmap": "^1.7.8", - "eslint": "^10.0.0", - "rollup": "^4.38.0" + "@shgysk8zer0/eslint-config": "^1.0.9", + "@shgysk8zer0/http-server": "^1.1.2", + "@shgysk8zer0/importmap": "^1.9.12", + "eslint": "^10.2.1", + "rollup": "^4.60.2" } } diff --git a/router.js b/router.js index 2cc75a7..d8c9513 100644 --- a/router.js +++ b/router.js @@ -18,6 +18,10 @@ let rootSelector = '#' + ROOT_ID; const SUPPORTS_TRUSTED_TYPES = 'trustedTypes' in globalThis; const _isTrustedHTML = input => SUPPORTS_TRUSTED_TYPES && trustedTypes.isHTML(input); +const startViewTransition = typeof document.startViewTransition === 'function' + ? (update, types) => document.startViewTransition({ update, types }) + : update => Promise.try(update); + function _handlePreloadMutations(target) { if (target instanceof MutationRecord) { _handlePreloadMutations(target.target); @@ -440,38 +444,40 @@ async function _getHTML(url, { signal, method = 'GET', body, integrity, cache = } } -function _updatePage(content) { +async function _updatePage(content) { const timestamp = performance.now(); - if (content instanceof Document) { - if (content.head.childElementCount !== 0) { - setTitle(content.title); - setDescription(content.querySelector(DESC_SELECTOR)?.content); - } + await startViewTransition(() => { + if (content instanceof Document) { + if (content.head.childElementCount !== 0) { + setTitle(content.title); + setDescription(content.querySelector(DESC_SELECTOR)?.content); + } - const contentEl = typeof rootSelector === 'string' ? content.body.querySelector(rootSelector) ?? content.body : content.body; - - rootEl.replaceChildren(...contentEl.childNodes); - } else if (content instanceof HTMLTemplateElement) { - rootEl.replaceChildren(content.content); - } else if (content instanceof Function && content.prototype instanceof HTMLElement) { - rootEl.replaceChildren(new content({ state: getStateObj(), url: new URL(location.href), timestamp })); - } else if (content instanceof Node) { - rootEl.replaceChildren(content); - } else if (content instanceof Function) { - _updatePage(content()); - } else if (typeof content === 'string') { - rootEl.setHTMLUnsafe(policy.createHTML(content)); - } else if (_isTrustedHTML(content)) { - rootEl.setHTMLUnsafe(content); - } else if (content instanceof Error) { - reportError(content); - rootEl.textContent = content.message; - } else if (content instanceof URL) { - navigate(content); - } else if (! (content === null || typeof content === 'undefined')) { - rootEl.textContent = content; - } + const contentEl = typeof rootSelector === 'string' ? content.body.querySelector(rootSelector) ?? content.body : content.body; + + rootEl.replaceChildren(...contentEl.childNodes); + } else if (content instanceof HTMLTemplateElement) { + rootEl.replaceChildren(content.content); + } else if (content instanceof Function && content.prototype instanceof HTMLElement) { + rootEl.replaceChildren(new content({ state: getStateObj(), url: new URL(location.href), timestamp })); + } else if (content instanceof Node) { + rootEl.replaceChildren(content); + } else if (content instanceof Function) { + _updatePage(content()); + } else if (typeof content === 'string') { + rootEl.setHTMLUnsafe(policy.createHTML(content)); + } else if (_isTrustedHTML(content)) { + rootEl.setHTMLUnsafe(content); + } else if (content instanceof Error) { + reportError(content); + rootEl.textContent = content.message; + } else if (content instanceof URL) { + navigate(content); + } else if (! (content === null || typeof content === 'undefined')) { + rootEl.textContent = content; + } + }); const ev = new AegisNavigationEvent(NAV_EVENT, EVENT_TYPES.load, { cancelable: false }); Promise.try(() => EVENT_TARGET.dispatchEvent(ev)).finally(ev[Symbol.asyncDispose].bind(ev)); diff --git a/test/index.js b/test/index.js index e8fc0ce..9cd3b72 100644 --- a/test/index.js +++ b/test/index.js @@ -56,16 +56,16 @@ init('#routes', { rootEl: '#root', inteceptRoot: document.body, observePreloads: true, - transition: { - keyframes: { - opacity: [1, 0], - transform: ['none', 'scale(0.8) translateX(-100%)'] - }, - options: { - easing: 'ease-in', - duration: 150, - } - }, + // transition: { + // keyframes: { + // opacity: [1, 0], + // transform: ['none', 'scale(0.8) translateX(-100%)'] + // }, + // options: { + // easing: 'ease-in', + // duration: 150, + // } + // }, signal: controller.signal, }).finally(() => console.timeEnd('init'));