Ron Au commited on
Commit
70cf894
1 Parent(s): 064c252
.dockerignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .git*
3
+ .eslint*
4
+ .prettier*
5
+ .svelte-kit
6
+ Dockerfile*
7
+ docker-compose.yml
8
+ node_modules
9
+ build
10
+ public
11
+ README.md
.eslintignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
.eslintrc.cjs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['eslint:recommended', 'prettier'],
4
+ plugins: ['svelte3'],
5
+ overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
6
+ parserOptions: {
7
+ sourceType: 'module',
8
+ ecmaVersion: 2020
9
+ },
10
+ env: {
11
+ browser: true,
12
+ es2017: true,
13
+ node: true
14
+ }
15
+ };
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
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-*
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
.prettierignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
.prettierrc ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 100,
6
+ "plugins": ["prettier-plugin-svelte"],
7
+ "pluginSearchDirs": ["."],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9
+ }
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from node:18-alpine
2
+
3
+ WORKDIR /app
4
+ COPY . .
5
+ COPY package.json package-lock.json
6
+ RUN npm install
7
+ COPY . .
8
+ RUN npm run build
9
+
10
+ EXPOSE 3000
11
+ CMD ["node", "build"]
12
+
13
+ # RUN npm install
14
+ # RUN npm run build
15
+
16
+ # WORKDIR /app
17
+ # RUN rm -rf ./*
18
+ # COPY --from=build /app/package.json .
19
+ # COPY --from=build /app/build .
20
+ # RUN npm run
README.md CHANGED
@@ -1,11 +1,12 @@
1
  ---
2
- title: Sk Docker
3
  emoji: 🐨
4
  colorFrom: purple
5
  colorTo: pink
6
  sdk: docker
7
  pinned: false
8
  license: openrail
 
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: SvelteKit Docker
3
  emoji: 🐨
4
  colorFrom: purple
5
  colorTo: pink
6
  sdk: docker
7
  pinned: false
8
  license: openrail
9
+ app_port: 3000
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
jsconfig.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sk-docker",
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
+ "lint": "prettier --plugin-search-dir . --check . && eslint .",
11
+ "format": "prettier --plugin-search-dir . --write ."
12
+ },
13
+ "devDependencies": {
14
+ "@fontsource/fira-mono": "^4.5.10",
15
+ "@neoconfetti/svelte": "^1.0.0",
16
+ "@sveltejs/adapter-auto": "^2.0.0",
17
+ "@sveltejs/adapter-node": "^1.2.0",
18
+ "@sveltejs/kit": "^1.5.0",
19
+ "@types/cookie": "^0.5.1",
20
+ "eslint": "^8.28.0",
21
+ "eslint-config-prettier": "^8.5.0",
22
+ "eslint-plugin-svelte3": "^4.0.0",
23
+ "prettier": "^2.8.0",
24
+ "prettier-plugin-svelte": "^2.8.1",
25
+ "svelte": "^3.54.0",
26
+ "svelte-check": "^3.0.1",
27
+ "typescript": "^4.9.3",
28
+ "vite": "^4.0.0"
29
+ },
30
+ "type": "module"
31
+ }
src/app.d.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
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 {};
src/app.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
6
+ <meta name="viewport" content="width=device-width" />
7
+ %sveltekit.head%
8
+ </head>
9
+ <body data-sveltekit-preload-data="hover">
10
+ <div style="display: contents">%sveltekit.body%</div>
11
+ </body>
12
+ </html>
src/lib/images/github.svg ADDED
src/lib/images/svelte-logo.svg ADDED
src/lib/images/svelte-welcome.png ADDED
src/lib/images/svelte-welcome.webp ADDED
src/routes/+layout.svelte ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import Header from './Header.svelte';
3
+ import './styles.css';
4
+ </script>
5
+
6
+ <div class="app">
7
+ <Header />
8
+
9
+ <main>
10
+ <slot />
11
+ </main>
12
+
13
+ <footer>
14
+ <p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>
15
+ </footer>
16
+ </div>
17
+
18
+ <style>
19
+ .app {
20
+ display: flex;
21
+ flex-direction: column;
22
+ min-height: 100vh;
23
+ }
24
+
25
+ main {
26
+ flex: 1;
27
+ display: flex;
28
+ flex-direction: column;
29
+ padding: 1rem;
30
+ width: 100%;
31
+ max-width: 64rem;
32
+ margin: 0 auto;
33
+ box-sizing: border-box;
34
+ }
35
+
36
+ footer {
37
+ display: flex;
38
+ flex-direction: column;
39
+ justify-content: center;
40
+ align-items: center;
41
+ padding: 12px;
42
+ }
43
+
44
+ footer a {
45
+ font-weight: bold;
46
+ }
47
+
48
+ @media (min-width: 480px) {
49
+ footer {
50
+ padding: 12px 0;
51
+ }
52
+ }
53
+ </style>
src/routes/+page.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
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;
src/routes/+page.svelte ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import Counter from './Counter.svelte';
3
+ import welcome from '$lib/images/svelte-welcome.webp';
4
+ import welcome_fallback from '$lib/images/svelte-welcome.png';
5
+ </script>
6
+
7
+ <svelte:head>
8
+ <title>Home</title>
9
+ <meta name="description" content="Svelte demo app" />
10
+ </svelte:head>
11
+
12
+ <section>
13
+ <h1>
14
+ <span class="welcome">
15
+ <picture>
16
+ <source srcset={welcome} type="image/webp" />
17
+ <img src={welcome_fallback} alt="Welcome" />
18
+ </picture>
19
+ </span>
20
+
21
+ to your new<br />SvelteKit app
22
+ </h1>
23
+
24
+ <h2>
25
+ try editing <strong>src/routes/+page.svelte</strong>
26
+ </h2>
27
+
28
+ <Counter />
29
+ </section>
30
+
31
+ <style>
32
+ section {
33
+ display: flex;
34
+ flex-direction: column;
35
+ justify-content: center;
36
+ align-items: center;
37
+ flex: 0.6;
38
+ }
39
+
40
+ h1 {
41
+ width: 100%;
42
+ }
43
+
44
+ .welcome {
45
+ display: block;
46
+ position: relative;
47
+ width: 100%;
48
+ height: 0;
49
+ padding: 0 0 calc(100% * 495 / 2048) 0;
50
+ }
51
+
52
+ .welcome img {
53
+ position: absolute;
54
+ width: 100%;
55
+ height: 100%;
56
+ top: 0;
57
+ display: block;
58
+ }
59
+ </style>
src/routes/Counter.svelte ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import { spring } from 'svelte/motion';
3
+
4
+ let count = 0;
5
+
6
+ const displayed_count = spring();
7
+ $: displayed_count.set(count);
8
+ $: offset = modulo($displayed_count, 1);
9
+
10
+ /**
11
+ * @param {number} n
12
+ * @param {number} m
13
+ */
14
+ function modulo(n, m) {
15
+ // handle negative numbers
16
+ return ((n % m) + m) % m;
17
+ }
18
+ </script>
19
+
20
+ <div class="counter">
21
+ <button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
22
+ <svg aria-hidden="true" viewBox="0 0 1 1">
23
+ <path d="M0,0.5 L1,0.5" />
24
+ </svg>
25
+ </button>
26
+
27
+ <div class="counter-viewport">
28
+ <div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
29
+ <strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
30
+ <strong>{Math.floor($displayed_count)}</strong>
31
+ </div>
32
+ </div>
33
+
34
+ <button on:click={() => (count += 1)} aria-label="Increase the counter by one">
35
+ <svg aria-hidden="true" viewBox="0 0 1 1">
36
+ <path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
37
+ </svg>
38
+ </button>
39
+ </div>
40
+
41
+ <style>
42
+ .counter {
43
+ display: flex;
44
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
45
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
46
+ margin: 1rem 0;
47
+ }
48
+
49
+ .counter button {
50
+ width: 2em;
51
+ padding: 0;
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ border: 0;
56
+ background-color: transparent;
57
+ touch-action: manipulation;
58
+ font-size: 2rem;
59
+ }
60
+
61
+ .counter button:hover {
62
+ background-color: var(--color-bg-1);
63
+ }
64
+
65
+ svg {
66
+ width: 25%;
67
+ height: 25%;
68
+ }
69
+
70
+ path {
71
+ vector-effect: non-scaling-stroke;
72
+ stroke-width: 2px;
73
+ stroke: #444;
74
+ }
75
+
76
+ .counter-viewport {
77
+ width: 8em;
78
+ height: 4em;
79
+ overflow: hidden;
80
+ text-align: center;
81
+ position: relative;
82
+ }
83
+
84
+ .counter-viewport strong {
85
+ position: absolute;
86
+ display: flex;
87
+ width: 100%;
88
+ height: 100%;
89
+ font-weight: 400;
90
+ color: var(--color-theme-1);
91
+ font-size: 4rem;
92
+ align-items: center;
93
+ justify-content: center;
94
+ }
95
+
96
+ .counter-digits {
97
+ position: absolute;
98
+ width: 100%;
99
+ height: 100%;
100
+ }
101
+
102
+ .hidden {
103
+ top: -100%;
104
+ user-select: none;
105
+ }
106
+ </style>
src/routes/Header.svelte ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import { page } from '$app/stores';
3
+ import logo from '$lib/images/svelte-logo.svg';
4
+ import github from '$lib/images/github.svg';
5
+ </script>
6
+
7
+ <header>
8
+ <div class="corner">
9
+ <a href="https://kit.svelte.dev">
10
+ <img src={logo} alt="SvelteKit" />
11
+ </a>
12
+ </div>
13
+
14
+ <nav>
15
+ <svg viewBox="0 0 2 3" aria-hidden="true">
16
+ <path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
17
+ </svg>
18
+ <ul>
19
+ <li aria-current={$page.url.pathname === '/' ? 'page' : undefined}>
20
+ <a href="/">Home</a>
21
+ </li>
22
+ <li aria-current={$page.url.pathname === '/about' ? 'page' : undefined}>
23
+ <a href="/about">About</a>
24
+ </li>
25
+ <li aria-current={$page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>
26
+ <a href="/sverdle">Sverdle</a>
27
+ </li>
28
+ </ul>
29
+ <svg viewBox="0 0 2 3" aria-hidden="true">
30
+ <path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
31
+ </svg>
32
+ </nav>
33
+
34
+ <div class="corner">
35
+ <a href="https://github.com/sveltejs/kit">
36
+ <img src={github} alt="GitHub" />
37
+ </a>
38
+ </div>
39
+ </header>
40
+
41
+ <style>
42
+ header {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ }
46
+
47
+ .corner {
48
+ width: 3em;
49
+ height: 3em;
50
+ }
51
+
52
+ .corner a {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ width: 100%;
57
+ height: 100%;
58
+ }
59
+
60
+ .corner img {
61
+ width: 2em;
62
+ height: 2em;
63
+ object-fit: contain;
64
+ }
65
+
66
+ nav {
67
+ display: flex;
68
+ justify-content: center;
69
+ --background: rgba(255, 255, 255, 0.7);
70
+ }
71
+
72
+ svg {
73
+ width: 2em;
74
+ height: 3em;
75
+ display: block;
76
+ }
77
+
78
+ path {
79
+ fill: var(--background);
80
+ }
81
+
82
+ ul {
83
+ position: relative;
84
+ padding: 0;
85
+ margin: 0;
86
+ height: 3em;
87
+ display: flex;
88
+ justify-content: center;
89
+ align-items: center;
90
+ list-style: none;
91
+ background: var(--background);
92
+ background-size: contain;
93
+ }
94
+
95
+ li {
96
+ position: relative;
97
+ height: 100%;
98
+ }
99
+
100
+ li[aria-current='page']::before {
101
+ --size: 6px;
102
+ content: '';
103
+ width: 0;
104
+ height: 0;
105
+ position: absolute;
106
+ top: 0;
107
+ left: calc(50% - var(--size));
108
+ border: var(--size) solid transparent;
109
+ border-top: var(--size) solid var(--color-theme-1);
110
+ }
111
+
112
+ nav a {
113
+ display: flex;
114
+ height: 100%;
115
+ align-items: center;
116
+ padding: 0 0.5rem;
117
+ color: var(--color-text);
118
+ font-weight: 700;
119
+ font-size: 0.8rem;
120
+ text-transform: uppercase;
121
+ letter-spacing: 0.1em;
122
+ text-decoration: none;
123
+ transition: color 0.2s linear;
124
+ }
125
+
126
+ a:hover {
127
+ color: var(--color-theme-1);
128
+ }
129
+ </style>
src/routes/about/+page.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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;
src/routes/about/+page.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svelte:head>
2
+ <title>About</title>
3
+ <meta name="description" content="About this app" />
4
+ </svelte:head>
5
+
6
+ <div class="text-column">
7
+ <h1>About this app</h1>
8
+
9
+ <p>
10
+ This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the
11
+ following into your command line and following the prompts:
12
+ </p>
13
+
14
+ <pre>npm create svelte@latest</pre>
15
+
16
+ <p>
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
+ </p>
21
+
22
+ <p>
23
+ The <a href="/sverdle">Sverdle</a> page illustrates SvelteKit's data loading and form handling. Try
24
+ using it with JavaScript disabled!
25
+ </p>
26
+ </div>
src/routes/styles.css ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
src/routes/sverdle/+page.server.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ };
src/routes/sverdle/+page.svelte ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import { confetti } from '@neoconfetti/svelte';
3
+ import { enhance } from '$app/forms';
4
+
5
+ import { reduced_motion } from './reduced-motion';
6
+
7
+ /** @type {import('./$types').PageData} */
8
+ export let data;
9
+
10
+ /** @type {import('./$types').ActionData} */
11
+ export let form;
12
+
13
+ /** Whether or not the user has won */
14
+ $: won = data.answers.at(-1) === 'xxxxx';
15
+
16
+ /** The index of the current guess */
17
+ $: i = won ? -1 : data.answers.length;
18
+
19
+ /** Whether the current guess can be submitted */
20
+ $: submittable = data.guesses[i]?.length === 5;
21
+
22
+ /**
23
+ * A map of classnames for all letters that have been guessed,
24
+ * used for styling the keyboard
25
+ * @type {Record<string, 'exact' | 'close' | 'missing'>}
26
+ */
27
+ let classnames;
28
+
29
+ /**
30
+ * A map of descriptions for all letters that have been guessed,
31
+ * used for adding text for assistive technology (e.g. screen readers)
32
+ * @type {Record<string, string>}
33
+ */
34
+ let description;
35
+
36
+ $: {
37
+ classnames = {};
38
+ description = {};
39
+
40
+ data.answers.forEach((answer, i) => {
41
+ const guess = data.guesses[i];
42
+
43
+ for (let i = 0; i < 5; i += 1) {
44
+ const letter = guess[i];
45
+
46
+ if (answer[i] === 'x') {
47
+ classnames[letter] = 'exact';
48
+ description[letter] = 'correct';
49
+ } else if (!classnames[letter]) {
50
+ classnames[letter] = answer[i] === 'c' ? 'close' : 'missing';
51
+ description[letter] = answer[i] === 'c' ? 'present' : 'absent';
52
+ }
53
+ }
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Modify the game state without making a trip to the server,
59
+ * if client-side JavaScript is enabled
60
+ * @param {MouseEvent} event
61
+ */
62
+ function update(event) {
63
+ const guess = data.guesses[i];
64
+ const key = /** @type {HTMLButtonElement} */ (event.target).getAttribute('data-key');
65
+
66
+ if (key === 'backspace') {
67
+ data.guesses[i] = guess.slice(0, -1);
68
+ if (form?.badGuess) form.badGuess = false;
69
+ } else if (guess.length < 5) {
70
+ data.guesses[i] += key;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Trigger form logic in response to a keydown event, so that
76
+ * desktop users can use the keyboard to play the game
77
+ * @param {KeyboardEvent} event
78
+ */
79
+ function keydown(event) {
80
+ if (event.metaKey) return;
81
+
82
+ document
83
+ .querySelector(`[data-key="${event.key}" i]`)
84
+ ?.dispatchEvent(new MouseEvent('click', { cancelable: true }));
85
+ }
86
+ </script>
87
+
88
+ <svelte:window on:keydown={keydown} />
89
+
90
+ <svelte:head>
91
+ <title>Sverdle</title>
92
+ <meta name="description" content="A Wordle clone written in SvelteKit" />
93
+ </svelte:head>
94
+
95
+ <h1 class="visually-hidden">Sverdle</h1>
96
+
97
+ <form
98
+ method="POST"
99
+ action="?/enter"
100
+ use:enhance={() => {
101
+ // prevent default callback from resetting the form
102
+ return ({ update }) => {
103
+ update({ reset: false });
104
+ };
105
+ }}
106
+ >
107
+ <a class="how-to-play" href="/sverdle/how-to-play">How to play</a>
108
+
109
+ <div class="grid" class:playing={!won} class:bad-guess={form?.badGuess}>
110
+ {#each Array(6) as _, row}
111
+ {@const current = row === i}
112
+ <h2 class="visually-hidden">Row {row + 1}</h2>
113
+ <div class="row" class:current>
114
+ {#each Array(5) as _, 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
+ <div class="letter" class:exact class:close class:missing class:selected>
122
+ {value}
123
+ <span class="visually-hidden">
124
+ {#if exact}
125
+ (correct)
126
+ {:else if close}
127
+ (present)
128
+ {:else if missing}
129
+ (absent)
130
+ {:else}
131
+ empty
132
+ {/if}
133
+ </span>
134
+ <input name="guess" disabled={!current} type="hidden" {value} />
135
+ </div>
136
+ {/each}
137
+ </div>
138
+ {/each}
139
+ </div>
140
+
141
+ <div class="controls">
142
+ {#if won || data.answers.length >= 6}
143
+ {#if !won && data.answer}
144
+ <p>the answer was "{data.answer}"</p>
145
+ {/if}
146
+ <button data-key="enter" class="restart selected" formaction="?/restart">
147
+ {won ? 'you won :)' : `game over :(`} play again?
148
+ </button>
149
+ {:else}
150
+ <div class="keyboard">
151
+ <button data-key="enter" class:selected={submittable} disabled={!submittable}>enter</button>
152
+
153
+ <button
154
+ on:click|preventDefault={update}
155
+ data-key="backspace"
156
+ formaction="?/update"
157
+ name="key"
158
+ value="backspace"
159
+ >
160
+ back
161
+ </button>
162
+
163
+ {#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row}
164
+ <div class="row">
165
+ {#each row as letter}
166
+ <button
167
+ on:click|preventDefault={update}
168
+ data-key={letter}
169
+ class={classnames[letter]}
170
+ disabled={data.guesses[i].length === 5}
171
+ formaction="?/update"
172
+ name="key"
173
+ value={letter}
174
+ aria-label="{letter} {description[letter] || ''}"
175
+ >
176
+ {letter}
177
+ </button>
178
+ {/each}
179
+ </div>
180
+ {/each}
181
+ </div>
182
+ {/if}
183
+ </div>
184
+ </form>
185
+
186
+ {#if won}
187
+ <div
188
+ style="position: absolute; left: 50%; top: 30%"
189
+ use:confetti={{
190
+ particleCount: $reduced_motion ? 0 : undefined,
191
+ force: 0.7,
192
+ stageWidth: window.innerWidth,
193
+ stageHeight: window.innerHeight,
194
+ colors: ['#ff3e00', '#40b3ff', '#676778']
195
+ }}
196
+ />
197
+ {/if}
198
+
199
+ <style>
200
+ form {
201
+ width: 100%;
202
+ height: 100%;
203
+ display: flex;
204
+ flex-direction: column;
205
+ align-items: center;
206
+ justify-content: center;
207
+ gap: 1rem;
208
+ flex: 1;
209
+ }
210
+
211
+ .how-to-play {
212
+ color: var(--color-text);
213
+ }
214
+
215
+ .how-to-play::before {
216
+ content: 'i';
217
+ display: inline-block;
218
+ font-size: 0.8em;
219
+ font-weight: 900;
220
+ width: 1em;
221
+ height: 1em;
222
+ padding: 0.2em;
223
+ line-height: 1;
224
+ border: 1.5px solid var(--color-text);
225
+ border-radius: 50%;
226
+ text-align: center;
227
+ margin: 0 0.5em 0 0;
228
+ position: relative;
229
+ top: -0.05em;
230
+ }
231
+
232
+ .grid {
233
+ --width: min(100vw, 40vh, 380px);
234
+ max-width: var(--width);
235
+ align-self: center;
236
+ justify-self: center;
237
+ width: 100%;
238
+ height: 100%;
239
+ display: flex;
240
+ flex-direction: column;
241
+ justify-content: flex-start;
242
+ }
243
+
244
+ .grid .row {
245
+ display: grid;
246
+ grid-template-columns: repeat(5, 1fr);
247
+ grid-gap: 0.2rem;
248
+ margin: 0 0 0.2rem 0;
249
+ }
250
+
251
+ @media (prefers-reduced-motion: no-preference) {
252
+ .grid.bad-guess .row.current {
253
+ animation: wiggle 0.5s;
254
+ }
255
+ }
256
+
257
+ .grid.playing .row.current {
258
+ filter: drop-shadow(3px 3px 10px var(--color-bg-0));
259
+ }
260
+
261
+ .letter {
262
+ aspect-ratio: 1;
263
+ width: 100%;
264
+ display: flex;
265
+ align-items: center;
266
+ justify-content: center;
267
+ text-align: center;
268
+ box-sizing: border-box;
269
+ text-transform: lowercase;
270
+ border: none;
271
+ font-size: calc(0.08 * var(--width));
272
+ border-radius: 2px;
273
+ background: white;
274
+ margin: 0;
275
+ color: rgba(0, 0, 0, 0.7);
276
+ }
277
+
278
+ .letter.missing {
279
+ background: rgba(255, 255, 255, 0.5);
280
+ color: rgba(0, 0, 0, 0.5);
281
+ }
282
+
283
+ .letter.exact {
284
+ background: var(--color-theme-2);
285
+ color: white;
286
+ }
287
+
288
+ .letter.close {
289
+ border: 2px solid var(--color-theme-2);
290
+ }
291
+
292
+ .selected {
293
+ outline: 2px solid var(--color-theme-1);
294
+ }
295
+
296
+ .controls {
297
+ text-align: center;
298
+ justify-content: center;
299
+ height: min(18vh, 10rem);
300
+ }
301
+
302
+ .keyboard {
303
+ --gap: 0.2rem;
304
+ position: relative;
305
+ display: flex;
306
+ flex-direction: column;
307
+ gap: var(--gap);
308
+ height: 100%;
309
+ }
310
+
311
+ .keyboard .row {
312
+ display: flex;
313
+ justify-content: center;
314
+ gap: 0.2rem;
315
+ flex: 1;
316
+ }
317
+
318
+ .keyboard button,
319
+ .keyboard button:disabled {
320
+ --size: min(8vw, 4vh, 40px);
321
+ background-color: white;
322
+ color: black;
323
+ width: var(--size);
324
+ border: none;
325
+ border-radius: 2px;
326
+ font-size: calc(var(--size) * 0.5);
327
+ margin: 0;
328
+ }
329
+
330
+ .keyboard button.exact {
331
+ background: var(--color-theme-2);
332
+ color: white;
333
+ }
334
+
335
+ .keyboard button.missing {
336
+ opacity: 0.5;
337
+ }
338
+
339
+ .keyboard button.close {
340
+ border: 2px solid var(--color-theme-2);
341
+ }
342
+
343
+ .keyboard button:focus {
344
+ background: var(--color-theme-1);
345
+ color: white;
346
+ outline: none;
347
+ }
348
+
349
+ .keyboard button[data-key='enter'],
350
+ .keyboard button[data-key='backspace'] {
351
+ position: absolute;
352
+ bottom: 0;
353
+ width: calc(1.5 * var(--size));
354
+ height: calc(1 / 3 * (100% - 2 * var(--gap)));
355
+ text-transform: uppercase;
356
+ font-size: calc(0.3 * var(--size));
357
+ padding-top: calc(0.15 * var(--size));
358
+ }
359
+
360
+ .keyboard button[data-key='enter'] {
361
+ right: calc(50% + 3.5 * var(--size) + 0.8rem);
362
+ }
363
+
364
+ .keyboard button[data-key='backspace'] {
365
+ left: calc(50% + 3.5 * var(--size) + 0.8rem);
366
+ }
367
+
368
+ .keyboard button[data-key='enter']:disabled {
369
+ opacity: 0.5;
370
+ }
371
+
372
+ .restart {
373
+ width: 100%;
374
+ padding: 1rem;
375
+ background: rgba(255, 255, 255, 0.5);
376
+ border-radius: 2px;
377
+ border: none;
378
+ }
379
+
380
+ .restart:focus,
381
+ .restart:hover {
382
+ background: var(--color-theme-1);
383
+ color: white;
384
+ outline: none;
385
+ }
386
+
387
+ @keyframes wiggle {
388
+ 0% {
389
+ transform: translateX(0);
390
+ }
391
+ 10% {
392
+ transform: translateX(-2px);
393
+ }
394
+ 30% {
395
+ transform: translateX(4px);
396
+ }
397
+ 50% {
398
+ transform: translateX(-6px);
399
+ }
400
+ 70% {
401
+ transform: translateX(+4px);
402
+ }
403
+ 90% {
404
+ transform: translateX(-2px);
405
+ }
406
+ 100% {
407
+ transform: translateX(0);
408
+ }
409
+ }
410
+ </style>
src/routes/sverdle/game.js ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
src/routes/sverdle/how-to-play/+page.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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;
src/routes/sverdle/how-to-play/+page.svelte ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svelte:head>
2
+ <title>How to play Sverdle</title>
3
+ <meta name="description" content="How to play Sverdle" />
4
+ </svelte:head>
5
+
6
+ <div class="text-column">
7
+ <h1>How to play Sverdle</h1>
8
+
9
+ <p>
10
+ Sverdle is a clone of <a href="https://www.nytimes.com/games/wordle/index.html">Wordle</a>, the
11
+ word guessing game. To play, enter a five-letter English word. For example:
12
+ </p>
13
+
14
+ <div class="example">
15
+ <span class="close">r</span>
16
+ <span class="missing">i</span>
17
+ <span class="close">t</span>
18
+ <span class="missing">z</span>
19
+ <span class="exact">y</span>
20
+ </div>
21
+
22
+ <p>
23
+ The <span class="exact">y</span> is in the right place. <span class="close">r</span> and
24
+ <span class="close">t</span>
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
+ </p>
28
+
29
+ <div class="example">
30
+ <span class="exact">p</span>
31
+ <span class="exact">a</span>
32
+ <span class="exact">r</span>
33
+ <span class="exact">t</span>
34
+ <span class="exact">y</span>
35
+ </div>
36
+
37
+ <p>This time we guessed right! You have <strong>six</strong> guesses to get the word.</p>
38
+
39
+ <p>
40
+ Unlike the original Wordle, Sverdle runs on the server instead of in the browser, making it
41
+ impossible to cheat. It uses <code>&lt;form&gt;</code> and cookies to submit data, meaning you can
42
+ even play with JavaScript disabled!
43
+ </p>
44
+ </div>
45
+
46
+ <style>
47
+ span {
48
+ display: inline-flex;
49
+ justify-content: center;
50
+ align-items: center;
51
+ font-size: 0.8em;
52
+ width: 2.4em;
53
+ height: 2.4em;
54
+ background-color: white;
55
+ box-sizing: border-box;
56
+ border-radius: 2px;
57
+ border-width: 2px;
58
+ color: rgba(0, 0, 0, 0.7);
59
+ }
60
+
61
+ .missing {
62
+ background: rgba(255, 255, 255, 0.5);
63
+ color: rgba(0, 0, 0, 0.5);
64
+ }
65
+
66
+ .close {
67
+ border-style: solid;
68
+ border-color: var(--color-theme-2);
69
+ }
70
+
71
+ .exact {
72
+ background: var(--color-theme-2);
73
+ color: white;
74
+ }
75
+
76
+ .example {
77
+ display: flex;
78
+ justify-content: flex-start;
79
+ margin: 1rem 0;
80
+ gap: 0.2rem;
81
+ }
82
+
83
+ .example span {
84
+ font-size: 1.4rem;
85
+ }
86
+
87
+ p span {
88
+ position: relative;
89
+ border-width: 1px;
90
+ border-radius: 1px;
91
+ font-size: 0.4em;
92
+ transform: scale(2) translate(0, -10%);
93
+ margin: 0 1em;
94
+ }
95
+ </style>
src/routes/sverdle/reduced-motion.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ });
src/routes/sverdle/words.server.js ADDED
The diff for this file is too large to render. See raw diff
 
static/favicon.png ADDED
static/robots.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
svelte.config.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import adapter from '@sveltejs/adapter-node';
2
+
3
+ /** @type {import('@sveltejs/kit').Config} */
4
+ const config = {
5
+ kit: {
6
+ adapter: adapter()
7
+ }
8
+ };
9
+
10
+ export default config;
vite.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { sveltekit } from '@sveltejs/kit/vite';
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig({
5
+ plugins: [sveltekit()]
6
+ });