├── .npmrc ├── static ├── robots.txt └── favicon.png ├── src ├── lib │ └── images │ │ ├── svelte-welcome.png │ │ ├── svelte-welcome.webp │ │ ├── github.svg │ │ └── svelte-logo.svg ├── routes │ ├── +page.js │ ├── about │ │ ├── +page.js │ │ └── +page.svelte │ ├── sverdle │ │ ├── how-to-play │ │ │ ├── +page.js │ │ │ └── +page.svelte │ │ ├── reduced-motion.js │ │ ├── +page.server.js │ │ ├── game.js │ │ └── +page.svelte │ ├── +page.svelte │ ├── +layout.svelte │ ├── styles.css │ ├── Counter.svelte │ └── Header.svelte ├── app.d.ts └── app.html ├── vite.config.js ├── .gitignore ├── svelte.config.js ├── jsconfig.json ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karimfromjordan/sveltekit-view-transitions/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/lib/images/svelte-welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karimfromjordan/sveltekit-view-transitions/HEAD/src/lib/images/svelte-welcome.png -------------------------------------------------------------------------------- /src/lib/images/svelte-welcome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karimfromjordan/sveltekit-view-transitions/HEAD/src/lib/images/svelte-welcome.webp -------------------------------------------------------------------------------- /src/routes/+page.js: -------------------------------------------------------------------------------- 1 | // since there's no dynamic data here, we can prerender 2 | // it so that it gets served as a static asset in production 3 | export const prerender = true; 4 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .vercel 10 | .output 11 | vite.config.js.timestamp-* 12 | vite.config.ts.timestamp-* 13 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-cloudflare"; 2 | 3 | export default { 4 | kit: { 5 | adapter: adapter({ 6 | // See below for an explanation of these options 7 | routes: { 8 | include: ["/*"], 9 | exclude: [""], 10 | }, 11 | }), 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /src/routes/about/+page.js: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment'; 2 | 3 | // we don't need any JS on this page, though we'll load 4 | // it in dev so that we get hot module replacement 5 | export const csr = dev; 6 | 7 | // since there's no dynamic data here, we can prerender 8 | // it so that it gets served as a static asset in production 9 | export const prerender = true; 10 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes/sverdle/how-to-play/+page.js: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment'; 2 | 3 | // we don't need any JS on this page, though we'll load 4 | // it in dev so that we get hot module replacement 5 | export const csr = dev; 6 | 7 | // since there's no dynamic data here, we can prerender 8 | // it so that it gets served as a static asset in production 9 | export const prerender = true; 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "view-transitions", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 9 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch" 10 | }, 11 | "devDependencies": { 12 | "@fontsource/fira-mono": "^4.5.10", 13 | "@neoconfetti/svelte": "^1.0.0", 14 | "@sveltejs/adapter-cloudflare": "^2.3.0", 15 | "@sveltejs/kit": "https://gitpkg.now.sh/sveltejs/kit/packages/kit?gh-5689", 16 | "@types/cookie": "^0.5.1", 17 | "svelte": "^3.54.0", 18 | "svelte-check": "^3.0.1", 19 | "typescript": "^5.0.0", 20 | "vite": "^4.3.0" 21 | }, 22 | "type": "module" 23 | } 24 | -------------------------------------------------------------------------------- /src/routes/sverdle/reduced-motion.js: -------------------------------------------------------------------------------- 1 | import { readable } from 'svelte/store'; 2 | import { browser } from '$app/environment'; 3 | 4 | const reduced_motion_query = '(prefers-reduced-motion: reduce)'; 5 | 6 | const get_initial_motion_preference = () => { 7 | if (!browser) return false; 8 | return window.matchMedia(reduced_motion_query).matches; 9 | }; 10 | 11 | export const reduced_motion = readable(get_initial_motion_preference(), (set) => { 12 | if (browser) { 13 | /** 14 | * @param {MediaQueryListEvent} event 15 | */ 16 | const set_reduced_motion = (event) => { 17 | set(event.matches); 18 | }; 19 | const media_query_list = window.matchMedia(reduced_motion_query); 20 | media_query_list.addEventListener('change', set_reduced_motion); 21 | 22 | return () => { 23 | media_query_list.removeEventListener('change', set_reduced_motion); 24 | }; 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /src/routes/about/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | About 3 | 4 | 5 | 6 |
7 |

About this app

8 | 9 |

10 | This is a SvelteKit app. You can make your own by typing the 11 | following into your command line and following the prompts: 12 |

13 | 14 |
npm create svelte@latest
15 | 16 |

17 | The page you're looking at is purely static HTML, with no client-side interactivity needed. 18 | Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening 19 | the devtools network panel and reloading. 20 |

21 | 22 |

23 | The Sverdle page illustrates SvelteKit's data loading and form handling. Try 24 | using it with JavaScript disabled! 25 |

26 |
27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Home 9 | 10 | 11 | 12 |
13 |

14 | 15 | 16 | 17 | Welcome 18 | 19 | 20 | 21 | to your new
SvelteKit app 22 |

23 | 24 |

25 | try editing src/routes/+page.svelte 26 |

27 | 28 | 29 |
30 | 31 | 60 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 |
21 | 22 |
23 | 24 |
25 | 26 | 31 |
32 | 33 | 69 | -------------------------------------------------------------------------------- /src/lib/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 16 | -------------------------------------------------------------------------------- /src/lib/images/svelte-logo.svg: -------------------------------------------------------------------------------- 1 | svelte-logo -------------------------------------------------------------------------------- /src/routes/sverdle/+page.server.js: -------------------------------------------------------------------------------- 1 | import { fail } from '@sveltejs/kit'; 2 | import { Game } from './game'; 3 | 4 | /** @type {import('./$types').PageServerLoad} */ 5 | export const load = ({ cookies }) => { 6 | const game = new Game(cookies.get('sverdle')); 7 | 8 | return { 9 | /** 10 | * The player's guessed words so far 11 | */ 12 | guesses: game.guesses, 13 | 14 | /** 15 | * An array of strings like '__x_c' corresponding to the guesses, where 'x' means 16 | * an exact match, and 'c' means a close match (right letter, wrong place) 17 | */ 18 | answers: game.answers, 19 | 20 | /** 21 | * The correct answer, revealed if the game is over 22 | */ 23 | answer: game.answers.length >= 6 ? game.answer : null 24 | }; 25 | }; 26 | 27 | /** @type {import('./$types').Actions} */ 28 | export const actions = { 29 | /** 30 | * Modify game state in reaction to a keypress. If client-side JavaScript 31 | * is available, this will happen in the browser instead of here 32 | */ 33 | update: async ({ request, cookies }) => { 34 | const game = new Game(cookies.get('sverdle')); 35 | 36 | const data = await request.formData(); 37 | const key = data.get('key'); 38 | 39 | const i = game.answers.length; 40 | 41 | if (key === 'backspace') { 42 | game.guesses[i] = game.guesses[i].slice(0, -1); 43 | } else { 44 | game.guesses[i] += key; 45 | } 46 | 47 | cookies.set('sverdle', game.toString()); 48 | }, 49 | 50 | /** 51 | * Modify game state in reaction to a guessed word. This logic always runs on 52 | * the server, so that people can't cheat by peeking at the JavaScript 53 | */ 54 | enter: async ({ request, cookies }) => { 55 | const game = new Game(cookies.get('sverdle')); 56 | 57 | const data = await request.formData(); 58 | const guess = /** @type {string[]} */ (data.getAll('guess')); 59 | 60 | if (!game.enter(guess)) { 61 | return fail(400, { badGuess: true }); 62 | } 63 | 64 | cookies.set('sverdle', game.toString()); 65 | }, 66 | 67 | restart: async ({ cookies }) => { 68 | cookies.delete('sverdle'); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/routes/sverdle/game.js: -------------------------------------------------------------------------------- 1 | import { words, allowed } from './words.server'; 2 | 3 | export class Game { 4 | /** 5 | * Create a game object from the player's cookie, or initialise a new game 6 | * @param {string | undefined} serialized 7 | */ 8 | constructor(serialized = undefined) { 9 | if (serialized) { 10 | const [index, guesses, answers] = serialized.split('-'); 11 | 12 | this.index = +index; 13 | this.guesses = guesses ? guesses.split(' ') : []; 14 | this.answers = answers ? answers.split(' ') : []; 15 | } else { 16 | this.index = Math.floor(Math.random() * words.length); 17 | this.guesses = ['', '', '', '', '', '']; 18 | this.answers = /** @type {string[]} */ ([]); 19 | } 20 | 21 | this.answer = words[this.index]; 22 | } 23 | 24 | /** 25 | * Update game state based on a guess of a five-letter word. Returns 26 | * true if the guess was valid, false otherwise 27 | * @param {string[]} letters 28 | */ 29 | enter(letters) { 30 | const word = letters.join(''); 31 | const valid = allowed.has(word); 32 | 33 | if (!valid) return false; 34 | 35 | this.guesses[this.answers.length] = word; 36 | 37 | const available = Array.from(this.answer); 38 | const answer = Array(5).fill('_'); 39 | 40 | // first, find exact matches 41 | for (let i = 0; i < 5; i += 1) { 42 | if (letters[i] === available[i]) { 43 | answer[i] = 'x'; 44 | available[i] = ' '; 45 | } 46 | } 47 | 48 | // then find close matches (this has to happen 49 | // in a second step, otherwise an early close 50 | // match can prevent a later exact match) 51 | for (let i = 0; i < 5; i += 1) { 52 | if (answer[i] === '_') { 53 | const index = available.indexOf(letters[i]); 54 | if (index !== -1) { 55 | answer[i] = 'c'; 56 | available[index] = ' '; 57 | } 58 | } 59 | } 60 | 61 | this.answers.push(answer.join('')); 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * Serialize game state so it can be set as a cookie 68 | */ 69 | toString() { 70 | return `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/routes/styles.css: -------------------------------------------------------------------------------- 1 | @import '@fontsource/fira-mono'; 2 | 3 | :root { 4 | --font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 5 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 6 | --font-mono: 'Fira Mono', monospace; 7 | --color-bg-0: rgb(202, 216, 228); 8 | --color-bg-1: hsl(209, 36%, 86%); 9 | --color-bg-2: hsl(224, 44%, 95%); 10 | --color-theme-1: #ff3e00; 11 | --color-theme-2: #4075a6; 12 | --color-text: rgba(0, 0, 0, 0.7); 13 | --column-width: 42rem; 14 | --column-margin-top: 4rem; 15 | font-family: var(--font-body); 16 | color: var(--color-text); 17 | } 18 | 19 | body { 20 | min-height: 100vh; 21 | margin: 0; 22 | background-attachment: fixed; 23 | background-color: var(--color-bg-1); 24 | background-size: 100vw 100vh; 25 | background-image: radial-gradient( 26 | 50% 50% at 50% 50%, 27 | rgba(255, 255, 255, 0.75) 0%, 28 | rgba(255, 255, 255, 0) 100% 29 | ), 30 | linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%); 31 | } 32 | 33 | h1, 34 | h2, 35 | p { 36 | font-weight: 400; 37 | } 38 | 39 | p { 40 | line-height: 1.5; 41 | } 42 | 43 | a { 44 | color: var(--color-theme-1); 45 | text-decoration: none; 46 | } 47 | 48 | a:hover { 49 | text-decoration: underline; 50 | } 51 | 52 | h1 { 53 | font-size: 2rem; 54 | text-align: center; 55 | } 56 | 57 | h2 { 58 | font-size: 1rem; 59 | } 60 | 61 | pre { 62 | font-size: 16px; 63 | font-family: var(--font-mono); 64 | background-color: rgba(255, 255, 255, 0.45); 65 | border-radius: 3px; 66 | box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); 67 | padding: 0.5em; 68 | overflow-x: auto; 69 | color: var(--color-text); 70 | } 71 | 72 | .text-column { 73 | display: flex; 74 | max-width: 48rem; 75 | flex: 0.6; 76 | flex-direction: column; 77 | justify-content: center; 78 | margin: 0 auto; 79 | } 80 | 81 | input, 82 | button { 83 | font-size: inherit; 84 | font-family: inherit; 85 | } 86 | 87 | button:focus:not(:focus-visible) { 88 | outline: none; 89 | } 90 | 91 | @media (min-width: 720px) { 92 | h1 { 93 | font-size: 2.4rem; 94 | } 95 | } 96 | 97 | .visually-hidden { 98 | border: 0; 99 | clip: rect(0 0 0 0); 100 | height: auto; 101 | margin: 0; 102 | overflow: hidden; 103 | padding: 0; 104 | position: absolute; 105 | width: 1px; 106 | white-space: nowrap; 107 | } 108 | -------------------------------------------------------------------------------- /src/routes/sverdle/how-to-play/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | How to play Sverdle 3 | 4 | 5 | 6 |
7 |

How to play Sverdle

8 | 9 |

10 | Sverdle is a clone of Wordle, the 11 | word guessing game. To play, enter a five-letter English word. For example: 12 |

13 | 14 |
15 | r 16 | i 17 | t 18 | z 19 | y 20 |
21 | 22 |

23 | The y is in the right place. r and 24 | t 25 | are the right letters, but in the wrong place. The other letters are wrong, and can be discarded. 26 | Let's make another guess: 27 |

28 | 29 |
30 | p 31 | a 32 | r 33 | t 34 | y 35 |
36 | 37 |

This time we guessed right! You have six guesses to get the word.

38 | 39 |

40 | Unlike the original Wordle, Sverdle runs on the server instead of in the browser, making it 41 | impossible to cheat. It uses <form> and cookies to submit data, meaning you can 42 | even play with JavaScript disabled! 43 |

44 |
45 | 46 | 96 | -------------------------------------------------------------------------------- /src/routes/Counter.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 26 | 27 |
28 |
29 | 30 | {Math.floor($displayed_count)} 31 |
32 |
33 | 34 | 39 |
40 | 41 | 107 | -------------------------------------------------------------------------------- /src/routes/Header.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 | 10 | SvelteKit 11 | 12 |
13 | 14 | 33 | 34 |
35 | 36 | GitHub 37 | 38 |
39 |
40 | 41 | 131 | -------------------------------------------------------------------------------- /src/routes/sverdle/+page.svelte: -------------------------------------------------------------------------------- 1 | 87 | 88 | 89 | 90 | 91 | Sverdle 92 | 93 | 94 | 95 |

Sverdle

96 | 97 |
{ 101 | // prevent default callback from resetting the form 102 | return ({ update }) => { 103 | update({ reset: false }); 104 | }; 105 | }} 106 | > 107 | How to play 108 | 109 |
110 | {#each Array.from(Array(6).keys()) as row (row)} 111 | {@const current = row === i} 112 |

Row {row + 1}

113 |
114 | {#each Array.from(Array(5).keys()) as column (column)} 115 | {@const answer = data.answers[row]?.[column]} 116 | {@const value = data.guesses[row]?.[column] ?? ''} 117 | {@const selected = current && column === data.guesses[row].length} 118 | {@const exact = answer === 'x'} 119 | {@const close = answer === 'c'} 120 | {@const missing = answer === '_'} 121 |
122 | {value} 123 | 124 | {#if exact} 125 | (correct) 126 | {:else if close} 127 | (present) 128 | {:else if missing} 129 | (absent) 130 | {:else} 131 | empty 132 | {/if} 133 | 134 | 135 |
136 | {/each} 137 |
138 | {/each} 139 |
140 | 141 |
142 | {#if won || data.answers.length >= 6} 143 | {#if !won && data.answer} 144 |

the answer was "{data.answer}"

145 | {/if} 146 | 149 | {:else} 150 |
151 | 152 | 153 | 162 | 163 | {#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row} 164 |
165 | {#each row as letter} 166 | 178 | {/each} 179 |
180 | {/each} 181 |
182 | {/if} 183 |
184 |
185 | 186 | {#if won} 187 |
197 | {/if} 198 | 199 | 411 | --------------------------------------------------------------------------------