sv 1.0.1 → 1.1.1

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.
@@ -1,22 +1,26 @@
1
1
  import { AgentName } from "@sveltejs/sv-utils";
2
- //#region src/addons/index.d.ts
3
- type OfficialAddons = {
4
- prettier: Addon<any>;
5
- eslint: Addon<any>;
6
- vitest: Addon<any>;
7
- playwright: Addon<any>;
8
- tailwindcss: Addon<any>;
9
- enhancedImg: Addon<any>;
10
- sveltekitAdapter: Addon<any>;
11
- drizzle: Addon<any>;
12
- betterAuth: Addon<any>;
13
- mdsvex: Addon<any>;
14
- paraglide: Addon<any>;
15
- storybook: Addon<any>;
16
- aiTools: Addon<any>;
17
- experimental: Addon<any>;
2
+ //#region src/addons/ids.d.ts
3
+ /**
4
+ * Official add-on ids, declared without importing the add-ons themselves so that
5
+ * add-ons can reference each other in `dependsOn`/`runsAfter` without import cycles.
6
+ */
7
+ declare const ADDON_IDS: {
8
+ readonly prettier: "prettier";
9
+ readonly eslint: "eslint";
10
+ readonly vitest: "vitest";
11
+ readonly playwright: "playwright";
12
+ readonly tailwindcss: "tailwindcss";
13
+ readonly enhancedImg: "enhanced-img";
14
+ readonly sveltekitAdapter: "sveltekit-adapter";
15
+ readonly drizzle: "drizzle";
16
+ readonly betterAuth: "better-auth";
17
+ readonly mdsvex: "mdsvex";
18
+ readonly paraglide: "paraglide";
19
+ readonly storybook: "storybook";
20
+ readonly aiTools: "ai-tools";
21
+ readonly experimental: "experimental";
18
22
  };
19
- declare const officialAddons: OfficialAddons;
23
+ type OfficialAddonId = (typeof ADDON_IDS)[keyof typeof ADDON_IDS];
20
24
  //#endregion
21
25
  //#region src/core/options.d.ts
22
26
  type BooleanQuestion = {
@@ -65,6 +69,12 @@ type BaseQuestion<Args extends OptionDefinition> = {
65
69
  };
66
70
  type Question<Args extends OptionDefinition = OptionDefinition> = BaseQuestion<Args> & (BooleanQuestion | StringQuestion | NumberQuestion | SelectQuestion<any> | MultiSelectQuestion<any>);
67
71
  type OptionDefinition = Record<string, Question<any>>;
72
+ type QuestionFor<Value> = BaseQuestion<any> & ([Value] extends [boolean] ? BooleanQuestion : [Value] extends [number] ? NumberQuestion : [Value] extends [Array<infer Item>] ? MultiSelectQuestion<Item> : [string] extends [Value] ? StringQuestion : SelectQuestion<Value>);
73
+ /** The questions of an add-on, described by the values they produce. See `defineAddonOptions<Values>()`. */
74
+ type AddonOptions<Values extends Record<string, unknown>> = { [K in keyof Values]: QuestionFor<Values[K]>; } extends (infer Questions extends OptionDefinition) ? Questions : never;
75
+ type SameType<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
76
+ /** Keys whose question is missing, extra, or produces a different value than declared. */
77
+ type MismatchedOptions<Args extends OptionDefinition, Values> = { [K in keyof Args | keyof Values]: K extends keyof Args ? K extends keyof Values ? SameType<OptionValues<Args>[K], Values[K]> extends true ? never : K : K : K; }[keyof Args | keyof Values];
68
78
  type OptionValues<Args extends OptionDefinition> = { [K in keyof Args]: Args[K] extends StringQuestion ? string : Args[K] extends BooleanQuestion ? boolean : Args[K] extends NumberQuestion ? number : Args[K] extends SelectQuestion<infer Value> ? Value : Args[K] extends MultiSelectQuestion<infer Value> ? Value[] : Args[K] extends Question<any> ? unknown : "ERROR: The value for this type is invalid. Ensure that the `default` value exists in `options`."; };
69
79
  //#endregion
70
80
  //#region src/core/workspace.d.ts
@@ -151,6 +161,7 @@ type SvApi = {
151
161
  where?: (content: string) => boolean;
152
162
  }, edit: FileEditMultiple) => void;
153
163
  };
164
+ type AddonId = OfficialAddonId | (string & {});
154
165
  type Addon<Args extends OptionDefinition, Id extends string = string, Setup extends Record<string, unknown> = Record<string, unknown>> = {
155
166
  id: Id;
156
167
  alias?: string;
@@ -163,16 +174,16 @@ type Addon<Args extends OptionDefinition, Id extends string = string, Setup exte
163
174
  options: Args;
164
175
  /** Setup the addon. Will be called before the addon is run. */
165
176
  setup?: (workspace: Workspace & {
166
- /** On what official addons does this addon depend on? */
167
- dependsOn: (name: keyof typeof officialAddons) => void;
177
+ /** Required add-ons. Implies `runsAfter`. Official ids are installed automatically, others must be part of the same run. */
178
+ dependsOn: (id: AddonId) => void;
168
179
  /**
169
180
  * Why is this addon not supported?
170
181
  * @example
171
182
  * if (!isKit) unsupported('Requires SvelteKit');
172
183
  */
173
184
  unsupported: (reason: string) => void;
174
- /** On what official addons does this addon run after? */
175
- runsAfter: (name: keyof typeof officialAddons) => void;
185
+ /** Ordering only: run after these add-ons when they are part of the same run. */
186
+ runsAfter: (id: AddonId) => void;
176
187
  /** Dynamically add an option to be prompted to the user */
177
188
  addOption: <K extends Extract<keyof Setup, string>>(key: K, question: SetupOptions<Setup>[K]) => void;
178
189
  }) => MaybePromise<void>;
@@ -281,7 +292,7 @@ type SetupResult = {
281
292
  type AddonDefinition<Id extends string = string> = Addon<Record<string, Question<any>>, Id>;
282
293
  type MaybePromise<T> = Promise<T> | T;
283
294
  type Prettify<T> = { [K in keyof T]: T[K]; } & unknown;
284
- type OptionBuilder<T extends OptionDefinition> = {
295
+ type OptionBuilder<T extends OptionDefinition, Values extends Record<string, unknown> = never> = {
285
296
  /**
286
297
  * This type is a bit complex, but in usage, it's quite simple!
287
298
  *
@@ -296,9 +307,9 @@ type OptionBuilder<T extends OptionDefinition> = {
296
307
  * })
297
308
  * ```
298
309
  */
299
- add<K extends string, const Q extends Question<T & Record<K, Q>>>(key: K, question: Q): OptionBuilder<T & Record<K, Q>>;
310
+ add<K extends string, const Q extends Question<T & Record<K, Q>>>(key: K, question: Q): OptionBuilder<T & Record<K, Q>, Values>;
300
311
  /** Finalize all options of your `add-on`. */
301
- build(): Prettify<T>;
312
+ build(...check: [Values] extends [never] ? [] : [MismatchedOptions<T, Values>] extends [never] ? [] : [mismatchedOptions: MismatchedOptions<T, Values>]): [Values] extends [never] ? Prettify<T> : AddonOptions<Values>;
302
313
  };
303
314
  /**
304
315
  * Options for an addon.
@@ -319,8 +330,15 @@ type OptionBuilder<T extends OptionDefinition> = {
319
330
  * ```sh
320
331
  * npx sv add <addon>=<option1>:<value1>+<option2>:<value2>
321
332
  * ```
333
+ *
334
+ * Pass the values your options produce to get a small, stable type to export, checked both ways
335
+ * against the questions:
336
+ * ```ts
337
+ * export type MyAddonOptions = { demo: boolean };
338
+ * const options = defineAddonOptions<MyAddonOptions>().add('demo', { ... }).build();
339
+ * ```
322
340
  */
323
- declare function defineAddonOptions(): OptionBuilder<{}>;
341
+ declare function defineAddonOptions<Values extends Record<string, unknown> = never>(): OptionBuilder<{}, Values>;
324
342
  //#endregion
325
343
  //#region src/core/engine.d.ts
326
344
  type InstallOptions<Addons extends AddonMap> = {
@@ -347,4 +365,4 @@ declare function applyAddons({ loadedAddons, workspace, setupResults, options }:
347
365
  installNeeded: boolean;
348
366
  }>;
349
367
  //#endregion
350
- export { StringQuestion as A, BooleanQuestion as C, OptionValues as D, OptionDefinition as E, Question as O, BaseQuestion as S, NumberQuestion as T, SvApi as _, Addon as a, Workspace as b, AddonReference as c, ConditionDefinition as d, ConfiguredAddon as f, SetupResult as g, PreparedAddon as h, add as i, officialAddons as j, SelectQuestion as k, AddonResult as l, OptionBuilder as m, InstallOptions as n, AddonDefinition as o, LoadedAddon as p, OptionMap as r, AddonInput as s, AddonMap as t, AddonSource as u, defineAddon as v, MultiSelectQuestion as w, WorkspaceOptions as x, defineAddonOptions as y };
368
+ export { SelectQuestion as A, BaseQuestion as C, OptionDefinition as D, NumberQuestion as E, ADDON_IDS as M, OptionValues as O, AddonOptions as S, MultiSelectQuestion as T, SvApi as _, Addon as a, Workspace as b, AddonReference as c, ConditionDefinition as d, ConfiguredAddon as f, SetupResult as g, PreparedAddon as h, add as i, StringQuestion as j, Question as k, AddonResult as l, OptionBuilder as m, InstallOptions as n, AddonDefinition as o, LoadedAddon as p, OptionMap as r, AddonInput as s, AddonMap as t, AddonSource as u, defineAddon as v, BooleanQuestion as w, WorkspaceOptions as x, defineAddonOptions as y };
@@ -5020,7 +5020,7 @@ function safeParse(schema, input, config$1) {
5020
5020
  //#endregion
5021
5021
  //#region package.json
5022
5022
  var name = "sv";
5023
- var version = "1.0.1";
5023
+ var version = "1.1.1";
5024
5024
  //#endregion
5025
5025
  //#region src/core/errors.ts
5026
5026
  var UnsupportedError = class extends Error {
@@ -5890,6 +5890,13 @@ function createLoadedAddon(addon) {
5890
5890
  * ```sh
5891
5891
  * npx sv add <addon>=<option1>:<value1>+<option2>:<value2>
5892
5892
  * ```
5893
+ *
5894
+ * Pass the values your options produce to get a small, stable type to export, checked both ways
5895
+ * against the questions:
5896
+ * ```ts
5897
+ * export type MyAddonOptions = { demo: boolean };
5898
+ * const options = defineAddonOptions<MyAddonOptions>().add('demo', { ... }).build();
5899
+ * ```
5893
5900
  */
5894
5901
  function defineAddonOptions() {
5895
5902
  return createOptionBuilder({});
@@ -6089,20 +6096,14 @@ async function createWorkspace({ cwd, packageManager, override }) {
6089
6096
  let dependencies = {};
6090
6097
  if (override?.dependencies) dependencies = override.dependencies;
6091
6098
  else {
6092
- let directory = resolvedCwd;
6093
- const workspaceRoot = findWorkspaceRoot(directory);
6094
- const { root } = path.parse(directory);
6095
- while (directory && directory.length >= workspaceRoot.length) {
6096
- if (fs.existsSync(path.join(directory, filePaths.packageJson))) {
6097
- const { data: packageJson } = loadPackageJson(directory);
6098
- dependencies = {
6099
- ...packageJson.devDependencies,
6100
- ...packageJson.dependencies,
6101
- ...dependencies
6102
- };
6103
- }
6104
- if (root === directory) break;
6105
- directory = path.dirname(directory);
6099
+ const workspaceRoot = findWorkspaceRoot(resolvedCwd);
6100
+ for (const directory of up$1(resolvedCwd, { last: workspaceRoot })) if (fs.existsSync(path.join(directory, filePaths.packageJson))) {
6101
+ const { data: packageJson } = loadPackageJson(directory);
6102
+ dependencies = {
6103
+ ...packageJson.devDependencies,
6104
+ ...packageJson.dependencies,
6105
+ ...dependencies
6106
+ };
6106
6107
  }
6107
6108
  }
6108
6109
  for (const [key, value] of Object.entries(dependencies)) try {
@@ -6147,9 +6148,7 @@ async function createWorkspace({ cwd, packageManager, override }) {
6147
6148
  };
6148
6149
  }
6149
6150
  function findWorkspaceRoot(cwd) {
6150
- const { root } = path.parse(cwd);
6151
- let directory = cwd;
6152
- while (directory && directory !== root) {
6151
+ for (const directory of up$1(cwd)) {
6153
6152
  if (fs.existsSync(path.join(directory, filePaths.packageJson))) {
6154
6153
  if (fs.existsSync(path.join(directory, "pnpm-workspace.yaml"))) return directory;
6155
6154
  const { data } = loadPackageJson(directory);
@@ -6157,7 +6156,6 @@ function findWorkspaceRoot(cwd) {
6157
6156
  }
6158
6157
  const parent = path.dirname(directory);
6159
6158
  if (directory.includes(".test-output") && !parent.includes(".test-output")) break;
6160
- directory = parent;
6161
6159
  }
6162
6160
  return cwd;
6163
6161
  }
@@ -6475,4 +6473,4 @@ function orderAddons(addons, setupResults) {
6475
6473
  return ordered;
6476
6474
  }
6477
6475
  //#endregion
6478
- export { pipe as $, filePaths as A, updateAgent as B, templates as C, buildAndLogArgs as D, PackageJSONSchema as E, isNodeError as F, version as G, updateReadme as H, normalizePosix as I, literal as J, array as K, parseAddonOptions as L, getHelpSections as M, getPadding as N, cliOptions as O, helpConfig as P, picklist as Q, runAndValidateVerifications as R, create as S, getSharedFiles as T, UnsupportedError as U, updateLibraryBuild as V, name as W, optional as X, nonEmpty as Y, parse$1 as Z, defineAddon as _, we as _t, setupAddons as a, union as at, exec as b, __require as bt, AGENT_NAMES as c, Option as ct, installDependencies as d, R as dt, record as et, installOption as f, Re as ft, createLoadedAddon as g, vt as gt, from as h, pe as ht, prepareSvApi as i, transform as it, forwardExitCode as j, errorAndExit as k, addAllowBuildsIfPnpm as l, Ce as lt, up as m, le as mt, applyAddons as n, strictObject as nt, createWorkspace as o, program as ot, packageManagerPrompt as p, ge as pt, boolean as q, orderAddons as r, string as rt, findWorkspaceRoot as s, Command as st, add as t, safeParse as tt, detectPackageManager as u, Ee as ut, defineAddonOptions as v, R$1 as vt, dist as w, execSync as x, __toESM as xt, getErrorHint as y, __commonJSMin as yt, runCommand as z };
6476
+ export { parse$1 as $, cliOptions as A, runAndValidateVerifications as B, execSync as C, __toESM as Ct, getSharedFiles as D, dist as E, getPadding as F, UnsupportedError as G, updateAgent as H, helpConfig as I, array as J, name as K, isNodeError as L, filePaths as M, forwardExitCode as N, PackageJSONSchema as O, getHelpSections as P, optional as Q, normalizePosix as R, exec as S, __require as St, templates as T, updateLibraryBuild as U, runCommand as V, updateReadme as W, literal as X, boolean as Y, nonEmpty as Z, createLoadedAddon as _, pe as _t, setupAddons as a, string as at, getErrorHint as b, R$1 as bt, AGENT_NAMES as c, program as ct, installDependencies as d, Ce as dt, picklist as et, installOption as f, Ee as ft, from as g, le as gt, up$1 as h, ge as ht, prepareSvApi as i, strictObject as it, errorAndExit as j, buildAndLogArgs as k, addAllowBuildsIfPnpm as l, Command as lt, up as m, Re as mt, applyAddons as n, record as nt, createWorkspace as o, transform as ot, packageManagerPrompt as p, R as pt, version as q, orderAddons as r, safeParse as rt, findWorkspaceRoot as s, union as st, add as t, pipe as tt, detectPackageManager as u, Option as ut, defineAddon as v, vt, create as w, NonZeroExitError as x, __commonJSMin as xt, defineAddonOptions as y, we as yt, parseAddonOptions as z };
@@ -1,4 +1,4 @@
1
- import { A as StringQuestion, C as BooleanQuestion, D as OptionValues, E as OptionDefinition, O as Question, S as BaseQuestion, T as NumberQuestion, _ as SvApi, a as Addon, b as Workspace, c as AddonReference, d as ConditionDefinition, f as ConfiguredAddon, g as SetupResult, h as PreparedAddon, i as add, j as officialAddons, k as SelectQuestion, l as AddonResult, m as OptionBuilder, n as InstallOptions, o as AddonDefinition, p as LoadedAddon, r as OptionMap, s as AddonInput, t as AddonMap, u as AddonSource, v as defineAddon, w as MultiSelectQuestion, x as WorkspaceOptions, y as defineAddonOptions } from "../engine-D2x5kvrf.mjs";
1
+ import { A as SelectQuestion, C as BaseQuestion, D as OptionDefinition, E as NumberQuestion, M as ADDON_IDS, O as OptionValues, S as AddonOptions, T as MultiSelectQuestion, _ as SvApi, a as Addon, b as Workspace, c as AddonReference, d as ConditionDefinition, f as ConfiguredAddon, g as SetupResult, h as PreparedAddon, i as add, j as StringQuestion, k as Question, l as AddonResult, m as OptionBuilder, n as InstallOptions, o as AddonDefinition, p as LoadedAddon, r as OptionMap, s as AddonInput, t as AddonMap, u as AddonSource, v as defineAddon, w as BooleanQuestion, x as WorkspaceOptions, y as defineAddonOptions } from "../engine-BLZdMrao.mjs";
2
2
  //#region src/create/index.d.ts
3
3
  type TemplateType = (typeof templateTypes)[number];
4
4
  type LanguageType = (typeof languageTypes)[number];
@@ -12,6 +12,83 @@ type Options = {
12
12
  };
13
13
  export declare function create({ cwd, ...options }: Options): void;
14
14
  //#endregion
15
+ //#region src/addons/ai-tools.d.ts
16
+ type AiToolsOptions = {
17
+ ide: string[];
18
+ delivery: "plugin" | "tools";
19
+ tools: string[];
20
+ mcpSetup: "local" | "remote";
21
+ };
22
+ //#endregion
23
+ //#region src/addons/better-auth.d.ts
24
+ type BetterAuthOptions = {
25
+ demo: Array<"password" | "github">;
26
+ };
27
+ //#endregion
28
+ //#region src/addons/drizzle.d.ts
29
+ type Database = "mysql" | "postgresql" | "sqlite" | "d1";
30
+ type DrizzleOptions = {
31
+ database: Database;
32
+ postgresql: "postgres.js" | "neon";
33
+ mysql: "mysql2" | "planetscale";
34
+ sqlite: "node-sqlite" | "better-sqlite3" | "libsql" | "turso";
35
+ docker: boolean;
36
+ };
37
+ //#endregion
38
+ //#region src/addons/experimental.d.ts
39
+ type ExperimentalOptions = {
40
+ features: string[];
41
+ };
42
+ //#endregion
43
+ //#region src/addons/paraglide.d.ts
44
+ type ParaglideOptions = {
45
+ languageTags: string;
46
+ demo: boolean;
47
+ };
48
+ //#endregion
49
+ //#region src/addons/playwright.d.ts
50
+ type PlaywrightOptions = {
51
+ demo: boolean;
52
+ };
53
+ //#endregion
54
+ //#region src/addons/sveltekit-adapter.d.ts
55
+ type SveltekitAdapterOptions = {
56
+ adapter: "auto" | "node" | "static" | "vercel" | "cloudflare" | "netlify";
57
+ cfTarget: "workers" | "pages";
58
+ };
59
+ //#endregion
60
+ //#region src/addons/tailwindcss.d.ts
61
+ type TailwindcssOptions = {
62
+ plugins: Array<"typography" | "forms">;
63
+ };
64
+ //#endregion
65
+ //#region src/addons/vitest-addon.d.ts
66
+ type VitestOptions = {
67
+ usages: Array<"unit" | "component">;
68
+ };
69
+ //#endregion
70
+ //#region src/addons/index.d.ts
71
+ type NoOptions = {};
72
+ type OfficialAddons = {
73
+ [ADDON_IDS.prettier]: Addon<NoOptions, typeof ADDON_IDS.prettier>;
74
+ [ADDON_IDS.eslint]: Addon<NoOptions, typeof ADDON_IDS.eslint>;
75
+ [ADDON_IDS.vitest]: Addon<AddonOptions<VitestOptions>, typeof ADDON_IDS.vitest>;
76
+ [ADDON_IDS.playwright]: Addon<AddonOptions<PlaywrightOptions>, typeof ADDON_IDS.playwright>;
77
+ [ADDON_IDS.tailwindcss]: Addon<AddonOptions<TailwindcssOptions>, typeof ADDON_IDS.tailwindcss>;
78
+ [ADDON_IDS.enhancedImg]: Addon<NoOptions, typeof ADDON_IDS.enhancedImg>;
79
+ [ADDON_IDS.sveltekitAdapter]: Addon<AddonOptions<SveltekitAdapterOptions>, typeof ADDON_IDS.sveltekitAdapter>;
80
+ [ADDON_IDS.drizzle]: Addon<AddonOptions<DrizzleOptions>, typeof ADDON_IDS.drizzle>;
81
+ [ADDON_IDS.betterAuth]: Addon<AddonOptions<BetterAuthOptions>, typeof ADDON_IDS.betterAuth>;
82
+ [ADDON_IDS.mdsvex]: Addon<NoOptions, typeof ADDON_IDS.mdsvex>;
83
+ [ADDON_IDS.paraglide]: Addon<AddonOptions<ParaglideOptions>, typeof ADDON_IDS.paraglide>;
84
+ [ADDON_IDS.storybook]: Addon<NoOptions, typeof ADDON_IDS.storybook>;
85
+ [ADDON_IDS.aiTools]: Addon<AddonOptions<AiToolsOptions>, typeof ADDON_IDS.aiTools>;
86
+ [ADDON_IDS.experimental]: Addon<AddonOptions<ExperimentalOptions>, typeof ADDON_IDS.experimental>;
87
+ };
88
+ /** What `add()` accepts as `options` for official add-ons, keyed by add-on id. */
89
+ type OfficialAddonOptions = OptionMap<OfficialAddons>;
90
+ export declare const officialAddons: OfficialAddons;
91
+ //#endregion
15
92
  //#region src/core/processors.d.ts
16
93
  type FileEditor = Workspace & {
17
94
  content: string;
@@ -22,4 +99,4 @@ type FileType = {
22
99
  content: (editor: FileEditor) => string;
23
100
  };
24
101
  //#endregion
25
- export { type Addon, type AddonDefinition, type AddonInput, type AddonMap, type AddonReference, type AddonResult, type AddonSource, type BaseQuestion, type BooleanQuestion, type ConfiguredAddon, type FileEditor, type FileType, type InstallOptions, type LanguageType, type LoadedAddon, type MultiSelectQuestion, type NumberQuestion, type OptionBuilder, type OptionDefinition, type OptionMap, type OptionValues, type PreparedAddon, type Question, type SelectQuestion, type SetupResult, type StringQuestion, type SvApi, type TemplateType, type Workspace, type WorkspaceOptions, add, defineAddon, defineAddonOptions, officialAddons };
102
+ export { type Addon, type AddonDefinition, type AddonInput, type AddonMap, type AddonOptions, type AddonReference, type AddonResult, type AddonSource, type BaseQuestion, type BooleanQuestion, type ConfiguredAddon, type FileEditor, type FileType, type InstallOptions, type LanguageType, type LoadedAddon, type MultiSelectQuestion, type NumberQuestion, type OfficialAddonOptions, type OfficialAddons, type OptionBuilder, type OptionDefinition, type OptionMap, type OptionValues, type PreparedAddon, type Question, type SelectQuestion, type SetupResult, type StringQuestion, type SvApi, type TemplateType, type Workspace, type WorkspaceOptions, add, defineAddon, defineAddonOptions };
@@ -1,3 +1,3 @@
1
- import { S as create, _ as defineAddon, t as add, v as defineAddonOptions } from "../engine-LhRAh1ZA.mjs";
2
- import { n as officialAddons } from "../addons-BvT-iII-.mjs";
1
+ import { t as add, v as defineAddon, w as create, y as defineAddonOptions } from "../engine-CpcTzL2R.mjs";
2
+ import { n as officialAddons } from "../addons-DaktRhvH.mjs";
3
3
  export { add, create, defineAddon, defineAddonOptions, officialAddons };
@@ -1,4 +1,4 @@
1
- import { r as OptionMap, t as AddonMap } from "../engine-D2x5kvrf.mjs";
1
+ import { r as OptionMap, t as AddonMap } from "../engine-BLZdMrao.mjs";
2
2
  import { Page } from "@playwright/test";
3
3
  import { TestProject } from "vitest/node";
4
4
  //#region src/testing.d.ts
@@ -1,4 +1,4 @@
1
- import { S as create, b as exec, bt as __require, l as addAllowBuildsIfPnpm, t as add, x as execSync, xt as __toESM, yt as __commonJSMin } from "../engine-LhRAh1ZA.mjs";
1
+ import { C as execSync, Ct as __toESM, S as exec, St as __require, l as addAllowBuildsIfPnpm, t as add, w as create, xt as __commonJSMin } from "../engine-CpcTzL2R.mjs";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import V from "node:process";
@@ -22,11 +22,11 @@
22
22
  "access": "public"
23
23
  },
24
24
  "peerDependencies": {
25
- "sv": "^1.0.1"
25
+ "sv": "^1.1.1"
26
26
  },
27
27
  "devDependencies": {
28
- "sv": "^1.0.1",
29
- "@sveltejs/sv-utils": "^1.0.0",
28
+ "sv": "^1.1.1",
29
+ "@sveltejs/sv-utils": "^1.0.2",
30
30
  "@playwright/test": "^1.60.0",
31
31
  "@types/node": "^25.9.1",
32
32
  "tsdown": "^0.22.1",
@@ -33,11 +33,11 @@
33
33
  },
34
34
  {
35
35
  "name": "src/routes/sverdle/+page.server.js",
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"
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: ({ 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 { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\t/**\n\t * @type {import('./$types').PageProps}\n\t */\n\tlet { data } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\tlet shake = $state(false);\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\tshake = 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 ({ result, update }) => {\n\t\t\tshake = result.type === 'failure';\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:shake onanimationend={() => (shake = false)}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{const current = $derived(row === i)}\n\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 = $derived(current ? currentGuess : data.guesses[row])}\n\t\t\t\t\t{const answer = $derived(data.answers[row]?.[column])}\n\t\t\t\t\t{const value = $derived(guess?.[column] ?? '')}\n\t\t\t\t\t{const selected = $derived(current && column === guess.length)}\n\t\t\t\t\t{const exact = $derived(answer === 'x')}\n\t\t\t\t\t{const close = $derived(answer === 'c')}\n\t\t\t\t\t{const missing = $derived(answer === '_')}\n\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.shake .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\timport { prefersReducedMotion } from 'svelte/motion';\n\n\t/**\n\t * @type {import('./$types').PageProps}\n\t */\n\tlet { data } = $props();\n\n\tlet shake = $state(false);\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\tshake = 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 ({ result, update }) => {\n\t\t\tshake = result.type === 'failure';\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:shake onanimationend={() => (shake = false)}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{const current = $derived(row === i)}\n\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 = $derived(current ? currentGuess : data.guesses[row])}\n\t\t\t\t\t{const answer = $derived(data.answers[row]?.[column])}\n\t\t\t\t\t{const value = $derived(guess?.[column] ?? '')}\n\t\t\t\t\t{const selected = $derived(current && column === guess.length)}\n\t\t\t\t\t{const exact = $derived(answer === 'x')}\n\t\t\t\t\t{const close = $derived(answer === 'c')}\n\t\t\t\t\t{const missing = $derived(answer === '_')}\n\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: prefersReducedMotion.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.shake .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",
@@ -29,11 +29,11 @@
29
29
  },
30
30
  {
31
31
  "name": "src/routes/sverdle/+page.server.js",
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"
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: ({ 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 { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\tlet { data } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\tlet shake = $state(false);\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\tshake = 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 ({ result, update }) => {\n\t\t\tshake = result.type === 'failure';\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:shake onanimationend={() => (shake = false)}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{const current = $derived(row === i)}\n\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 = $derived(current ? currentGuess : data.guesses[row])}\n\t\t\t\t\t{const answer = $derived(data.answers[row]?.[column])}\n\t\t\t\t\t{const value = $derived(guess?.[column] ?? '')}\n\t\t\t\t\t{const selected = $derived(current && column === guess.length)}\n\t\t\t\t\t{const exact = $derived(answer === 'x')}\n\t\t\t\t\t{const close = $derived(answer === 'c')}\n\t\t\t\t\t{const missing = $derived(answer === '_')}\n\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.shake .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\timport { prefersReducedMotion } from 'svelte/motion';\n\n\tlet { data } = $props();\n\n\tlet shake = $state(false);\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\tshake = 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 ({ result, update }) => {\n\t\t\tshake = result.type === 'failure';\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:shake onanimationend={() => (shake = false)}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{const current = $derived(row === i)}\n\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 = $derived(current ? currentGuess : data.guesses[row])}\n\t\t\t\t\t{const answer = $derived(data.answers[row]?.[column])}\n\t\t\t\t\t{const value = $derived(guess?.[column] ?? '')}\n\t\t\t\t\t{const selected = $derived(current && column === guess.length)}\n\t\t\t\t\t{const exact = $derived(answer === 'x')}\n\t\t\t\t\t{const close = $derived(answer === 'c')}\n\t\t\t\t\t{const missing = $derived(answer === '_')}\n\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: prefersReducedMotion.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.shake .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",
@@ -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 './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"
8
+ "contents": "<script lang=\"ts\">\n\timport type { LayoutProps } from './$types';\n\timport Header from './Header.svelte';\n\timport './layout.css';\n\n\tlet { children }: LayoutProps = $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",
@@ -33,11 +33,11 @@
33
33
  },
34
34
  {
35
35
  "name": "src/routes/sverdle/+page.server.ts",
36
- "contents": "import { fail } from '@sveltejs/kit';\nimport type { Actions, PageServerLoad } from './$types';\nimport { Game } from './game.ts';\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 type { Actions, PageServerLoad } from './$types';\nimport { Game } from './game.ts';\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') as string) ?? '';\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: ({ 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 { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\timport { MediaQuery } from 'svelte/reactivity';\n\timport type { PageProps } from './$types';\n\n\tlet { data }: PageProps = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\tlet shake = $state(false);\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\tshake = 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 ({ result, update }) => {\n\t\t\tshake = result.type === 'failure';\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:shake onanimationend={() => (shake = false)}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{const current = $derived(row === i)}\n\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 = $derived(current ? currentGuess : data.guesses[row])}\n\t\t\t\t\t{const answer = $derived(data.answers[row]?.[column])}\n\t\t\t\t\t{const value = $derived(guess?.[column] ?? '')}\n\t\t\t\t\t{const selected = $derived(current && column === guess.length)}\n\t\t\t\t\t{const exact = $derived(answer === 'x')}\n\t\t\t\t\t{const close = $derived(answer === 'c')}\n\t\t\t\t\t{const missing = $derived(answer === '_')}\n\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.shake .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 { prefersReducedMotion } from 'svelte/motion';\n\timport type { PageProps } from './$types';\n\n\tlet { data }: PageProps = $props();\n\n\tlet shake = $state(false);\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\tshake = 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 ({ result, update }) => {\n\t\t\tshake = result.type === 'failure';\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:shake onanimationend={() => (shake = false)}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{const current = $derived(row === i)}\n\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 = $derived(current ? currentGuess : data.guesses[row])}\n\t\t\t\t\t{const answer = $derived(data.answers[row]?.[column])}\n\t\t\t\t\t{const value = $derived(guess?.[column] ?? '')}\n\t\t\t\t\t{const selected = $derived(current && column === guess.length)}\n\t\t\t\t\t{const exact = $derived(answer === 'x')}\n\t\t\t\t\t{const close = $derived(answer === 'c')}\n\t\t\t\t\t{const missing = $derived(answer === '_')}\n\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: prefersReducedMotion.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.shake .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",
@@ -15,7 +15,7 @@
15
15
  "@sveltejs/adapter-auto": "^8.0.0",
16
16
  "@sveltejs/kit": "^3.0.0",
17
17
  "@sveltejs/vite-plugin-svelte": "^7.2.0",
18
- "svelte": "^5.56.7",
18
+ "svelte": "^5.57.1",
19
19
  "vite": "^8.3.0"
20
20
  },
21
21
  "imports": {
@@ -28,7 +28,7 @@
28
28
  "@sveltejs/package": "^2.5.8",
29
29
  "@sveltejs/vite-plugin-svelte": "^7.2.0",
30
30
  "publint": "^0.3.21",
31
- "svelte": "^5.56.7",
31
+ "svelte": "^5.57.1",
32
32
  "typescript": "^6.0.3",
33
33
  "vite": "^8.3.0"
34
34
  },