@markforster/heroquest-card-creator 0.5.3-2
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/LICENSE +674 -0
- package/README.md +59 -0
- package/README.npm.md +59 -0
- package/bin/clear asset type classification in indexdb.ts +31 -0
- package/bin/heroquest-card-creator.js +572 -0
- package/out/404/index.html +66 -0
- package/out/404.html +66 -0
- package/out/_next/static/chunks/117-b8bb75121b26b6b3.js +2 -0
- package/out/_next/static/chunks/140-37b9051037068ddf.js +1 -0
- package/out/_next/static/chunks/275-f9dcc3143453fdcd.js +1 -0
- package/out/_next/static/chunks/421.f7d6805b56f50951.js +1 -0
- package/out/_next/static/chunks/481-657f5adb2ca2e132.js +1 -0
- package/out/_next/static/chunks/792.78b6ddda4e792193.js +1 -0
- package/out/_next/static/chunks/901.97c805b54bd60aa3.js +1 -0
- package/out/_next/static/chunks/924.4a054bc97175666b.js +1 -0
- package/out/_next/static/chunks/app/_not-found/page-12d4e6e8548a2d4a.js +1 -0
- package/out/_next/static/chunks/app/layout-280540b43bd5178c.js +1 -0
- package/out/_next/static/chunks/app/page-d8b18bd78378643b.js +1 -0
- package/out/_next/static/chunks/b536a0f1-15fcb22ec48614b3.js +230 -0
- package/out/_next/static/chunks/cb7556af-ac91ad5099c3d0ef.js +8 -0
- package/out/_next/static/chunks/fd9d1056-898d1c4f9326004a.js +1 -0
- package/out/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- package/out/_next/static/chunks/main-5e42ad27df38cdd8.js +1 -0
- package/out/_next/static/chunks/main-app-0b6a7f5b2fb24b40.js +1 -0
- package/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- package/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/out/_next/static/chunks/webpack-498a638590fe54e0.js +1 -0
- package/out/_next/static/css/83d0cd8babb21fe7.css +1 -0
- package/out/_next/static/css/d071dca61fbb01bf.css +5 -0
- package/out/_next/static/css/f4ac97d6546ca0d3.css +1 -0
- package/out/_next/static/css/fc205991348f38b9.css +1 -0
- package/out/_next/static/lmyD8fTLUGVhT66l1Dl08/_buildManifest.js +1 -0
- package/out/_next/static/lmyD8fTLUGVhT66l1Dl08/_ssgManifest.js +1 -0
- package/out/_next/static/media/blueprint.3b5b53f6.png +0 -0
- package/out/_next/static/media/bordered-mask.7fa8cc53.png +0 -0
- package/out/_next/static/media/combat_ad.c4d7dbac.svg +4 -0
- package/out/_next/static/media/combat_cd.37de7e25.svg +4 -0
- package/out/_next/static/media/combat_dd.1e82a6e7.svg +4 -0
- package/out/_next/static/media/combat_md.1be931b6.svg +4 -0
- package/out/_next/static/media/combat_monster_white.2cd272da.svg +4 -0
- package/out/_next/static/media/combat_shield_white.d707acf1.svg +11 -0
- package/out/_next/static/media/combat_skull_white.fc69a846.svg +7 -0
- package/out/_next/static/media/d6_pips_1.6c7ee5ed.svg +4 -0
- package/out/_next/static/media/d6_pips_2.5cbe4a47.svg +5 -0
- package/out/_next/static/media/d6_pips_3.1dedc2f4.svg +6 -0
- package/out/_next/static/media/d6_pips_4.9ae66448.svg +7 -0
- package/out/_next/static/media/d6_pips_5.e39667a9.svg +8 -0
- package/out/_next/static/media/d6_pips_6.9e8e43ee.svg +9 -0
- package/out/_next/static/media/hero-back.747a9227.png +0 -0
- package/out/_next/static/media/hero-stats.4e17ceac.png +0 -0
- package/out/_next/static/media/hero.be23b2e7.png +0 -0
- package/out/_next/static/media/labelled-back.0547e374.png +0 -0
- package/out/_next/static/media/large-treasure.e0644006.png +0 -0
- package/out/_next/static/media/large-window.ea8dd1b5.png +0 -0
- package/out/_next/static/media/linen-5.7e5cb84b.png +0 -0
- package/out/_next/static/media/monster-stats.e914ce2f.png +0 -0
- package/out/_next/static/media/monster.72370890.png +0 -0
- package/out/_next/static/media/parchment.09cbcdf5.png +0 -0
- package/out/_next/static/media/ribbon.1481d194.png +0 -0
- package/out/_next/static/media/small-large-artwork-border-alpha-mask.e9ccef3c.png +0 -0
- package/out/_next/static/media/small-large-artwork-border-blend-texture.e5ea5707.png +0 -0
- package/out/_next/static/media/small-treasure.3a8b8444.png +0 -0
- package/out/_next/static/media/small-window.3109823c.png +0 -0
- package/out/_next/static/media/white-paper.6aa299d7.png +0 -0
- package/out/assets/apple-touch-icon.png +0 -0
- package/out/assets/favicon-96x96.png +0 -0
- package/out/assets/favicon.ico +0 -0
- package/out/assets/favicon.svg +3 -0
- package/out/assets/site.webmanifest +21 -0
- package/out/assets/web-app-manifest-192x192.png +0 -0
- package/out/assets/web-app-manifest-512x512.png +0 -0
- package/out/favicon.ico +0 -0
- package/out/fonts/Carter Sans W01 Bold.ttf +0 -0
- package/out/fonts/Carter Sans W01 Medium.ttf +0 -0
- package/out/fonts/Carter Sans W01 Regular.ttf +0 -0
- package/out/fonts/HeroQuest.ttf +0 -0
- package/out/index.html +66 -0
- package/out/index.txt +42 -0
- package/out/robots.txt +4 -0
- package/out/sitemap.xml +9 -0
- package/out/zip/zip-web-worker.js +1 -0
- package/package.json +133 -0
- package/readme-assets/banner.jpg +0 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const net = require("net");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawn } = require("child_process");
|
|
10
|
+
const mime = require("mime");
|
|
11
|
+
const yaml = require("yaml");
|
|
12
|
+
const chalk = require("chalk");
|
|
13
|
+
|
|
14
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
15
|
+
const DEFAULT_PORT = 3000;
|
|
16
|
+
const HISTORY_LIMIT = 10;
|
|
17
|
+
|
|
18
|
+
function parseArgs(args) {
|
|
19
|
+
let port = null;
|
|
20
|
+
let portSpecified = false;
|
|
21
|
+
let help = false;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
24
|
+
const arg = args[i];
|
|
25
|
+
|
|
26
|
+
if (arg === "-h" || arg === "--help") {
|
|
27
|
+
help = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (arg === "-p" || arg === "--port") {
|
|
32
|
+
const next = args[i + 1];
|
|
33
|
+
if (!next) {
|
|
34
|
+
throw new Error("Missing value for --port");
|
|
35
|
+
}
|
|
36
|
+
port = Number(next);
|
|
37
|
+
portSpecified = true;
|
|
38
|
+
i += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (arg.startsWith("--port=")) {
|
|
43
|
+
port = Number(arg.split("=")[1]);
|
|
44
|
+
portSpecified = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (portSpecified) {
|
|
50
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
51
|
+
throw new Error(`Invalid port: ${port}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { port, portSpecified, help };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printHelp() {
|
|
59
|
+
console.log(
|
|
60
|
+
[
|
|
61
|
+
"",
|
|
62
|
+
"HeroQuest Card Creator",
|
|
63
|
+
"",
|
|
64
|
+
"Usage:",
|
|
65
|
+
" heroquest-card-creator [options]",
|
|
66
|
+
"",
|
|
67
|
+
"Options:",
|
|
68
|
+
" -p, --port <number> Port to serve on (default 3000; auto-picks a free port if needed)",
|
|
69
|
+
" -h, --help Show this help",
|
|
70
|
+
"",
|
|
71
|
+
].join("\n")
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toSafeFilePath(rootDir, pathname) {
|
|
76
|
+
const fullPath = path.resolve(rootDir, "." + pathname);
|
|
77
|
+
|
|
78
|
+
if (!fullPath.startsWith(rootDir + path.sep) && fullPath !== rootDir) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return fullPath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getFileForRequest(rootDir, pathname) {
|
|
86
|
+
let resolvedPath = pathname;
|
|
87
|
+
|
|
88
|
+
if (resolvedPath.endsWith("/")) {
|
|
89
|
+
resolvedPath += "index.html";
|
|
90
|
+
} else if (!path.extname(resolvedPath)) {
|
|
91
|
+
resolvedPath += "/index.html";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const candidate = toSafeFilePath(rootDir, resolvedPath);
|
|
95
|
+
if (!candidate) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
100
|
+
return candidate;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const notFound = path.join(rootDir, "404.html");
|
|
104
|
+
if (fs.existsSync(notFound) && fs.statSync(notFound).isFile()) {
|
|
105
|
+
return { fallback: notFound };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createServer(rootDir) {
|
|
112
|
+
return http.createServer((req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const rawUrl = req.url || "/";
|
|
115
|
+
const pathOnly = rawUrl.split("?")[0];
|
|
116
|
+
let pathname = decodeURIComponent(pathOnly || "/");
|
|
117
|
+
if (!pathname.startsWith("/")) {
|
|
118
|
+
pathname = `/${pathname}`;
|
|
119
|
+
}
|
|
120
|
+
pathname = pathname.replace(/\/{2,}/g, "/");
|
|
121
|
+
const result = getFileForRequest(rootDir, pathname);
|
|
122
|
+
|
|
123
|
+
if (!result) {
|
|
124
|
+
res.statusCode = 404;
|
|
125
|
+
res.end("Not Found");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const filePath = typeof result === "string" ? result : result.fallback;
|
|
130
|
+
const isFallback = typeof result !== "string";
|
|
131
|
+
|
|
132
|
+
const type = mime.getType(filePath) || "application/octet-stream";
|
|
133
|
+
const contentType = type.startsWith("text/") ? `${type}; charset=utf-8` : type;
|
|
134
|
+
|
|
135
|
+
res.statusCode = isFallback ? 404 : 200;
|
|
136
|
+
res.setHeader("Content-Type", contentType);
|
|
137
|
+
fs.createReadStream(filePath).pipe(res);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
res.statusCode = 500;
|
|
140
|
+
res.end("Internal Server Error");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getInfoFilePath() {
|
|
146
|
+
return path.join(os.homedir(), ".hqcc", "info.yml");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function loadInfo() {
|
|
150
|
+
const infoPath = getInfoFilePath();
|
|
151
|
+
if (!fs.existsSync(infoPath)) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const raw = fs.readFileSync(infoPath, "utf8");
|
|
157
|
+
const parsed = yaml.parse(raw);
|
|
158
|
+
if (!parsed || typeof parsed !== "object") {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return parsed;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.warn("[heroquest-card-creator] Failed to read info file. It will be recreated.");
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function saveInfo(port, existing) {
|
|
169
|
+
const infoPath = getInfoFilePath();
|
|
170
|
+
const dir = path.dirname(infoPath);
|
|
171
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
172
|
+
|
|
173
|
+
const previousPorts = Array.isArray(existing?.ports) ? existing.ports : [];
|
|
174
|
+
const nextPorts = [port, ...previousPorts.filter((value) => value !== port)].slice(0, HISTORY_LIMIT);
|
|
175
|
+
|
|
176
|
+
const payload = {
|
|
177
|
+
lastPort: port,
|
|
178
|
+
ports: nextPorts,
|
|
179
|
+
updatedAt: new Date().toISOString(),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
fs.writeFileSync(infoPath, yaml.stringify(payload), "utf8");
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.warn(
|
|
186
|
+
"[heroquest-card-creator] Warning: Failed to write ~/.hqcc/info.yml; port history will not be saved."
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return payload;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isInteractive() {
|
|
194
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function openBrowser(url) {
|
|
198
|
+
let command;
|
|
199
|
+
let args;
|
|
200
|
+
|
|
201
|
+
if (process.platform === "darwin") {
|
|
202
|
+
command = "open";
|
|
203
|
+
args = [url];
|
|
204
|
+
} else if (process.platform === "win32") {
|
|
205
|
+
command = "cmd";
|
|
206
|
+
args = ["/c", "start", "", url];
|
|
207
|
+
} else {
|
|
208
|
+
command = "xdg-open";
|
|
209
|
+
args = [url];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
214
|
+
child.unref();
|
|
215
|
+
return true;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function promptOpenBrowser(url) {
|
|
222
|
+
if (!isInteractive()) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const { default: inquirer } = await import("inquirer");
|
|
227
|
+
const answer = await inquirer.prompt([
|
|
228
|
+
{
|
|
229
|
+
name: "openBrowser",
|
|
230
|
+
type: "confirm",
|
|
231
|
+
message: chalk.cyanBright("Open the app in your browser now?"),
|
|
232
|
+
default: true,
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
if (!answer.openBrowser) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const opened = openBrowser(url);
|
|
241
|
+
if (!opened) {
|
|
242
|
+
console.warn("[heroquest-card-creator] Unable to open browser automatically. Open the URL manually.");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function canConnect(port, host) {
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
const socket = new net.Socket();
|
|
249
|
+
const timeoutMs = 300;
|
|
250
|
+
|
|
251
|
+
socket.setTimeout(timeoutMs);
|
|
252
|
+
|
|
253
|
+
socket.once("connect", () => {
|
|
254
|
+
socket.destroy();
|
|
255
|
+
resolve(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
socket.once("timeout", () => {
|
|
259
|
+
socket.destroy();
|
|
260
|
+
resolve(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
socket.once("error", (error) => {
|
|
264
|
+
if (
|
|
265
|
+
error.code === "ECONNREFUSED" ||
|
|
266
|
+
error.code === "EHOSTUNREACH" ||
|
|
267
|
+
error.code === "ENETUNREACH" ||
|
|
268
|
+
error.code === "EAFNOSUPPORT"
|
|
269
|
+
) {
|
|
270
|
+
resolve(false);
|
|
271
|
+
} else {
|
|
272
|
+
resolve(false);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
socket.connect(port, host);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function checkPortFree(port) {
|
|
281
|
+
const results = await Promise.all([canConnect(port, "127.0.0.1"), canConnect(port, "::1")]);
|
|
282
|
+
return !results.some(Boolean);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function findFreePort() {
|
|
286
|
+
const ranges = [
|
|
287
|
+
[3000, 3100],
|
|
288
|
+
[4000, 4100],
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
for (const [start, end] of ranges) {
|
|
292
|
+
for (let port = start; port <= end; port += 1) {
|
|
293
|
+
if (await checkPortFree(port)) {
|
|
294
|
+
return port;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
const tester = net.createServer();
|
|
301
|
+
tester.once("error", (error) => reject(error));
|
|
302
|
+
tester.listen(0, DEFAULT_HOST, () => {
|
|
303
|
+
const address = tester.address();
|
|
304
|
+
const port = typeof address === "object" && address ? address.port : null;
|
|
305
|
+
tester.close(() => {
|
|
306
|
+
if (port) {
|
|
307
|
+
resolve(port);
|
|
308
|
+
} else {
|
|
309
|
+
reject(new Error("Failed to acquire a free port"));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function promptForPort(options) {
|
|
317
|
+
const { defaultPort, defaultLabel, historyPorts, reason, allowCustom, exitOption } = options;
|
|
318
|
+
const { default: inquirer } = await import("inquirer");
|
|
319
|
+
|
|
320
|
+
const messageText = chalk.cyanBright(reason);
|
|
321
|
+
const choices = [];
|
|
322
|
+
if (defaultPort) {
|
|
323
|
+
const label = defaultLabel || "Use suggested port";
|
|
324
|
+
choices.push({ name: chalk.cyanBright(`${label} ${defaultPort}`), value: defaultPort });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (historyPorts.length > 0) {
|
|
328
|
+
for (const port of historyPorts) {
|
|
329
|
+
choices.push({ name: chalk.cyanBright(`Use port ${port}`), value: port });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (allowCustom) {
|
|
334
|
+
choices.push({ name: chalk.cyanBright("Enter another port"), value: "custom" });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (exitOption) {
|
|
338
|
+
choices.push({ name: chalk.cyanBright("Exit"), value: "exit" });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const answer = await inquirer.prompt([
|
|
342
|
+
{
|
|
343
|
+
name: "portChoice",
|
|
344
|
+
type: "list",
|
|
345
|
+
message: messageText,
|
|
346
|
+
choices,
|
|
347
|
+
},
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
if (answer.portChoice === "exit") {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (answer.portChoice !== "custom") {
|
|
355
|
+
return answer.portChoice;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const customAnswer = await inquirer.prompt([
|
|
359
|
+
{
|
|
360
|
+
name: "customPort",
|
|
361
|
+
type: "input",
|
|
362
|
+
message: chalk.cyanBright("Enter a port number:"),
|
|
363
|
+
validate: (input) => {
|
|
364
|
+
const value = Number(input);
|
|
365
|
+
if (!Number.isInteger(value) || value <= 0 || value > 65535) {
|
|
366
|
+
return "Please enter a valid port between 1 and 65535.";
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
},
|
|
370
|
+
filter: (input) => Number(input),
|
|
371
|
+
},
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
return customAnswer.customPort;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function resolvePort({ portSpecified, requestedPort, info }) {
|
|
378
|
+
const historyPorts = Array.isArray(info?.ports) ? info.ports : [];
|
|
379
|
+
const lastPort = typeof info?.lastPort === "number" ? info.lastPort : null;
|
|
380
|
+
|
|
381
|
+
if (portSpecified) {
|
|
382
|
+
if (await checkPortFree(requestedPort)) {
|
|
383
|
+
return requestedPort;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!isInteractive()) {
|
|
387
|
+
const suggested = await findFreePort();
|
|
388
|
+
throw new Error(
|
|
389
|
+
`Port ${requestedPort} is already in use. Try -p ${suggested} or another free port.`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let port = await promptForPort({
|
|
394
|
+
defaultPort: await findFreePort(),
|
|
395
|
+
historyPorts: historyPorts.filter((value) => value !== requestedPort),
|
|
396
|
+
reason: `Port ${requestedPort} is in use. Choose another port:`,
|
|
397
|
+
allowCustom: true,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
while (!(await checkPortFree(port))) {
|
|
401
|
+
port = await promptForPort({
|
|
402
|
+
defaultPort: await findFreePort(),
|
|
403
|
+
historyPorts: historyPorts.filter((value) => value !== port),
|
|
404
|
+
reason: `Port ${port} is still in use. Choose another port:`,
|
|
405
|
+
allowCustom: true,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return port;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (info) {
|
|
413
|
+
if (lastPort && (await checkPortFree(lastPort))) {
|
|
414
|
+
if (!isInteractive()) {
|
|
415
|
+
return lastPort;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const choice = await promptForPort({
|
|
419
|
+
defaultPort: lastPort,
|
|
420
|
+
defaultLabel: "Use last port",
|
|
421
|
+
historyPorts: historyPorts.filter((value) => value !== lastPort),
|
|
422
|
+
reason: `Use last port ${lastPort}?`,
|
|
423
|
+
allowCustom: true,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
if (await checkPortFree(choice)) {
|
|
427
|
+
return choice;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (!isInteractive()) {
|
|
432
|
+
return findFreePort();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (lastPort) {
|
|
436
|
+
const suggestedPort = await findFreePort();
|
|
437
|
+
console.warn(
|
|
438
|
+
`You last ran on port ${lastPort}, but it is currently in use. If you switch ports, you will not see the library saved on port ${lastPort}.`
|
|
439
|
+
);
|
|
440
|
+
const choice = await promptForPort({
|
|
441
|
+
defaultPort: suggestedPort,
|
|
442
|
+
defaultLabel: "Use suggested port",
|
|
443
|
+
historyPorts: [],
|
|
444
|
+
reason: "Use a suggested port or exit?",
|
|
445
|
+
allowCustom: false,
|
|
446
|
+
exitOption: true,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
if (choice === null) {
|
|
450
|
+
console.log("[heroquest-card-creator] Exiting without starting the server.");
|
|
451
|
+
process.exit(0);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (await checkPortFree(choice)) {
|
|
455
|
+
return choice;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const freeHistory = [];
|
|
460
|
+
for (const port of historyPorts) {
|
|
461
|
+
if (await checkPortFree(port)) {
|
|
462
|
+
freeHistory.push(port);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let port = await promptForPort({
|
|
467
|
+
defaultPort: freeHistory.length === 0 ? await findFreePort() : null,
|
|
468
|
+
historyPorts: freeHistory,
|
|
469
|
+
reason: "Choose a port to run the app:",
|
|
470
|
+
allowCustom: true,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (port === null) {
|
|
474
|
+
console.log("[heroquest-card-creator] Exiting without starting the server.");
|
|
475
|
+
process.exit(0);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
while (!(await checkPortFree(port))) {
|
|
479
|
+
port = await promptForPort({
|
|
480
|
+
defaultPort: await findFreePort(),
|
|
481
|
+
historyPorts: freeHistory.filter((value) => value !== port),
|
|
482
|
+
reason: `Port ${port} is in use. Choose another port:`,
|
|
483
|
+
allowCustom: true,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
if (port === null) {
|
|
487
|
+
console.log("[heroquest-card-creator] Exiting without starting the server.");
|
|
488
|
+
process.exit(0);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return port;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (await checkPortFree(DEFAULT_PORT)) {
|
|
496
|
+
return DEFAULT_PORT;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return findFreePort();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function warnOnNewPort(port, info) {
|
|
503
|
+
const historyPorts = Array.isArray(info?.ports) ? info.ports : [];
|
|
504
|
+
if (historyPorts.includes(port)) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (historyPorts.length === 0) {
|
|
509
|
+
console.warn(
|
|
510
|
+
"[heroquest-card-creator] Note: Browser storage (IndexedDB/localStorage) is tied to the origin. Using a new port means you won’t see libraries saved on other ports."
|
|
511
|
+
);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.warn(
|
|
516
|
+
`[heroquest-card-creator] Note: Browser storage (IndexedDB/localStorage) is tied to the origin. Using a new port means you won’t see libraries saved on other ports. Known ports: ${historyPorts.join(
|
|
517
|
+
", "
|
|
518
|
+
)}.`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function main() {
|
|
523
|
+
const args = process.argv.slice(2);
|
|
524
|
+
const { port: requestedPort, portSpecified, help } = parseArgs(args);
|
|
525
|
+
|
|
526
|
+
if (help) {
|
|
527
|
+
printHelp();
|
|
528
|
+
process.exit(0);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const rootDir = path.resolve(__dirname, "..", "out");
|
|
532
|
+
|
|
533
|
+
if (!fs.existsSync(rootDir)) {
|
|
534
|
+
console.error("[heroquest-card-creator] out/ directory not found. Run `npm run build` before packaging.");
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const info = loadInfo();
|
|
539
|
+
|
|
540
|
+
let port;
|
|
541
|
+
try {
|
|
542
|
+
port = await resolvePort({ portSpecified, requestedPort, info });
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.error(`[heroquest-card-creator] ${error.message}`);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
warnOnNewPort(port, info);
|
|
549
|
+
|
|
550
|
+
const server = createServer(rootDir);
|
|
551
|
+
server.on("error", (error) => {
|
|
552
|
+
if (error.code === "EADDRINUSE") {
|
|
553
|
+
console.error(
|
|
554
|
+
`[heroquest-card-creator] Port ${port} is already in use. Try a different port with -p.`
|
|
555
|
+
);
|
|
556
|
+
} else {
|
|
557
|
+
console.error("[heroquest-card-creator] Server error:", error);
|
|
558
|
+
}
|
|
559
|
+
process.exit(1);
|
|
560
|
+
});
|
|
561
|
+
server.listen(port, DEFAULT_HOST, () => {
|
|
562
|
+
saveInfo(port, info);
|
|
563
|
+
const url = `http://${DEFAULT_HOST}:${port}`;
|
|
564
|
+
console.log(`HeroQuest Card Creator ready at ${url}`);
|
|
565
|
+
void promptOpenBrowser(url);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
main().catch((error) => {
|
|
570
|
+
console.error("[heroquest-card-creator] Failed to start:", error);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="./_next/static/css/d071dca61fbb01bf.css" data-precedence="next"/><link rel="stylesheet" href="./_next/static/css/fc205991348f38b9.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="./_next/static/chunks/webpack-498a638590fe54e0.js"/><script src="./_next/static/chunks/fd9d1056-898d1c4f9326004a.js" async=""></script><script src="./_next/static/chunks/117-b8bb75121b26b6b3.js" async=""></script><script src="./_next/static/chunks/main-app-0b6a7f5b2fb24b40.js" async=""></script><script src="./_next/static/chunks/275-f9dcc3143453fdcd.js" async=""></script><script src="./_next/static/chunks/481-657f5adb2ca2e132.js" async=""></script><script src="./_next/static/chunks/app/layout-280540b43bd5178c.js" async=""></script><link rel="preload" href="https://www.googletagmanager.com/gtm.js?id=G-KSVRMQ5CPG" as="script"/><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><meta name="theme-color" content="#375f8a"/><meta name="color-scheme" content="light dark"/><title>HeroQuest Card Creator</title><meta name="description" content="A browser-based tool for creating custom HeroQuest-style cards."/><meta name="author" content="HeroQuest Card Creator"/><meta name="keywords" content="HeroQuest,card creator,card maker,tabletop,board game,print and play"/><link rel="canonical" href="https://public.markforster.info/Heroquest/Tools/card-maker/"/><meta property="og:title" content="HeroQuest Card Creator"/><meta property="og:description" content="Create and export custom HeroQuest-style cards directly in your browser."/><meta property="og:url" content="https://public.markforster.info/Heroquest/Tools/card-maker/"/><meta property="og:site_name" content="HeroQuest Card Creator"/><meta property="og:locale" content="en_GB"/><meta property="og:image" content="https://public.markforster.info/Heroquest/Tools/card-maker/images/IMG_2912.jpg"/><meta property="og:type" content="website"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:title" content="HeroQuest Card Creator"/><meta name="twitter:description" content="Create and export custom HeroQuest-style cards directly in your browser."/><meta name="twitter:image" content="https://public.markforster.info/Heroquest/Tools/card-maker/images/IMG_2912.jpg"/><link rel="shortcut icon" href="./favicon.ico"/><link rel="icon" href="./favicon.ico"/><link rel="apple-touch-icon" href="./favicon.ico"/><script src="./_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><script>
|
|
2
|
+
(function () {
|
|
3
|
+
try {
|
|
4
|
+
if (location.protocol !== "file:") return;
|
|
5
|
+
if (!navigator.userAgent || navigator.userAgent.indexOf("Firefox/") === -1) return;
|
|
6
|
+
if (window.__HQCC_URL_PATCHED__) return;
|
|
7
|
+
window.__HQCC_URL_PATCHED__ = true;
|
|
8
|
+
|
|
9
|
+
var OriginalURL = URL;
|
|
10
|
+
function PatchedURL(url, base) {
|
|
11
|
+
if (base == null || base === "null") {
|
|
12
|
+
base = window.location.href;
|
|
13
|
+
}
|
|
14
|
+
return new OriginalURL(url, base);
|
|
15
|
+
}
|
|
16
|
+
PatchedURL.prototype = OriginalURL.prototype;
|
|
17
|
+
Object.getOwnPropertyNames(OriginalURL).forEach(function (prop) {
|
|
18
|
+
try {
|
|
19
|
+
if (!(prop in PatchedURL)) {
|
|
20
|
+
Object.defineProperty(PatchedURL, prop, Object.getOwnPropertyDescriptor(OriginalURL, prop));
|
|
21
|
+
}
|
|
22
|
+
} catch (_err) {}
|
|
23
|
+
});
|
|
24
|
+
Object.getOwnPropertySymbols(OriginalURL).forEach(function (prop) {
|
|
25
|
+
try {
|
|
26
|
+
if (!(prop in PatchedURL)) {
|
|
27
|
+
Object.defineProperty(PatchedURL, prop, Object.getOwnPropertyDescriptor(OriginalURL, prop));
|
|
28
|
+
}
|
|
29
|
+
} catch (_err) {}
|
|
30
|
+
});
|
|
31
|
+
window.URL = PatchedURL;
|
|
32
|
+
} catch (_err) {}
|
|
33
|
+
})();
|
|
34
|
+
</script><style>
|
|
35
|
+
@font-face {
|
|
36
|
+
font-family: "Carter Sans W01";
|
|
37
|
+
src: url("./fonts/Carter Sans W01 Regular.ttf") format("truetype");
|
|
38
|
+
font-style: normal;
|
|
39
|
+
font-weight: 400;
|
|
40
|
+
font-display: swap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@font-face {
|
|
44
|
+
font-family: "Carter Sans W01";
|
|
45
|
+
src: url("./fonts/Carter Sans W01 Medium.ttf") format("truetype");
|
|
46
|
+
font-style: normal;
|
|
47
|
+
font-weight: 550;
|
|
48
|
+
font-display: swap;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@font-face {
|
|
52
|
+
font-family: "Carter Sans W01";
|
|
53
|
+
src: url("./fonts/Carter Sans W01 Bold.ttf") format("truetype");
|
|
54
|
+
font-style: normal;
|
|
55
|
+
font-weight: 700;
|
|
56
|
+
font-display: swap;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@font-face {
|
|
60
|
+
font-family: "HeroQuest";
|
|
61
|
+
src: url("./fonts/HeroQuest.ttf") format("truetype");
|
|
62
|
+
font-style: normal;
|
|
63
|
+
font-weight: 400;
|
|
64
|
+
font-display: swap;
|
|
65
|
+
}
|
|
66
|
+
</style><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script src="./_next/static/chunks/webpack-498a638590fe54e0.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"./_next/static/css/d071dca61fbb01bf.css\",\"style\"]\n2:HL[\"./_next/static/css/fc205991348f38b9.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"3:I[2846,[],\"\"]\n5:I[4707,[],\"\"]\n6:I[6423,[],\"\"]\n7:I[8087,[\"275\",\"static/chunks/275-f9dcc3143453fdcd.js\",\"481\",\"static/chunks/481-657f5adb2ca2e132.js\",\"185\",\"static/chunks/app/layout-280540b43bd5178c.js\"],\"GoogleTagManager\"]\n9:I[2672,[\"275\",\"static/chunks/275-f9dcc3143453fdcd.js\",\"481\",\"static/chunks/481-657f5adb2ca2e132.js\",\"185\",\"static/chunks/app/layout-280540b43bd5178c.js\"],\"default\"]\nf:I[1060,[],\"\"]\n8:T445,\n(function () {\n try {\n if (location.protocol !== \"file:\") return;\n if (!navigator.userAgent || navigator.userAgent.indexOf(\"Firefox/\") === -1) return;\n if (window.__HQCC_URL_PATCHED__) return;\n window.__HQCC_URL_PATCHED__ = true;\n\n var OriginalURL = URL;\n function PatchedURL(url, base) {\n if (base == null || base === \"null\") {\n base = window.location.href;\n }\n return new OriginalURL(url, base);\n }\n PatchedURL.prototype = OriginalURL.prototype;\n Object.getOwnPropertyNames(OriginalURL).forEach(function (prop) {\n try {\n if (!(prop in PatchedURL)) {\n Object.defineProperty(PatchedURL, prop, Object.getOwnPropertyDescriptor(OriginalURL, prop));\n }\n } catch (_err) {}\n });\n Object.getOwnPropertySymbols(OriginalURL).forEach(function (prop) {\n try {\n if (!(prop in PatchedURL)) {\n Object.defineProperty(PatchedURL, prop, Object.getOwnPropertyDescriptor(OriginalURL, prop));\n }\n } catch (_err) {}\n });\n window.URL = PatchedURL;\n } catch (_err) {}\n})();\n a:{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"}\nb:{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"}\nc:{\"display\":\"inline-block\"}\nd:{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0}\n10:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L3\",null,{\"buildId\":\"lmyD8fTLUGVhT66l1Dl08\",\"assetPrefix\":\".\",\"urlParts\":[\"\",\"_not-found\",\"\"],\"initialTree\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{},[[\"$L4\",[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null],null],null]},[null,[\"$\",\"$L5\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"/_not-found\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L6\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"./_next/static/css/d071dca61fbb01bf.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"./_next/static/css/fc205991348f38b9.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[[\"$\",\"$L7\",null,{\"gtmId\":\"G-KSVRMQ5CPG\"}],[\"$\",\"body\",null,{\"children\":[[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"$8\"}}],[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"\\n@font-face {\\n font-family: \\\"Carter Sans W01\\\";\\n src: url(\\\"./fonts/Carter Sans W01 Regular.ttf\\\") format(\\\"truetype\\\");\\n font-style: normal;\\n font-weight: 400;\\n font-display: swap;\\n}\\n\\n@font-face {\\n font-family: \\\"Carter Sans W01\\\";\\n src: url(\\\"./fonts/Carter Sans W01 Medium.ttf\\\") format(\\\"truetype\\\");\\n font-style: normal;\\n font-weight: 550;\\n font-display: swap;\\n}\\n\\n@font-face {\\n font-family: \\\"Carter Sans W01\\\";\\n src: url(\\\"./fonts/Carter Sans W01 Bold.ttf\\\") format(\\\"truetype\\\");\\n font-style: normal;\\n font-weight: 700;\\n font-display: swap;\\n}\\n\\n@font-face {\\n font-family: \\\"HeroQuest\\\";\\n src: url(\\\"./fonts/HeroQuest.ttf\\\") format(\\\"truetype\\\");\\n font-style: normal;\\n font-weight: 400;\\n font-display: swap;\\n}\\n \"}}],[\"$\",\"$L9\",null,{\"children\":[\"$\",\"$L5\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L6\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$a\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$b\",\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":\"$c\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$d\",\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]]}]]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],\"$Le\"],\"globalErrorComponent\":\"$f\",\"missingSlots\":\"$W10\"}]\n"])</script><script>self.__next_f.push([1,"e:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"name\":\"theme-color\",\"content\":\"#375f8a\"}],[\"$\",\"meta\",\"2\",{\"name\":\"color-scheme\",\"content\":\"light dark\"}],[\"$\",\"meta\",\"3\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"4\",{\"children\":\"HeroQuest Card Creator\"}],[\"$\",\"meta\",\"5\",{\"name\":\"description\",\"content\":\"A browser-based tool for creating custom HeroQuest-style cards.\"}],[\"$\",\"meta\",\"6\",{\"name\":\"author\",\"content\":\"HeroQuest Card Creator\"}],[\"$\",\"meta\",\"7\",{\"name\":\"keywords\",\"content\":\"HeroQuest,card creator,card maker,tabletop,board game,print and play\"}],[\"$\",\"link\",\"8\",{\"rel\":\"canonical\",\"href\":\"https://public.markforster.info/Heroquest/Tools/card-maker/\"}],[\"$\",\"meta\",\"9\",{\"property\":\"og:title\",\"content\":\"HeroQuest Card Creator\"}],[\"$\",\"meta\",\"10\",{\"property\":\"og:description\",\"content\":\"Create and export custom HeroQuest-style cards directly in your browser.\"}],[\"$\",\"meta\",\"11\",{\"property\":\"og:url\",\"content\":\"https://public.markforster.info/Heroquest/Tools/card-maker/\"}],[\"$\",\"meta\",\"12\",{\"property\":\"og:site_name\",\"content\":\"HeroQuest Card Creator\"}],[\"$\",\"meta\",\"13\",{\"property\":\"og:locale\",\"content\":\"en_GB\"}],[\"$\",\"meta\",\"14\",{\"property\":\"og:image\",\"content\":\"https://public.markforster.info/Heroquest/Tools/card-maker/images/IMG_2912.jpg\"}],[\"$\",\"meta\",\"15\",{\"property\":\"og:type\",\"content\":\"website\"}],[\"$\",\"meta\",\"16\",{\"name\":\"twitter:card\",\"content\":\"summary_large_image\"}],[\"$\",\"meta\",\"17\",{\"name\":\"twitter:title\",\"content\":\"HeroQuest Card Creator\"}],[\"$\",\"meta\",\"18\",{\"name\":\"twitter:description\",\"content\":\"Create and export custom HeroQuest-style cards directly in your browser.\"}],[\"$\",\"meta\",\"19\",{\"name\":\"twitter:image\",\"content\":\"https://public.markforster.info/Heroquest/Tools/card-maker/images/IMG_2912.jpg\"}],[\"$\",\"link\",\"20\",{\"rel\":\"shortcut icon\",\"href\":\"./favicon.ico\"}],[\"$\",\"link\",\"21\",{\"rel\":\"icon\",\"href\":\"./favicon.ico\"}],[\"$\",\"link\",\"22\",{\"rel\":\"apple-touch-icon\",\"href\":\"./favicon.ico\"}]]\n4:null\n"])</script></body></html>
|