├── .browserslistrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── Readme.md ├── enable-testing.js ├── env.example ├── package.json ├── postcss.config.cjs ├── src ├── app.html ├── assets │ ├── page-transitions-svelte.jpg │ └── page-transitions-svelte.png ├── global.d.ts ├── global.scss ├── lib │ ├── cohosts.json │ ├── components │ │ ├── Header.svelte │ │ ├── Overview.svelte │ │ ├── Page.svelte │ │ ├── Scroller.svelte │ │ ├── Switch.svelte │ │ ├── Teaser.svelte │ │ └── Video.svelte │ └── services │ │ ├── buildUrl.ts │ │ ├── delay.ts │ │ ├── episode-fns.ts │ │ └── pageCrossfade.ts └── routes │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.server.ts │ ├── +page.svelte │ ├── robots.txt │ └── +server.ts │ ├── videos │ └── [episode] │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── with-[cohost] │ ├── +page.server.ts │ └── +page.svelte ├── static └── favicon.ico ├── svelte.config.js ├── tsconfig.eslint.json ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /.svelte-kit 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["triple/svelte"], 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "project": "./tsconfig.eslint.json", 10 | "extraFileExtensions": [".cjs", ".svelte"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /.svelte-kit 4 | /build 5 | /package 6 | /functions 7 | /storybook-static 8 | .env 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.svelte-kit 2 | /build 3 | /package 4 | /node_modules -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Page transitions for the web in Svelte 2 | 3 | A Google IO talk [Bringing page transitions to the web (Youtube)](https://www.youtube.com/watch?v=JCJUPJ_zDQ4) 4 | given by [Jake Archibald](https://jakearchibald.com/) 5 | 6 | Inspired me to recreate these animations using [Svelte](https://svelte.dev/) 7 | 8 | - [Demo (Svelte)](https://http203-playlist-svelte.netlify.app/) 9 | - [Demo (Original)](https://http203-playlist.netlify.app/) - [src](https://github.com/jakearchibald/http203-playlist) 10 | 11 | > "Well, the truth is it's really hard with the APIs we have today" 12 | 13 | True, I've had to hack both the source and target pages to facilitate the transitions. 14 | 15 | > "It's not particularly easy in modern frameworks" 16 | 17 | Svelte's powerful animation system helps a lot, but as both pages are active at once, getting good performance is hard, especially because a navigation is a big DOM change. 18 | -------------------------------------------------------------------------------- /enable-testing.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { promises as fs } from "fs"; 4 | import path from "path"; 5 | 6 | const projectDir = new URL(".", import.meta.url).pathname; 7 | 8 | const packageJson = JSON.parse( 9 | await fs.readFile(path.resolve(projectDir, "package.json"), "utf-8") 10 | ); 11 | 12 | const scripts = { 13 | "dev:sveltekit": "svelte-kit dev", 14 | "dev:storybook": "start-storybook --modern --no-open --port 6006", 15 | "build:sveltekit": "svelte-kit build", 16 | "build:storybook": 17 | "build-storybook --modern --output-dir build/styleguide-storybook", 18 | test: "vitest --passWithNoTests run", 19 | "test:watch": "vitest", 20 | }; 21 | for (const [task, command] of Object.entries(scripts)) { 22 | packageJson.scripts[task] = packageJson.scripts[task] || command; 23 | } 24 | if (packageJson.scripts.dev === "svelte-kit dev") { 25 | packageJson.scripts.dev = 26 | 'concurrently -c "#676778","#990f3f" --kill-others-on-fail "npm:dev:*"'; 27 | } 28 | if (packageJson.scripts.build === "svelte-kit build") { 29 | packageJson.scripts.build = 30 | "npm run build:sveltekit && npm run build:storybook"; 31 | } 32 | 33 | const devDependencies = { 34 | "@storybook/addon-actions": "^6.4.22", 35 | "@storybook/addon-essentials": "^6.4.22", 36 | "@storybook/addon-links": "^6.4.22", 37 | "@storybook/addon-svelte-csf": "^2.0.2", 38 | "@storybook/builder-vite": "^0.1.23", 39 | "@storybook/svelte": "^6.4.22", 40 | "@testing-library/svelte": "^3.1.0", 41 | jsdom: "^19.0.0", 42 | "vite-tsconfig-paths": "^3.4.1", 43 | vitest: "^0.12.4", 44 | }; 45 | for (const [dependency, version] of Object.entries(devDependencies)) { 46 | packageJson.devDependencies[dependency] = 47 | packageJson.devDependencies[dependency] || version; 48 | } 49 | 50 | await fs.stat(path.resolve(projectDir, ".storybook")).catch(() => { 51 | return fs.mkdir(path.resolve(projectDir, ".storybook")); 52 | }); 53 | 54 | async function writeFile(filename, body) { 55 | await fs.writeFile(path.resolve(projectDir, filename), body); 56 | process.stdout.write(`created "${filename}" (${body.length} bytes)\n`); 57 | } 58 | 59 | await writeFile("package.json", `${JSON.stringify(packageJson, null, 2)}\n`); 60 | await writeFile( 61 | "vitest.config.ts", 62 | `// eslint-disable-next-line import/no-extraneous-dependencies 63 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 64 | import { configDefaults, defineConfig } from "vitest/config"; 65 | 66 | export default defineConfig({ 67 | plugins: [svelte({ hot: !process.env.VITEST })], 68 | test: { 69 | global: true, 70 | environment: "jsdom", 71 | exclude: [...configDefaults.exclude, "package"], 72 | }, 73 | }); 74 | ` 75 | ); 76 | await writeFile( 77 | ".storybook/main.cjs", 78 | `const path = require("path"); 79 | const preprocess = require("svelte-preprocess"); 80 | const { default: tsconfigPaths } = require("vite-tsconfig-paths"); 81 | 82 | module.exports = { 83 | core: { builder: "@storybook/builder-vite" }, 84 | stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(ts|svelte)"], 85 | addons: [ 86 | "@storybook/addon-links", 87 | "@storybook/addon-essentials", 88 | "@storybook/addon-svelte-csf", 89 | ], 90 | staticDirs: ["../static"], 91 | svelteOptions: { 92 | preprocess: preprocess({ sourceMap: true }), 93 | }, 94 | viteFinal(config) { 95 | /* eslint-disable no-param-reassign */ 96 | config.base = ""; 97 | config.resolve.alias = config.resolve.alias || {}; 98 | config.resolve.alias.$lib = path.resolve(__dirname, "../src/lib"); 99 | config.plugins.push(tsconfigPaths()); 100 | config.build = config.build || {}; 101 | config.build.chunkSizeWarningLimit = 1000; 102 | return config; 103 | }, 104 | }; 105 | ` 106 | ); 107 | const globalScssExists = await fs 108 | .stat(path.resolve(projectDir, "src/global.scss")) 109 | .catch(() => false); 110 | 111 | if (globalScssExists) { 112 | await writeFile( 113 | ".storybook/preview.cjs", 114 | `import "../src/global.scss"; 115 | ` 116 | ); 117 | } 118 | 119 | await writeFile( 120 | ".husky/pre-push", 121 | `#!/bin/sh 122 | . "$(dirname "$0")/_/husky.sh" 123 | 124 | npm run test 125 | ` 126 | ); 127 | await fs.chmod(path.resolve(projectDir, ".husky/pre-push"), "755"); 128 | 129 | const helloComponentExists = await fs 130 | .stat(path.resolve(projectDir, "src/lib/components/Hello/Hello.svelte")) 131 | .catch(() => false); 132 | 133 | if (helloComponentExists) { 134 | await writeFile( 135 | "src/lib/components/Hello/Hello.spec.ts", 136 | `import { expect, it, describe, vi } from "vitest"; 137 | import { render, fireEvent } from "@testing-library/svelte"; 138 | import { tick } from "svelte"; 139 | import Hallo from "./Hello.svelte"; 140 | 141 | /** 142 | * Note! For demonstation purposes only. this is a terrible unittest: 143 | * - It doesn't test any complexity we wrote 144 | * - The components is trivial an unlikely to break/change 145 | */ 146 | describe("Hello component", () => { 147 | it("should render based on prop", async () => { 148 | const { getByText, component } = render(Hallo, { name: "world" }); 149 | const el = getByText("Hello world"); 150 | expect(el.textContent).toBe("Hello world"); 151 | component.$set({ name: "you" }); 152 | await tick(); 153 | expect(el.textContent).toBe("Hello you"); 154 | }); 155 | 156 | it("should trigger handlers based on events", async () => { 157 | const { getByText, component } = render(Hallo, { name: "click" }); 158 | const listener = vi.fn(); 159 | component.$on("click", listener); 160 | fireEvent(getByText("Hello click"), new MouseEvent("click")); 161 | expect(listener).toBeCalledTimes(1); 162 | }); 163 | }); 164 | ` 165 | ); 166 | await writeFile( 167 | "src/lib/components/Hello/Hello.stories.svelte", 168 | ` 172 | 173 | 181 | 182 | 185 | 186 | 192 | 193 | 199 | ` 200 | ); 201 | } 202 | process.stdout.write( 203 | "\n\nTo bring in the additional depencencies for Vitest & Storybook run:\n\nyarn install # or npm install\n" 204 | ); 205 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # .env example 2 | 3 | ROBOTSTXT=noindex 4 | 5 | SVELTE_PUBLIC_API_ENDPOINT=https://raw.githubusercontent.com/jakearchibald/http203-playlist/main/lib/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-project-template", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "lint": "concurrently -c \"#c596c7\",\"#676778\",\"#3074c0\",\"#7c7cea\" --kill-others-on-fail \"npm:lint:*\"", 10 | "lint:prettier": "prettier --check --loglevel=warn \"src/**/*.svelte\"", 11 | "lint:svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --ignore build,package", 12 | "lint:tsc": "tsc --noEmit", 13 | "lint:eslint": "eslint --ext=js,ts,svelte --max-warnings=0 src", 14 | "format": "prettier --write . && eslint --ext=js,ts,svelte --fix src", 15 | "prepare": "husky install" 16 | }, 17 | "prettier": "eslint-config-triple/.prettierrc", 18 | "lint-staged": { 19 | "*.ts": [ 20 | "eslint --max-warnings 0 --no-ignore", 21 | "sh -c 'tsc -p tsconfig.json --noEmit'" 22 | ], 23 | "*.(c)?js": [ 24 | "eslint --max-warnings 0 --no-ignore" 25 | ], 26 | "*.svelte": [ 27 | "eslint --max-warnings 0 --no-ignore", 28 | "svelte-check --fail-on-warnings --fail-on-hints", 29 | "prettier --check" 30 | ] 31 | }, 32 | "devDependencies": { 33 | "@sveltejs/adapter-static": "^1.0.0-next.32", 34 | "@sveltejs/kit": "^1.0.0-next.345", 35 | "@types/marked": "^4.0.3", 36 | "@typescript-eslint/eslint-plugin": "^5.17.0", 37 | "@typescript-eslint/parser": "^5.17.0", 38 | "autoprefixer": "^10.4.4", 39 | "concurrently": "^7.1.0", 40 | "eslint": "^8.9.0", 41 | "eslint-config-airbnb-base": "^15.0.0", 42 | "eslint-config-airbnb-typescript": "^17.0.0", 43 | "eslint-config-prettier": "^8.5.0", 44 | "eslint-config-triple": "^0.5.1", 45 | "eslint-import-resolver-typescript": "^3.5.0", 46 | "eslint-plugin-import": "^2.25.4", 47 | "eslint-plugin-only-warn": "^1.0.3", 48 | "eslint-plugin-prettier": "^4.0.0", 49 | "eslint-plugin-svelte3": "^4.0.0", 50 | "husky": "^8.0.1", 51 | "lint-staged": "^13.0.3", 52 | "postcss": "^8.4.12", 53 | "prettier": "^2.6.1", 54 | "prettier-plugin-svelte": "^2.6.0", 55 | "sass": "^1.49.11", 56 | "svelte": "^3.46.6", 57 | "svelte-check": "^2.4.6", 58 | "svelte-preprocess": "^4.10.4", 59 | "typescript": "^4.6.3", 60 | "vite": "^3.0.9" 61 | }, 62 | "dependencies": { 63 | "dotenv": "^16.0.0", 64 | "marked": "^4.0.15", 65 | "slugify": "^1.6.5" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("autoprefixer")], 3 | }; 4 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 | 11 | %sveltekit.body% 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/page-transitions-svelte.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/page-transitions-in-svelte/3fed5f861ce46526d034b06b51b62cea23e7b0fa/src/assets/page-transitions-svelte.jpg -------------------------------------------------------------------------------- /src/assets/page-transitions-svelte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfanger/page-transitions-in-svelte/3fed5f861ce46526d034b06b51b62cea23e7b0fa/src/assets/page-transitions-svelte.png -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import type { Writable } from "svelte/store"; 6 | 7 | declare namespace App { 8 | interface Stuff { 9 | backVisisble: Writable; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --white: #fff; 3 | --primary-text: #212121; 4 | --secondary-text: #757575; 5 | --background: #bdbdbd; 6 | --primary-color: #673ab7; 7 | --primary-dark: #512da8; 8 | --primary-light: #d1c4e9; 9 | --accent-color: #ff9800; 10 | --content-padding: 1.9rem; 11 | } 12 | 13 | html { 14 | background: var(--background); 15 | color: var(--primary-text); 16 | font-size: 62.5%; 17 | height: -webkit-fill-available; 18 | } 19 | 20 | body { 21 | font: 16px/1.5 system-ui, sans-serif; 22 | margin: 0; 23 | height: 100vh; 24 | height: -webkit-fill-available; 25 | } 26 | 27 | a { 28 | text-decoration: none; 29 | } 30 | 31 | a:hover { 32 | text-decoration: underline; 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/cohosts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "slug": "cassie", "name": "Cassie" }, 3 | { "slug": "ada", "name": "Ada" }, 4 | { "slug": "surma", "name": "Surma" }, 5 | { "slug": "paul", "name": "Paul" } 6 | ] 7 | -------------------------------------------------------------------------------- /src/lib/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 34 | 35 |
36 | {#key backVisible} 37 | 38 | {#if backVisible} 39 | 40 | 41 | 42 | {/if} 43 |
HTTP 203
44 |
45 | {/key} 46 |
47 | 48 | 81 | -------------------------------------------------------------------------------- /src/lib/components/Overview.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | 23 |
24 | 25 |
    26 | {#each teasers as teaser (teaser.href)} 27 |
  • 28 | 29 |
  • 30 | {/each} 31 |
32 |
33 | 34 | 58 | -------------------------------------------------------------------------------- /src/lib/components/Page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 | 29 | -------------------------------------------------------------------------------- /src/lib/components/Scroller.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
    10 | {#each teasers as teaser (teaser.href)} 11 |
  1. 12 | {/each} 13 |
14 |
15 | 16 | 56 | -------------------------------------------------------------------------------- /src/lib/components/Switch.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
    8 |
  1. All
  2. 9 | {#each cohosts as cohost} 10 |
  3. 11 | {cohost.name} 16 |
  4. 17 | {/each} 18 |
19 | 20 | 46 | -------------------------------------------------------------------------------- /src/lib/components/Teaser.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 27 |

34 | 35 |

36 |
37 | 38 | 64 | -------------------------------------------------------------------------------- /src/lib/components/Video.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
29 | {#if youtube} 30 |