sv 0.12.8 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +14 -18
- package/dist/{engine-DUNH7ELq.mjs → engine-DOKagHfI.mjs} +1003 -876
- package/dist/{engine-BZ0rj9tz.d.mts → engine-M8NNzIgv.d.mts} +29 -48
- package/dist/{package-manager-BYzDyeam.mjs → package-manager-BU-MLhop.mjs} +946 -1503
- package/dist/shared.json +2 -2
- package/dist/src/index.d.mts +1 -1
- package/dist/src/index.mjs +3 -4
- package/dist/src/testing.d.mts +3 -6
- package/dist/src/testing.mjs +33 -47
- package/dist/templates/demo/files.types=checkjs.json +1 -1
- package/dist/templates/demo/files.types=none.json +1 -1
- package/dist/templates/demo/files.types=typescript.json +1 -1
- package/dist/templates/demo/package.json +1 -1
- package/dist/templates/library/package.json +1 -1
- package/dist/templates/minimal/package.json +1 -1
- package/dist/templates/svelte/files.types=none.json +1 -1
- package/dist/templates/svelte/files.types=typescript.json +1 -1
- package/package.json +1 -1
package/dist/shared.json
CHANGED
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"typescript"
|
|
85
85
|
],
|
|
86
86
|
"exclude": [],
|
|
87
|
-
"contents": "import adapter from '@sveltejs/adapter-auto';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.\n\t\t// If your environment is not supported, or you settled on a specific environment, switch out the adapter.\n\t\t// See https://svelte.dev/docs/kit/adapters for more information about adapters.\n\t\tadapter: adapter()\n\t}
|
|
87
|
+
"contents": "import adapter from '@sveltejs/adapter-auto';\nimport { relative, sep } from 'node:path';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\tcompilerOptions: {\n\t\t// defaults to rune mode for the project, execept for `node_modules`. Can be removed in svelte 6.\n\t\trunes: ({ filename }) => {\n\t\t\tconst relativePath = relative(import.meta.dirname, filename);\n\t\t\tconst pathSegments = relativePath.toLowerCase().split(sep);\n\t\t\tconst isExternalLibrary = pathSegments.includes('node_modules');\n\n\t\t\treturn isExternalLibrary ? undefined : true;\n\t\t}\n\t},\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.\n\t\t// If your environment is not supported, or you settled on a specific environment, switch out the adapter.\n\t\t// See https://svelte.dev/docs/kit/adapters for more information about adapters.\n\t\tadapter: adapter()\n\t}\n};\n\nexport default config;\n"
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"name": "tsconfig.json",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"exclude": [
|
|
101
101
|
"typescript"
|
|
102
102
|
],
|
|
103
|
-
"contents": "import adapter from '@sveltejs/adapter-auto';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.\n\t\t// If your environment is not supported, or you settled on a specific environment, switch out the adapter.\n\t\t// See https://svelte.dev/docs/kit/adapters for more information about adapters.\n\t\tadapter: adapter()\n\t}
|
|
103
|
+
"contents": "import adapter from '@sveltejs/adapter-auto';\nimport { relative, sep } from 'node:path';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\tcompilerOptions: {\n\t\t// defaults to rune mode for the project, execept for `node_modules`. Can be removed in svelte 6.\n\t\trunes: ({ filename }) => {\n\t\t\tconst relativePath = relative(import.meta.dirname, filename);\n\t\t\tconst pathSegments = relativePath.toLowerCase().split(sep);\n\t\t\tconst isExternalLibrary = pathSegments.includes('node_modules');\n\n\t\t\treturn isExternalLibrary ? undefined : true;\n\t\t}\n\t},\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.\n\t\t// If your environment is not supported, or you settled on a specific environment, switch out the adapter.\n\t\t// See https://svelte.dev/docs/kit/adapters for more information about adapters.\n\t\tadapter: adapter()\n\t}\n};\n\nexport default config;\n"
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
"name": "vite.config.ts",
|
package/dist/src/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as BooleanQuestion, C as defineAddon, D as WorkspaceOptions, E as Workspace, F as Question, I as SelectQuestion, L as StringQuestion, M as NumberQuestion, N as OptionDefinition, O as createWorkspace, P as OptionValues, R as officialAddons, S as Verification, T as getErrorHint, _ as Scripts, a as Addon, b as TestDefinition, c as AddonReference, d as ConditionDefinition, f as ConfiguredAddon, g as PreparedAddon, h as PackageDefinition, i as add, j as MultiSelectQuestion, k as BaseQuestion, 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 SetupResult, w as defineAddonOptions, x as Tests, y as SvApi } from "../engine-
|
|
1
|
+
import { A as BooleanQuestion, C as defineAddon, D as WorkspaceOptions, E as Workspace, F as Question, I as SelectQuestion, L as StringQuestion, M as NumberQuestion, N as OptionDefinition, O as createWorkspace, P as OptionValues, R as officialAddons, S as Verification, T as getErrorHint, _ as Scripts, a as Addon, b as TestDefinition, c as AddonReference, d as ConditionDefinition, f as ConfiguredAddon, g as PreparedAddon, h as PackageDefinition, i as add, j as MultiSelectQuestion, k as BaseQuestion, 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 SetupResult, w as defineAddonOptions, x as Tests, y as SvApi } from "../engine-M8NNzIgv.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/create/index.d.ts
|
|
4
4
|
type TemplateType = (typeof templateTypes)[number];
|
package/dist/src/index.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { p as create } from "../package-manager-
|
|
2
|
-
import { c as officialAddons, l as defineAddon, t as add, u as defineAddonOptions } from "../engine-
|
|
3
|
-
|
|
4
|
-
export { add, create, defineAddon, defineAddonOptions, officialAddons };
|
|
1
|
+
import { p as create } from "../package-manager-BU-MLhop.mjs";
|
|
2
|
+
import { c as officialAddons, l as defineAddon, t as add, u as defineAddonOptions } from "../engine-DOKagHfI.mjs";
|
|
3
|
+
export { add, create, defineAddon, defineAddonOptions, officialAddons };
|
package/dist/src/testing.d.mts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { r as OptionMap, t as AddonMap } from "../engine-
|
|
1
|
+
import { r as OptionMap, t as AddonMap } from "../engine-M8NNzIgv.mjs";
|
|
2
2
|
import { AgentName } from "@sveltejs/sv-utils";
|
|
3
3
|
import { Page } from "@playwright/test";
|
|
4
4
|
import { TestProject } from "vitest/node";
|
|
5
5
|
|
|
6
6
|
//#region src/core/package-manager.d.ts
|
|
7
|
-
|
|
8
7
|
declare function addPnpmBuildDependencies(cwd: string, packageManager: AgentName | null | undefined, allowedPackages: string[]): Promise<void>;
|
|
9
8
|
//#endregion
|
|
10
9
|
//#region src/testing.d.ts
|
|
@@ -12,14 +11,12 @@ type ProjectVariant = "kit-js" | "kit-ts" | "vite-js" | "vite-ts";
|
|
|
12
11
|
declare const variants: ProjectVariant[];
|
|
13
12
|
type CreateProject = (options: {
|
|
14
13
|
testId: string;
|
|
15
|
-
variant: ProjectVariant;
|
|
16
|
-
/** @default true */
|
|
14
|
+
variant: ProjectVariant; /** @default true */
|
|
17
15
|
clean?: boolean;
|
|
18
16
|
}) => string;
|
|
19
17
|
type SetupOptions = {
|
|
20
18
|
cwd: string;
|
|
21
|
-
variants: readonly ProjectVariant[];
|
|
22
|
-
/** @default false */
|
|
19
|
+
variants: readonly ProjectVariant[]; /** @default false */
|
|
23
20
|
clean?: boolean;
|
|
24
21
|
};
|
|
25
22
|
declare function setup({
|
package/dist/src/testing.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { B as __toESM, R as __commonJSMin, T as
|
|
1
|
+
import { B as __toESM, R as __commonJSMin, T as z, n as addPnpmBuildDependencies, p as create, w as R, z as __require } from "../package-manager-BU-MLhop.mjs";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process$1 from "node:process";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
|
-
|
|
7
6
|
//#region ../../node_modules/.pnpm/through@2.3.8/node_modules/through/index.js
|
|
8
7
|
var require_through = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
9
8
|
var Stream$4 = __require("stream");
|
|
@@ -83,15 +82,14 @@ var require_through = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
83
82
|
return stream;
|
|
84
83
|
}
|
|
85
84
|
}));
|
|
86
|
-
|
|
87
85
|
//#endregion
|
|
88
86
|
//#region ../../node_modules/.pnpm/from@0.1.7/node_modules/from/index.js
|
|
89
87
|
var require_from = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
90
88
|
var Stream$3 = __require("stream");
|
|
91
|
-
module.exports = function from
|
|
89
|
+
module.exports = function from(source) {
|
|
92
90
|
if (Array.isArray(source)) {
|
|
93
91
|
var source_index = 0, source_len = source.length;
|
|
94
|
-
return from
|
|
92
|
+
return from(function(i) {
|
|
95
93
|
if (source_index < source_len) this.emit("data", source[source_index++]);
|
|
96
94
|
else this.emit("end");
|
|
97
95
|
return true;
|
|
@@ -135,7 +133,6 @@ var require_from = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
135
133
|
return s;
|
|
136
134
|
};
|
|
137
135
|
}));
|
|
138
|
-
|
|
139
136
|
//#endregion
|
|
140
137
|
//#region ../../node_modules/.pnpm/duplexer@0.1.2/node_modules/duplexer/index.js
|
|
141
138
|
var require_duplexer = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -184,8 +181,8 @@ var require_duplexer = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
184
181
|
}
|
|
185
182
|
}
|
|
186
183
|
function proxyStream(methodName) {
|
|
187
|
-
reader.on(methodName, reemit
|
|
188
|
-
function reemit
|
|
184
|
+
reader.on(methodName, reemit);
|
|
185
|
+
function reemit() {
|
|
189
186
|
var args = slice.call(arguments);
|
|
190
187
|
args.unshift(methodName);
|
|
191
188
|
stream.emit.apply(stream, args);
|
|
@@ -203,7 +200,6 @@ var require_duplexer = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
203
200
|
}
|
|
204
201
|
}
|
|
205
202
|
}));
|
|
206
|
-
|
|
207
203
|
//#endregion
|
|
208
204
|
//#region ../../node_modules/.pnpm/map-stream@0.1.0/node_modules/map-stream/index.js
|
|
209
205
|
var require_map_stream = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -284,13 +280,11 @@ var require_map_stream = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
284
280
|
return stream;
|
|
285
281
|
};
|
|
286
282
|
}));
|
|
287
|
-
|
|
288
283
|
//#endregion
|
|
289
284
|
//#region ../../node_modules/.pnpm/pause-stream@0.0.11/node_modules/pause-stream/index.js
|
|
290
285
|
var require_pause_stream = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
291
286
|
module.exports = require_through();
|
|
292
287
|
}));
|
|
293
|
-
|
|
294
288
|
//#endregion
|
|
295
289
|
//#region ../../node_modules/.pnpm/split@0.3.3/node_modules/split/index.js
|
|
296
290
|
var require_split = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -331,7 +325,6 @@ var require_split = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
331
325
|
});
|
|
332
326
|
}
|
|
333
327
|
}));
|
|
334
|
-
|
|
335
328
|
//#endregion
|
|
336
329
|
//#region ../../node_modules/.pnpm/stream-combiner@0.0.4/node_modules/stream-combiner/index.js
|
|
337
330
|
var require_stream_combiner = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -340,10 +333,10 @@ var require_stream_combiner = /* @__PURE__ */ __commonJSMin(((exports, module) =
|
|
|
340
333
|
var streams = [].slice.call(arguments), first = streams[0], last = streams[streams.length - 1], thepipe = duplexer(first, last);
|
|
341
334
|
if (streams.length == 1) return streams[0];
|
|
342
335
|
else if (!streams.length) throw new Error("connect called with empty args");
|
|
343
|
-
function recurse(streams
|
|
344
|
-
if (streams
|
|
345
|
-
streams
|
|
346
|
-
recurse(streams
|
|
336
|
+
function recurse(streams) {
|
|
337
|
+
if (streams.length < 2) return;
|
|
338
|
+
streams[0].pipe(streams[1]);
|
|
339
|
+
recurse(streams.slice(1));
|
|
347
340
|
}
|
|
348
341
|
recurse(streams);
|
|
349
342
|
function onerror() {
|
|
@@ -355,7 +348,6 @@ var require_stream_combiner = /* @__PURE__ */ __commonJSMin(((exports, module) =
|
|
|
355
348
|
return thepipe;
|
|
356
349
|
};
|
|
357
350
|
}));
|
|
358
|
-
|
|
359
351
|
//#endregion
|
|
360
352
|
//#region ../../node_modules/.pnpm/event-stream@3.3.4/node_modules/event-stream/index.js
|
|
361
353
|
var require_event_stream = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
@@ -460,8 +452,8 @@ var require_event_stream = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
460
452
|
reading = false;
|
|
461
453
|
get.apply(null, arguments);
|
|
462
454
|
});
|
|
463
|
-
} catch (err
|
|
464
|
-
stream.emit("error", err
|
|
455
|
+
} catch (err) {
|
|
456
|
+
stream.emit("error", err);
|
|
465
457
|
}
|
|
466
458
|
});
|
|
467
459
|
}
|
|
@@ -516,13 +508,13 @@ var require_event_stream = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
516
508
|
});
|
|
517
509
|
};
|
|
518
510
|
es$1.stringify = function() {
|
|
519
|
-
var Buffer
|
|
511
|
+
var Buffer = __require("buffer").Buffer;
|
|
520
512
|
return es$1.mapSync(function(e) {
|
|
521
|
-
return JSON.stringify(Buffer
|
|
513
|
+
return JSON.stringify(Buffer.isBuffer(e) ? e.toString() : e) + "\n";
|
|
522
514
|
});
|
|
523
515
|
};
|
|
524
|
-
es$1.replace = function(from
|
|
525
|
-
return es$1.pipeline(es$1.split(from
|
|
516
|
+
es$1.replace = function(from, to) {
|
|
517
|
+
return es$1.pipeline(es$1.split(from), es$1.join(to));
|
|
526
518
|
};
|
|
527
519
|
es$1.join = function(str) {
|
|
528
520
|
if ("function" === typeof str) return es$1.wait(str);
|
|
@@ -549,10 +541,9 @@ var require_event_stream = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
549
541
|
throw new Error("[EVENT-STREAM] es.pipeable is deprecated");
|
|
550
542
|
};
|
|
551
543
|
}));
|
|
552
|
-
|
|
553
544
|
//#endregion
|
|
554
|
-
//#region
|
|
555
|
-
var
|
|
545
|
+
//#region src/testing.ts
|
|
546
|
+
var import_ps_tree = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
556
547
|
var spawn$1 = __require("child_process").spawn, es = require_event_stream();
|
|
557
548
|
module.exports = function childrenOfPid(pid, callback) {
|
|
558
549
|
var headers = null;
|
|
@@ -608,11 +599,7 @@ var require_ps_tree = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
608
599
|
default: throw new Error("Unknown process listing header: " + str);
|
|
609
600
|
}
|
|
610
601
|
}
|
|
611
|
-
}));
|
|
612
|
-
|
|
613
|
-
//#endregion
|
|
614
|
-
//#region src/testing.ts
|
|
615
|
-
var import_ps_tree = /* @__PURE__ */ __toESM(require_ps_tree(), 1);
|
|
602
|
+
})))(), 1);
|
|
616
603
|
const variants = [
|
|
617
604
|
"kit-js",
|
|
618
605
|
"kit-ts",
|
|
@@ -620,15 +607,15 @@ const variants = [
|
|
|
620
607
|
"vite-ts"
|
|
621
608
|
];
|
|
622
609
|
const TEMPLATES_DIR = ".templates";
|
|
623
|
-
function setup({ cwd
|
|
624
|
-
const workingDir = path.resolve(cwd
|
|
610
|
+
function setup({ cwd, clean = false, variants }) {
|
|
611
|
+
const workingDir = path.resolve(cwd);
|
|
625
612
|
if (clean && fs.existsSync(workingDir)) fs.rmSync(workingDir, {
|
|
626
613
|
force: true,
|
|
627
614
|
recursive: true
|
|
628
615
|
});
|
|
629
616
|
const templatesDir = path.resolve(workingDir, TEMPLATES_DIR);
|
|
630
617
|
fs.mkdirSync(templatesDir, { recursive: true });
|
|
631
|
-
for (const variant of variants
|
|
618
|
+
for (const variant of variants) {
|
|
632
619
|
const templatePath = path.resolve(templatesDir, variant);
|
|
633
620
|
if (fs.existsSync(templatePath)) continue;
|
|
634
621
|
if (variant === "kit-js") create(templatePath, {
|
|
@@ -655,8 +642,8 @@ function setup({ cwd: cwd$1, clean = false, variants: variants$1 }) {
|
|
|
655
642
|
}
|
|
656
643
|
return { templatesDir };
|
|
657
644
|
}
|
|
658
|
-
function createProject({ cwd
|
|
659
|
-
const testDir = path.resolve(cwd
|
|
645
|
+
function createProject({ cwd, testName, templatesDir }) {
|
|
646
|
+
const testDir = path.resolve(cwd, testName);
|
|
660
647
|
fs.mkdirSync(testDir, { recursive: true });
|
|
661
648
|
return ({ testId, variant, clean = true }) => {
|
|
662
649
|
const targetDir = path.resolve(testDir, testId);
|
|
@@ -672,11 +659,11 @@ function createProject({ cwd: cwd$1, testName, templatesDir }) {
|
|
|
672
659
|
return targetDir;
|
|
673
660
|
};
|
|
674
661
|
}
|
|
675
|
-
async function startPreview({ cwd
|
|
662
|
+
async function startPreview({ cwd, command = "npm run preview" }) {
|
|
676
663
|
const [cmd, ...args] = command.split(" ");
|
|
677
|
-
const proc =
|
|
664
|
+
const proc = z(cmd, args, {
|
|
678
665
|
nodeOptions: {
|
|
679
|
-
cwd
|
|
666
|
+
cwd,
|
|
680
667
|
stdio: "pipe"
|
|
681
668
|
},
|
|
682
669
|
throwOnError: true,
|
|
@@ -686,13 +673,13 @@ async function startPreview({ cwd: cwd$1, command = "npm run preview" }) {
|
|
|
686
673
|
if (!proc.pid) return;
|
|
687
674
|
await terminate(proc.pid);
|
|
688
675
|
};
|
|
689
|
-
return await new Promise((resolve
|
|
676
|
+
return await new Promise((resolve, reject) => {
|
|
690
677
|
if (!proc.process?.stdout) return reject("impossible state");
|
|
691
678
|
proc.process.stdout.on("data", (data) => {
|
|
692
679
|
const urls = data.toString().replace(/[^\x20-\xaf]+/g, "").replace(/\[[0-9]{1,2}m/g, "").match(/http:\/\/[^:\s]+:[0-9]+\//g);
|
|
693
680
|
if (urls && urls.length > 0) {
|
|
694
681
|
const url = urls[0];
|
|
695
|
-
resolve
|
|
682
|
+
resolve({
|
|
696
683
|
url,
|
|
697
684
|
close
|
|
698
685
|
});
|
|
@@ -710,7 +697,7 @@ async function getProcessTree(pid) {
|
|
|
710
697
|
}
|
|
711
698
|
async function terminate(pid) {
|
|
712
699
|
if (process$1.platform === "win32") {
|
|
713
|
-
await
|
|
700
|
+
await R("taskkill", [
|
|
714
701
|
"/PID",
|
|
715
702
|
`${pid}`,
|
|
716
703
|
"/T",
|
|
@@ -745,13 +732,13 @@ function setupGlobal({ TEST_DIR, pre, post }) {
|
|
|
745
732
|
};
|
|
746
733
|
};
|
|
747
734
|
}
|
|
748
|
-
async function prepareServer({ cwd
|
|
735
|
+
async function prepareServer({ cwd, page, buildCommand = "pnpm build", previewCommand = "pnpm preview" }) {
|
|
749
736
|
if (buildCommand) execSync(buildCommand, {
|
|
750
|
-
cwd
|
|
737
|
+
cwd,
|
|
751
738
|
stdio: "pipe"
|
|
752
739
|
});
|
|
753
740
|
const { url, close } = await startPreview({
|
|
754
|
-
cwd
|
|
741
|
+
cwd,
|
|
755
742
|
command: previewCommand
|
|
756
743
|
});
|
|
757
744
|
page.setDefaultNavigationTimeout(62e3);
|
|
@@ -766,6 +753,5 @@ async function prepareServer({ cwd: cwd$1, page, buildCommand = "pnpm build", pr
|
|
|
766
753
|
close
|
|
767
754
|
};
|
|
768
755
|
}
|
|
769
|
-
|
|
770
756
|
//#endregion
|
|
771
|
-
export { addPnpmBuildDependencies, createProject, prepareServer, setup, setupGlobal, startPreview, variants };
|
|
757
|
+
export { addPnpmBuildDependencies, createProject, prepareServer, setup, setupGlobal, startPreview, variants };
|
|
@@ -37,7 +37,7 @@
|
|
|
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 * @typedef {Object} Props\n\t * @property {import('./$types').PageData} data\n\t * @property {import('./$types').ActionData} form\n\t */\n\n\t/**\n\t * @type {Props}\n\t */\n\tlet { data, form = $bindable() } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\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 = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.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 { 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 = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.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",
|
|
@@ -33,7 +33,7 @@
|
|
|
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, form = $bindable() } = $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 = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.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 { 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 = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.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",
|
|
@@ -37,7 +37,7 @@
|
|
|
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\n\timport type { ActionData, PageData } from './$types';\n\n\tinterface Props {\n\t\tdata: PageData;\n\t\tform: ActionData;\n\t}\n\tlet { data, form = $bindable() }: Props = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\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 = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.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 { MediaQuery } from 'svelte/reactivity';\n\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 = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.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",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"name": "package.json",
|
|
16
|
-
"contents": "{\n \"name\": \"vite-svelte-starter\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"@sveltejs/vite-plugin-svelte\": \"^6.2.1\",\n \"svelte\": \"^5.
|
|
16
|
+
"contents": "{\n \"name\": \"vite-svelte-starter\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\"\n },\n \"devDependencies\": {\n \"@sveltejs/vite-plugin-svelte\": \"^6.2.1\",\n \"svelte\": \"^5.45.2\",\n \"vite\": \"^7.3.1\"\n }\n}\n"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "src/App.svelte",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"name": "package.json",
|
|
12
|
-
"contents": "{\n \"name\": \"vite-svelte-ts-starter\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"check\": \"svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json\"\n },\n \"devDependencies\": {\n \"@sveltejs/vite-plugin-svelte\": \"^6.2.1\",\n \"@tsconfig/svelte\": \"^5.0.6\",\n \"@types/node\": \"^24.10.1\",\n \"svelte\": \"^5.
|
|
12
|
+
"contents": "{\n \"name\": \"vite-svelte-ts-starter\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"check\": \"svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json\"\n },\n \"devDependencies\": {\n \"@sveltejs/vite-plugin-svelte\": \"^6.2.1\",\n \"@tsconfig/svelte\": \"^5.0.6\",\n \"@types/node\": \"^24.10.1\",\n \"svelte\": \"^5.45.2\",\n \"svelte-check\": \"^4.3.4\",\n \"typescript\": \"~5.9.3\",\n \"vite\": \"^7.3.1\"\n }\n}\n"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"name": "src/App.svelte",
|