sv 0.9.12 → 0.9.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { B as We, F as Ge, H as ke, I as J, J as require_picocolors, K as up$1, L as Ke, N as De, O as isVersionUnsupportedBelow, P as Fe, R as T, U as ze, V as et, W as Vu, X as Option, Y as Command, Z as program, _ as parseScript, a as addPnpmBuildDependencies, c as installOption, et as __toESM, f as detect, g as parseJson, i as AGENT_NAMES, l as packageManagerPrompt, n as templates, o as getUserAgent, p as resolveCommand, q as from, r as dist, s as installDependencies, t as create$1, v as parseSvelte, z as Ue } from "./create-Bt2-1pFJ.js";
3
- import { a as formatFiles, c as getCommunityAddon, d as createDefault, f as addDefault, i as createWorkspace, l as getAddonDetails, m as overrideProperties, o as getHighlighter, p as create$2, r as setupAddons, s as communityAddonIds, t as applyAddons, u as officialAddons } from "./install-Dy7mZYPj.js";
3
+ import { a as formatFiles, c as getCommunityAddon, d as createDefault, f as addDefault, i as createWorkspace, l as getAddonDetails, m as overrideProperties, o as getHighlighter, p as create$2, r as setupAddons, s as communityAddonIds, t as applyAddons, u as officialAddons } from "./install-CisPUEH8.js";
4
4
  import { exec, execSync } from "node:child_process";
5
5
  import path, { dirname, join } from "node:path";
6
6
  import fs, { existsSync } from "node:fs";
@@ -12,7 +12,7 @@ import { pipeline } from "node:stream/promises";
12
12
 
13
13
  //#region package.json
14
14
  var name = "sv";
15
- var version = "0.9.12";
15
+ var version = "0.9.14";
16
16
 
17
17
  //#endregion
18
18
  //#region ../../node_modules/.pnpm/valibot@0.41.0_typescript@5.8.3/node_modules/valibot/dist/index.js
@@ -773,11 +773,17 @@ function addEslintConfigPrettier(content) {
773
773
  else elements.push(...nodesToInsert);
774
774
  return generateCode();
775
775
  }
776
- function addToDemoPage(content, path$1) {
777
- const { template, generateCode } = parseSvelte(content);
778
- for (const node of template.ast.childNodes) if (node.type === "tag" && node.attribs["href"] === `/demo/${path$1}`) return content;
779
- const newLine = template.source ? "\n" : "";
780
- return generateCode({ template: template.source + `${newLine}<a href="/demo/${path$1}">${path$1}</a>` });
776
+ function addToDemoPage(existingContent, path$1, typescript) {
777
+ const { script, template, generateCode } = parseSvelte(existingContent, { typescript });
778
+ for (const node of template.ast.childNodes) if (node.type === "tag" && node.attribs["href"].includes(`/demo/${path$1}`)) return existingContent;
779
+ addNamed(script.ast, {
780
+ imports: ["resolve"],
781
+ from: "$app/paths"
782
+ });
783
+ return generateCode({
784
+ script: script.generateCode(),
785
+ template: `<a href={resolve('/demo/${path$1}')}>${path$1}</a>\n${template.source}`
786
+ });
781
787
  }
782
788
  /**
783
789
  * Returns the corresponding `@types/node` version for the version of Node.js running in the current process.
@@ -1656,7 +1662,7 @@ var lucia_default = defineAddon({
1656
1662
  });
1657
1663
  if (options$7.demo) {
1658
1664
  sv.file(`${kit?.routesDirectory}/demo/+page.svelte`, (content) => {
1659
- return addToDemoPage(content, "lucia");
1665
+ return addToDemoPage(content, "lucia", typescript);
1660
1666
  });
1661
1667
  sv.file(`${kit.routesDirectory}/demo/lucia/login/+page.server.${ext}`, (content) => {
1662
1668
  if (content) {
@@ -2143,7 +2149,7 @@ var paraglide_default = defineAddon({
2143
2149
  });
2144
2150
  if (options$7.demo) {
2145
2151
  sv.file(`${kit.routesDirectory}/demo/+page.svelte`, (content) => {
2146
- return addToDemoPage(content, "paraglide");
2152
+ return addToDemoPage(content, "paraglide", typescript);
2147
2153
  });
2148
2154
  sv.file(`${kit.routesDirectory}/demo/paraglide/+page.svelte`, (content) => {
2149
2155
  const { script, template, generateCode } = parseSvelte(content, { typescript });
@@ -2260,9 +2266,9 @@ var mcp_default = defineAddon({
2260
2266
  const getLocalConfig = (o) => {
2261
2267
  return {
2262
2268
  ...o?.type ? { type: o.type } : {},
2263
- command: "npx",
2264
- args: ["-y", "@sveltejs/mcp"],
2265
- ...o?.env ? { env: {} } : {}
2269
+ command: o?.command ?? "npx",
2270
+ ...o?.env ? { env: {} } : {},
2271
+ ...o?.args === null ? {} : { args: o?.args ?? ["-y", "@sveltejs/mcp"] }
2266
2272
  };
2267
2273
  };
2268
2274
  const getRemoteConfig = (o) => {
@@ -2279,13 +2285,22 @@ var mcp_default = defineAddon({
2279
2285
  env: true
2280
2286
  },
2281
2287
  cursor: { filePath: ".cursor/mcp.json" },
2282
- gemini: { filePath: ".gemini/settings.json" },
2288
+ gemini: {
2289
+ schema: "https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json",
2290
+ filePath: ".gemini/settings.json"
2291
+ },
2283
2292
  opencode: {
2284
2293
  schema: "https://opencode.ai/config.json",
2285
2294
  mcpServersKey: "mcp",
2286
2295
  filePath: "opencode.json",
2287
2296
  typeLocal: "local",
2288
- typeRemote: "remote"
2297
+ typeRemote: "remote",
2298
+ command: [
2299
+ "npx",
2300
+ "-y",
2301
+ "@sveltejs/mcp"
2302
+ ],
2303
+ args: null
2289
2304
  },
2290
2305
  vscode: {
2291
2306
  mcpServersKey: "servers",
@@ -2296,7 +2311,7 @@ var mcp_default = defineAddon({
2296
2311
  for (const ide of options$7.ide) {
2297
2312
  const value = configurator[ide];
2298
2313
  if ("other" in value) continue;
2299
- const { mcpServersKey, filePath, typeLocal, typeRemote, env, schema } = value;
2314
+ const { mcpServersKey, filePath, typeLocal, typeRemote, env, schema, command, args } = value;
2300
2315
  sv.file(filePath, (content) => {
2301
2316
  const { data, generateCode } = parseJson(content);
2302
2317
  if (schema) data["$schema"] = schema;
@@ -2304,7 +2319,9 @@ var mcp_default = defineAddon({
2304
2319
  data[key] ??= {};
2305
2320
  data[key].svelte = options$7.setup === "local" ? getLocalConfig({
2306
2321
  type: typeLocal,
2307
- env
2322
+ env,
2323
+ command,
2324
+ args
2308
2325
  }) : getRemoteConfig({ type: typeRemote });
2309
2326
  return generateCode();
2310
2327
  });
@@ -2385,7 +2402,7 @@ var prettier_default = defineAddon({
2385
2402
  shortDescription: "formatter",
2386
2403
  homepage: "https://prettier.io",
2387
2404
  options: {},
2388
- run: ({ sv, dependencyVersion }) => {
2405
+ run: ({ sv, dependencyVersion, kit }) => {
2389
2406
  const tailwindcssInstalled = Boolean(dependencyVersion("tailwindcss"));
2390
2407
  if (tailwindcssInstalled) sv.devDependency("prettier-plugin-tailwindcss", "^0.7.1");
2391
2408
  sv.devDependency("prettier", "^3.6.2");
@@ -2422,7 +2439,7 @@ var prettier_default = defineAddon({
2422
2439
  const plugins$1 = data.plugins;
2423
2440
  if (tailwindcssInstalled) {
2424
2441
  if (!plugins$1.includes("prettier-plugin-tailwindcss")) data.plugins.unshift("prettier-plugin-tailwindcss");
2425
- data.tailwindStylesheet ??= "./src/app.css";
2442
+ data.tailwindStylesheet ??= kit ? `${kit?.routesDirectory}/layout.css` : "./src/app.css";
2426
2443
  }
2427
2444
  if (!plugins$1.includes("prettier-plugin-svelte")) data.plugins.unshift("prettier-plugin-svelte");
2428
2445
  data.overrides ??= [];
@@ -2609,6 +2626,13 @@ var tailwindcss_default = defineAddon({
2609
2626
  options: options$1,
2610
2627
  run: ({ sv, options: options$7, files, typescript, kit, dependencyVersion }) => {
2611
2628
  const prettierInstalled = Boolean(dependencyVersion("prettier"));
2629
+ const stylesheet = kit ? {
2630
+ rootPath: `${kit.routesDirectory}/layout.css`,
2631
+ relativePath: "./layout.css"
2632
+ } : {
2633
+ rootPath: "src/app.css",
2634
+ relativePath: "./app.css"
2635
+ };
2612
2636
  sv.devDependency("tailwindcss", "^4.1.14");
2613
2637
  sv.devDependency("@tailwindcss/vite", "^4.1.14");
2614
2638
  sv.pnpmBuildDependency("@tailwindcss/oxide");
@@ -2630,7 +2654,7 @@ var tailwindcss_default = defineAddon({
2630
2654
  });
2631
2655
  return generateCode();
2632
2656
  });
2633
- sv.file("src/app.css", (content) => {
2657
+ sv.file(stylesheet.rootPath, (content) => {
2634
2658
  let atRules = parseCss(content).ast.nodes.filter((node) => node.type === "atrule");
2635
2659
  const findAtRule = (name, params) => atRules.find((rule) => rule.name === name && rule.params.replace(/['"]/g, "") === params);
2636
2660
  let code = content;
@@ -2650,12 +2674,12 @@ var tailwindcss_default = defineAddon({
2650
2674
  });
2651
2675
  if (!kit) sv.file("src/App.svelte", (content) => {
2652
2676
  const { script, generateCode } = parseSvelte(content, { typescript });
2653
- addEmpty(script.ast, { from: "./app.css" });
2677
+ addEmpty(script.ast, { from: stylesheet.relativePath });
2654
2678
  return generateCode({ script: script.generateCode() });
2655
2679
  });
2656
2680
  else sv.file(`${kit?.routesDirectory}/+layout.svelte`, (content) => {
2657
2681
  const { script, template, generateCode } = parseSvelte(content, { typescript });
2658
- addEmpty(script.ast, { from: "../app.css" });
2682
+ addEmpty(script.ast, { from: stylesheet.relativePath });
2659
2683
  if (content.length === 0) {
2660
2684
  const svelteVersion = dependencyVersion("svelte");
2661
2685
  if (!svelteVersion) throw new Error("Failed to determine svelte version");
@@ -2669,13 +2693,19 @@ var tailwindcss_default = defineAddon({
2669
2693
  template: content.length === 0 ? template.generateCode() : void 0
2670
2694
  });
2671
2695
  });
2696
+ sv.file(".vscode/settings.json", (content) => {
2697
+ const { data, generateCode } = parseJson(content);
2698
+ data["files.associations"] ??= {};
2699
+ data["files.associations"]["*.css"] = "tailwind";
2700
+ return generateCode();
2701
+ });
2672
2702
  if (prettierInstalled) sv.file(".prettierrc", (content) => {
2673
2703
  const { data, generateCode } = parseJson(content);
2674
2704
  const PLUGIN_NAME = "prettier-plugin-tailwindcss";
2675
2705
  data.plugins ??= [];
2676
2706
  const plugins$1 = data.plugins;
2677
2707
  if (!plugins$1.includes(PLUGIN_NAME)) plugins$1.push(PLUGIN_NAME);
2678
- data.tailwindStylesheet ??= "./src/app.css";
2708
+ data.tailwindStylesheet ??= stylesheet.rootPath;
2679
2709
  return generateCode();
2680
2710
  });
2681
2711
  }
package/dist/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { t as create } from "../create-Bt2-1pFJ.js";
2
- import { n as installAddon, u as officialAddons } from "../install-Dy7mZYPj.js";
2
+ import { n as installAddon, u as officialAddons } from "../install-CisPUEH8.js";
3
3
 
4
4
  export { create, installAddon, officialAddons };
package/dist/shared.json CHANGED
@@ -68,7 +68,7 @@
68
68
  "typescript"
69
69
  ],
70
70
  "exclude": [],
71
- "contents": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n\t\t\"moduleResolution\": \"bundler\"\n\t}\n\t// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias\n\t// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files\n\t//\n\t// To make changes to top-level options such as include and exclude, we recommend extending\n\t// the generated config; see https://svelte.dev/docs/kit/configuration#typescript\n}\n"
71
+ "contents": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowImportingTsExtensions\": true,\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n\t\t\"moduleResolution\": \"bundler\"\n\t}\n\t// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias\n\t// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files\n\t//\n\t// To make changes to top-level options such as include and exclude, we recommend extending\n\t// the generated config; see https://svelte.dev/docs/kit/configuration#typescript\n}\n"
72
72
  },
73
73
  {
74
74
  "name": "svelte.config.js",
@@ -129,7 +129,7 @@
129
129
  "typescript"
130
130
  ],
131
131
  "exclude": [],
132
- "contents": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n\t\t\"module\": \"NodeNext\",\n\t\t\"moduleResolution\": \"NodeNext\"\n\t}\n}\n"
132
+ "contents": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowImportingTsExtensions\": true,\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n\t\t\"module\": \"NodeNext\",\n\t\t\"moduleResolution\": \"NodeNext\"\n\t}\n}\n"
133
133
  },
134
134
  {
135
135
  "name": "vite.config.js",
@@ -5,7 +5,7 @@
5
5
  },
6
6
  {
7
7
  "name": "src/routes/+layout.svelte",
8
- "contents": "<script>\n\timport Header from './Header.svelte';\n\timport '../app.css';\n\n\t/** @type {{children: import('svelte').Snippet}} */\n\tlet { children } = $props();\n</script>\n\n<div class=\"app\">\n\t<Header />\n\n\t<main>\n\t\t{@render children()}\n\t</main>\n\n\t<footer>\n\t\t<p>\n\t\t\tvisit <a href=\"https://svelte.dev/docs/kit\">svelte.dev/docs/kit</a> to learn about SvelteKit\n\t\t</p>\n\t</footer>\n</div>\n\n<style>\n\t.app {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tmin-height: 100vh;\n\t}\n\n\tmain {\n\t\tflex: 1;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tpadding: 1rem;\n\t\twidth: 100%;\n\t\tmax-width: 64rem;\n\t\tmargin: 0 auto;\n\t\tbox-sizing: border-box;\n\t}\n\n\tfooter {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tpadding: 12px;\n\t}\n\n\tfooter a {\n\t\tfont-weight: bold;\n\t}\n\n\t@media (min-width: 480px) {\n\t\tfooter {\n\t\t\tpadding: 12px 0;\n\t\t}\n\t}\n</style>\n"
8
+ "contents": "<script>\n\timport Header from './Header.svelte';\n\timport './layout.css';\n\n\t/** @type {{children: import('svelte').Snippet}} */\n\tlet { children } = $props();\n</script>\n\n<div class=\"app\">\n\t<Header />\n\n\t<main>\n\t\t{@render children()}\n\t</main>\n\n\t<footer>\n\t\t<p>\n\t\t\tvisit <a href=\"https://svelte.dev/docs/kit\">svelte.dev/docs/kit</a> to learn about SvelteKit\n\t\t</p>\n\t</footer>\n</div>\n\n<style>\n\t.app {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tmin-height: 100vh;\n\t}\n\n\tmain {\n\t\tflex: 1;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tpadding: 1rem;\n\t\twidth: 100%;\n\t\tmax-width: 64rem;\n\t\tmargin: 0 auto;\n\t\tbox-sizing: border-box;\n\t}\n\n\tfooter {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tpadding: 12px;\n\t}\n\n\tfooter a {\n\t\tfont-weight: bold;\n\t}\n\n\t@media (min-width: 480px) {\n\t\tfooter {\n\t\t\tpadding: 12px 0;\n\t\t}\n\t}\n</style>\n"
9
9
  },
10
10
  {
11
11
  "name": "src/routes/+page.svelte",
@@ -21,11 +21,11 @@
21
21
  },
22
22
  {
23
23
  "name": "src/routes/Header.svelte",
24
- "contents": "<script>\n\timport { page } from '$app/state';\n\timport logo from '$lib/images/svelte-logo.svg';\n\timport github from '$lib/images/github.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href=\"/\">Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href=\"/about\">About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href=\"/sverdle\">Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
24
+ "contents": "<script>\n\timport { resolve } from '$app/paths';\n\timport { page } from '$app/state';\n\timport logo from '$lib/images/svelte-logo.svg';\n\timport github from '$lib/images/github.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/')}>Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/about')}>About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/sverdle')}>Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
25
25
  },
26
26
  {
27
27
  "name": "src/routes/about/+page.svelte",
28
- "contents": "<svelte:head>\n\t<title>About</title>\n\t<meta name=\"description\" content=\"About this app\" />\n</svelte:head>\n\n<div class=\"text-column\">\n\t<h1>About this app</h1>\n\n\t<p>\n\t\tThis is a <a href=\"https://svelte.dev/docs/kit\">SvelteKit</a> app. You can make your own by typing\n\t\tthe following into your command line and following the prompts:\n\t</p>\n\n\t<pre>npx sv create</pre>\n\n\t<p>\n\t\tThe page you're looking at is purely static HTML, with no client-side interactivity needed.\n\t\tBecause of that, we don't need to load any JavaScript. Try viewing the page's source, or opening\n\t\tthe devtools network panel and reloading.\n\t</p>\n\n\t<p>\n\t\tThe <a href=\"/sverdle\">Sverdle</a> page illustrates SvelteKit's data loading and form handling. Try\n\t\tusing it with JavaScript disabled!\n\t</p>\n</div>\n"
28
+ "contents": "<script>\n\timport { resolve } from '$app/paths';\n</script>\n\n<svelte:head>\n\t<title>About</title>\n\t<meta name=\"description\" content=\"About this app\" />\n</svelte:head>\n\n<div class=\"text-column\">\n\t<h1>About this app</h1>\n\n\t<p>\n\t\tThis is a <a href=\"https://svelte.dev/docs/kit\">SvelteKit</a> app. You can make your own by typing\n\t\tthe following into your command line and following the prompts:\n\t</p>\n\n\t<pre>npx sv create</pre>\n\n\t<p>\n\t\tThe page you're looking at is purely static HTML, with no client-side interactivity needed.\n\t\tBecause of that, we don't need to load any JavaScript. Try viewing the page's source, or opening\n\t\tthe devtools network panel and reloading.\n\t</p>\n\n\t<p>\n\t\tThe <a href={resolve('/sverdle')}>Sverdle</a> page illustrates SvelteKit's data loading and form\n\t\thandling. Try using it with JavaScript disabled!\n\t</p>\n</div>\n"
29
29
  },
30
30
  {
31
31
  "name": "src/routes/about/+page.js",
@@ -33,15 +33,15 @@
33
33
  },
34
34
  {
35
35
  "name": "src/routes/sverdle/+page.server.js",
36
- "contents": "import { fail } from '@sveltejs/kit';\nimport { Game } from './game';\n\n/** @satisfies {import('./$types').PageServerLoad} */\nexport const load = ({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n};\n\n/** @satisfies {import('./$types').Actions} */\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = /** @type {string[]} */ (data.getAll('guess'));\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n};\n"
36
+ "contents": "import { fail } from '@sveltejs/kit';\nimport { Game } from './game.js';\n\n/** @satisfies {import('./$types').PageServerLoad} */\nexport const load = ({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n};\n\n/** @satisfies {import('./$types').Actions} */\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = /** @type {string[]} */ (data.getAll('guess'));\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n};\n"
37
37
  },
38
38
  {
39
39
  "name": "src/routes/sverdle/+page.svelte",
40
- "contents": "<script>\n\timport { enhance } from '$app/forms';\n\timport { confetti } from '@neoconfetti/svelte';\n\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\t/**\n\t * @typedef {Object} Props\n\t * @property {import('./$types').PageData} data\n\t * @property {import('./$types').ActionData} form\n\t */\n\n\t/**\n\t * @type {Props}\n\t */\n\tlet { data, form = $bindable() } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t * @type {Record<string, 'exact' | 'close' | 'missing'>}\n\t\t */\n\t\tlet classnames = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t * @type {Record<string, string>}\n\t\t */\n\t\tlet description = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t * @param {MouseEvent} event\n\t */\n\tfunction update(event) {\n\t\tevent.preventDefault();\n\t\tconst key = /** @type {HTMLButtonElement} */ (event.target).getAttribute('data-key');\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t * @param {KeyboardEvent} event\n\t */\n\tfunction keydown(event) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href=\"/sverdle/how-to-play\">How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
40
+ "contents": "<script>\n\timport { enhance } from '$app/forms';\n\timport { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\t/**\n\t * @typedef {Object} Props\n\t * @property {import('./$types').PageData} data\n\t * @property {import('./$types').ActionData} form\n\t */\n\n\t/**\n\t * @type {Props}\n\t */\n\tlet { data, form = $bindable() } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t * @type {Record<string, 'exact' | 'close' | 'missing'>}\n\t\t */\n\t\tlet classnames = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t * @type {Record<string, string>}\n\t\t */\n\t\tlet description = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t * @param {MouseEvent} event\n\t */\n\tfunction update(event) {\n\t\tevent.preventDefault();\n\t\tconst key = /** @type {HTMLButtonElement} */ (event.target).getAttribute('data-key');\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t * @param {KeyboardEvent} event\n\t */\n\tfunction keydown(event) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href={resolve('/sverdle/how-to-play')}>How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
41
41
  },
42
42
  {
43
43
  "name": "src/routes/sverdle/game.js",
44
- "contents": "import { words, allowed } from './words.server';\n\nexport class Game {\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t * @param {string | undefined} serialized\n\t */\n\tconstructor(serialized = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = /** @type {string[]} */ ([]);\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t * @param {string[]} letters\n\t */\n\tenter(letters) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
44
+ "contents": "import { words, allowed } from './words.server.js';\n\nexport class Game {\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t * @param {string | undefined} serialized\n\t */\n\tconstructor(serialized = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = /** @type {string[]} */ ([]);\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t * @param {string[]} letters\n\t */\n\tenter(letters) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
45
45
  },
46
46
  {
47
47
  "name": "src/routes/sverdle/how-to-play/+page.svelte",
@@ -1,7 +1,7 @@
1
1
  [
2
2
  {
3
3
  "name": "src/routes/+layout.svelte",
4
- "contents": "<script>\n\timport Header from './Header.svelte';\n\timport '../app.css';\n\n\tlet { children } = $props();\n</script>\n\n<div class=\"app\">\n\t<Header />\n\n\t<main>\n\t\t{@render children()}\n\t</main>\n\n\t<footer>\n\t\t<p>\n\t\t\tvisit <a href=\"https://svelte.dev/docs/kit\">svelte.dev/docs/kit</a> to learn about SvelteKit\n\t\t</p>\n\t</footer>\n</div>\n\n<style>\n\t.app {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tmin-height: 100vh;\n\t}\n\n\tmain {\n\t\tflex: 1;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tpadding: 1rem;\n\t\twidth: 100%;\n\t\tmax-width: 64rem;\n\t\tmargin: 0 auto;\n\t\tbox-sizing: border-box;\n\t}\n\n\tfooter {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tpadding: 12px;\n\t}\n\n\tfooter a {\n\t\tfont-weight: bold;\n\t}\n\n\t@media (min-width: 480px) {\n\t\tfooter {\n\t\t\tpadding: 12px 0;\n\t\t}\n\t}\n</style>\n"
4
+ "contents": "<script>\n\timport Header from './Header.svelte';\n\timport './layout.css';\n\n\tlet { children } = $props();\n</script>\n\n<div class=\"app\">\n\t<Header />\n\n\t<main>\n\t\t{@render children()}\n\t</main>\n\n\t<footer>\n\t\t<p>\n\t\t\tvisit <a href=\"https://svelte.dev/docs/kit\">svelte.dev/docs/kit</a> to learn about SvelteKit\n\t\t</p>\n\t</footer>\n</div>\n\n<style>\n\t.app {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tmin-height: 100vh;\n\t}\n\n\tmain {\n\t\tflex: 1;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tpadding: 1rem;\n\t\twidth: 100%;\n\t\tmax-width: 64rem;\n\t\tmargin: 0 auto;\n\t\tbox-sizing: border-box;\n\t}\n\n\tfooter {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tpadding: 12px;\n\t}\n\n\tfooter a {\n\t\tfont-weight: bold;\n\t}\n\n\t@media (min-width: 480px) {\n\t\tfooter {\n\t\t\tpadding: 12px 0;\n\t\t}\n\t}\n</style>\n"
5
5
  },
6
6
  {
7
7
  "name": "src/routes/+page.svelte",
@@ -17,11 +17,11 @@
17
17
  },
18
18
  {
19
19
  "name": "src/routes/Header.svelte",
20
- "contents": "<script>\n\timport { page } from '$app/state';\n\timport logo from '$lib/images/svelte-logo.svg';\n\timport github from '$lib/images/github.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href=\"/\">Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href=\"/about\">About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href=\"/sverdle\">Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
20
+ "contents": "<script>\n\timport { resolve } from '$app/paths';\n\timport { page } from '$app/state';\n\timport logo from '$lib/images/svelte-logo.svg';\n\timport github from '$lib/images/github.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/')}>Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/about')}>About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/sverdle')}>Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
21
21
  },
22
22
  {
23
23
  "name": "src/routes/about/+page.svelte",
24
- "contents": "<svelte:head>\n\t<title>About</title>\n\t<meta name=\"description\" content=\"About this app\" />\n</svelte:head>\n\n<div class=\"text-column\">\n\t<h1>About this app</h1>\n\n\t<p>\n\t\tThis is a <a href=\"https://svelte.dev/docs/kit\">SvelteKit</a> app. You can make your own by typing\n\t\tthe following into your command line and following the prompts:\n\t</p>\n\n\t<pre>npx sv create</pre>\n\n\t<p>\n\t\tThe page you're looking at is purely static HTML, with no client-side interactivity needed.\n\t\tBecause of that, we don't need to load any JavaScript. Try viewing the page's source, or opening\n\t\tthe devtools network panel and reloading.\n\t</p>\n\n\t<p>\n\t\tThe <a href=\"/sverdle\">Sverdle</a> page illustrates SvelteKit's data loading and form handling. Try\n\t\tusing it with JavaScript disabled!\n\t</p>\n</div>\n"
24
+ "contents": "<script>\n\timport { resolve } from '$app/paths';\n</script>\n\n<svelte:head>\n\t<title>About</title>\n\t<meta name=\"description\" content=\"About this app\" />\n</svelte:head>\n\n<div class=\"text-column\">\n\t<h1>About this app</h1>\n\n\t<p>\n\t\tThis is a <a href=\"https://svelte.dev/docs/kit\">SvelteKit</a> app. You can make your own by typing\n\t\tthe following into your command line and following the prompts:\n\t</p>\n\n\t<pre>npx sv create</pre>\n\n\t<p>\n\t\tThe page you're looking at is purely static HTML, with no client-side interactivity needed.\n\t\tBecause of that, we don't need to load any JavaScript. Try viewing the page's source, or opening\n\t\tthe devtools network panel and reloading.\n\t</p>\n\n\t<p>\n\t\tThe <a href={resolve('/sverdle')}>Sverdle</a> page illustrates SvelteKit's data loading and form\n\t\thandling. Try using it with JavaScript disabled!\n\t</p>\n</div>\n"
25
25
  },
26
26
  {
27
27
  "name": "src/routes/about/+page.js",
@@ -29,15 +29,15 @@
29
29
  },
30
30
  {
31
31
  "name": "src/routes/sverdle/+page.server.js",
32
- "contents": "import { fail } from '@sveltejs/kit';\nimport { Game } from './game';\n\nexport const load = ({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n};\n\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = (data.getAll('guess'));\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n};\n"
32
+ "contents": "import { fail } from '@sveltejs/kit';\nimport { Game } from './game.js';\n\nexport const load = ({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n};\n\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = (data.getAll('guess'));\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n};\n"
33
33
  },
34
34
  {
35
35
  "name": "src/routes/sverdle/+page.svelte",
36
- "contents": "<script>\n\timport { enhance } from '$app/forms';\n\timport { confetti } from '@neoconfetti/svelte';\n\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\tlet { data, form = $bindable() } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t */\n\t\tlet classnames = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t */\n\t\tlet description = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t */\n\tfunction update(event) {\n\t\tevent.preventDefault();\n\t\tconst key = (event.target).getAttribute('data-key');\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t */\n\tfunction keydown(event) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href=\"/sverdle/how-to-play\">How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
36
+ "contents": "<script>\n\timport { enhance } from '$app/forms';\n\timport { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\tlet { data, form = $bindable() } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t */\n\t\tlet classnames = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t */\n\t\tlet description = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t */\n\tfunction update(event) {\n\t\tevent.preventDefault();\n\t\tconst key = (event.target).getAttribute('data-key');\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t */\n\tfunction keydown(event) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href={resolve('/sverdle/how-to-play')}>How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
37
37
  },
38
38
  {
39
39
  "name": "src/routes/sverdle/game.js",
40
- "contents": "import { words, allowed } from './words.server';\n\nexport class Game {\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t */\n\tconstructor(serialized = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = ([]);\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t */\n\tenter(letters) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
40
+ "contents": "import { words, allowed } from './words.server.js';\n\nexport class Game {\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t */\n\tconstructor(serialized = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = ([]);\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t */\n\tenter(letters) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
41
41
  },
42
42
  {
43
43
  "name": "src/routes/sverdle/how-to-play/+page.svelte",
@@ -5,7 +5,7 @@
5
5
  },
6
6
  {
7
7
  "name": "src/routes/+layout.svelte",
8
- "contents": "<script lang=\"ts\">\n\timport Header from './Header.svelte';\n\timport '../app.css';\n\n\tlet { children } = $props();\n</script>\n\n<div class=\"app\">\n\t<Header />\n\n\t<main>\n\t\t{@render children()}\n\t</main>\n\n\t<footer>\n\t\t<p>\n\t\t\tvisit <a href=\"https://svelte.dev/docs/kit\">svelte.dev/docs/kit</a> to learn about SvelteKit\n\t\t</p>\n\t</footer>\n</div>\n\n<style>\n\t.app {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tmin-height: 100vh;\n\t}\n\n\tmain {\n\t\tflex: 1;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tpadding: 1rem;\n\t\twidth: 100%;\n\t\tmax-width: 64rem;\n\t\tmargin: 0 auto;\n\t\tbox-sizing: border-box;\n\t}\n\n\tfooter {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tpadding: 12px;\n\t}\n\n\tfooter a {\n\t\tfont-weight: bold;\n\t}\n\n\t@media (min-width: 480px) {\n\t\tfooter {\n\t\t\tpadding: 12px 0;\n\t\t}\n\t}\n</style>\n"
8
+ "contents": "<script lang=\"ts\">\n\timport Header from './Header.svelte';\n\timport './layout.css';\n\n\tlet { children } = $props();\n</script>\n\n<div class=\"app\">\n\t<Header />\n\n\t<main>\n\t\t{@render children()}\n\t</main>\n\n\t<footer>\n\t\t<p>\n\t\t\tvisit <a href=\"https://svelte.dev/docs/kit\">svelte.dev/docs/kit</a> to learn about SvelteKit\n\t\t</p>\n\t</footer>\n</div>\n\n<style>\n\t.app {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tmin-height: 100vh;\n\t}\n\n\tmain {\n\t\tflex: 1;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tpadding: 1rem;\n\t\twidth: 100%;\n\t\tmax-width: 64rem;\n\t\tmargin: 0 auto;\n\t\tbox-sizing: border-box;\n\t}\n\n\tfooter {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tpadding: 12px;\n\t}\n\n\tfooter a {\n\t\tfont-weight: bold;\n\t}\n\n\t@media (min-width: 480px) {\n\t\tfooter {\n\t\t\tpadding: 12px 0;\n\t\t}\n\t}\n</style>\n"
9
9
  },
10
10
  {
11
11
  "name": "src/routes/+page.svelte",
@@ -21,11 +21,11 @@
21
21
  },
22
22
  {
23
23
  "name": "src/routes/Header.svelte",
24
- "contents": "<script lang=\"ts\">\n\timport { page } from '$app/state';\n\timport logo from '$lib/images/svelte-logo.svg';\n\timport github from '$lib/images/github.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href=\"/\">Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href=\"/about\">About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href=\"/sverdle\">Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
24
+ "contents": "<script lang=\"ts\">\n\timport { resolve } from '$app/paths';\n\timport { page } from '$app/state';\n\timport logo from '$lib/images/svelte-logo.svg';\n\timport github from '$lib/images/github.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/')}>Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/about')}>About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/sverdle')}>Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
25
25
  },
26
26
  {
27
27
  "name": "src/routes/about/+page.svelte",
28
- "contents": "<svelte:head>\n\t<title>About</title>\n\t<meta name=\"description\" content=\"About this app\" />\n</svelte:head>\n\n<div class=\"text-column\">\n\t<h1>About this app</h1>\n\n\t<p>\n\t\tThis is a <a href=\"https://svelte.dev/docs/kit\">SvelteKit</a> app. You can make your own by typing\n\t\tthe following into your command line and following the prompts:\n\t</p>\n\n\t<pre>npx sv create</pre>\n\n\t<p>\n\t\tThe page you're looking at is purely static HTML, with no client-side interactivity needed.\n\t\tBecause of that, we don't need to load any JavaScript. Try viewing the page's source, or opening\n\t\tthe devtools network panel and reloading.\n\t</p>\n\n\t<p>\n\t\tThe <a href=\"/sverdle\">Sverdle</a> page illustrates SvelteKit's data loading and form handling. Try\n\t\tusing it with JavaScript disabled!\n\t</p>\n</div>\n"
28
+ "contents": "<script lang=\"ts\">\n\timport { resolve } from '$app/paths';\n</script>\n\n<svelte:head>\n\t<title>About</title>\n\t<meta name=\"description\" content=\"About this app\" />\n</svelte:head>\n\n<div class=\"text-column\">\n\t<h1>About this app</h1>\n\n\t<p>\n\t\tThis is a <a href=\"https://svelte.dev/docs/kit\">SvelteKit</a> app. You can make your own by typing\n\t\tthe following into your command line and following the prompts:\n\t</p>\n\n\t<pre>npx sv create</pre>\n\n\t<p>\n\t\tThe page you're looking at is purely static HTML, with no client-side interactivity needed.\n\t\tBecause of that, we don't need to load any JavaScript. Try viewing the page's source, or opening\n\t\tthe devtools network panel and reloading.\n\t</p>\n\n\t<p>\n\t\tThe <a href={resolve('/sverdle')}>Sverdle</a> page illustrates SvelteKit's data loading and form\n\t\thandling. Try using it with JavaScript disabled!\n\t</p>\n</div>\n"
29
29
  },
30
30
  {
31
31
  "name": "src/routes/about/+page.ts",
@@ -33,15 +33,15 @@
33
33
  },
34
34
  {
35
35
  "name": "src/routes/sverdle/+page.server.ts",
36
- "contents": "import { fail } from '@sveltejs/kit';\nimport { Game } from './game';\nimport type { PageServerLoad, Actions } from './$types';\n\nexport const load = (({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n}) satisfies PageServerLoad;\n\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = data.getAll('guess') as string[];\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n} satisfies Actions;\n"
36
+ "contents": "import { fail } from '@sveltejs/kit';\nimport { Game } from './game.ts';\nimport type { PageServerLoad, Actions } from './$types';\n\nexport const load = (({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n}) satisfies PageServerLoad;\n\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = data.getAll('guess') as string[];\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n} satisfies Actions;\n"
37
37
  },
38
38
  {
39
39
  "name": "src/routes/sverdle/+page.svelte",
40
- "contents": "<script lang=\"ts\">\n\timport { enhance } from '$app/forms';\n\timport { confetti } from '@neoconfetti/svelte';\n\timport type { ActionData, PageData } from './$types';\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t\tform: ActionData;\n\t}\n\tlet { data, form = $bindable() }: Props = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t */\n\t\tlet classnames: Record<string, 'exact' | 'close' | 'missing'> = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t */\n\t\tlet description: Record<string, string> = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t */\n\tfunction update(event: MouseEvent) {\n\t\tevent.preventDefault();\n\t\tconst key = (event.target as HTMLButtonElement).getAttribute(\n\t\t\t'data-key'\n\t\t);\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t */\n\tfunction keydown(event: KeyboardEvent) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href=\"/sverdle/how-to-play\">How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
40
+ "contents": "<script lang=\"ts\">\n\timport { enhance } from '$app/forms';\n\timport { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\timport type { ActionData, PageData } from './$types';\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t\tform: ActionData;\n\t}\n\tlet { data, form = $bindable() }: Props = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t */\n\t\tlet classnames: Record<string, 'exact' | 'close' | 'missing'> = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t */\n\t\tlet description: Record<string, string> = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t */\n\tfunction update(event: MouseEvent) {\n\t\tevent.preventDefault();\n\t\tconst key = (event.target as HTMLButtonElement).getAttribute(\n\t\t\t'data-key'\n\t\t);\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t */\n\tfunction keydown(event: KeyboardEvent) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href={resolve('/sverdle/how-to-play')}>How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
41
41
  },
42
42
  {
43
43
  "name": "src/routes/sverdle/game.ts",
44
- "contents": "import { words, allowed } from './words.server';\n\nexport class Game {\n\tindex: number;\n\tguesses: string[];\n\tanswers: string[];\n\tanswer: string;\n\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t */\n\tconstructor(serialized: string | undefined = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = [];\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t */\n\tenter(letters: string[]) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
44
+ "contents": "import { words, allowed } from './words.server.ts';\n\nexport class Game {\n\tindex: number;\n\tguesses: string[];\n\tanswers: string[];\n\tanswer: string;\n\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t */\n\tconstructor(serialized: string | undefined = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = [];\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t */\n\tenter(letters: string[]) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
45
45
  },
46
46
  {
47
47
  "name": "src/routes/sverdle/how-to-play/+page.svelte",
@@ -7,6 +7,6 @@
7
7
  %sveltekit.head%
8
8
  </head>
9
9
  <body data-sveltekit-preload-data="hover">
10
- <div>%sveltekit.body%</div>
10
+ <div style="display: contents">%sveltekit.body%</div>
11
11
  </body>
12
12
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sv",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
4
4
  "type": "module",
5
5
  "description": "A CLI for creating and updating SvelteKit projects",
6
6
  "license": "MIT",
@@ -37,8 +37,8 @@
37
37
  "tinyexec": "^0.3.2",
38
38
  "valibot": "^0.41.0",
39
39
  "@sveltejs/addons": "0.0.0",
40
- "@sveltejs/cli-core": "0.0.0",
41
- "@sveltejs/create": "0.0.0"
40
+ "@sveltejs/create": "0.0.0",
41
+ "@sveltejs/cli-core": "0.0.0"
42
42
  },
43
43
  "keywords": [
44
44
  "create",