@joystyk/launcher 0.0.0 → 0.2.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +245 -2
  3. package/dist/browser/daily-prefetch.d.ts +8 -0
  4. package/dist/browser/daily-prefetch.js +32 -0
  5. package/dist/browser/daily-prefetch.js.map +1 -0
  6. package/dist/browser/index.d.ts +3 -0
  7. package/dist/browser/index.js +4 -0
  8. package/dist/browser/rooms.d.ts +2 -0
  9. package/dist/browser/rooms.js +49 -0
  10. package/dist/browser/rooms.js.map +1 -0
  11. package/dist/browser/services.d.ts +127 -0
  12. package/dist/browser/services.js +95 -0
  13. package/dist/browser/services.js.map +1 -0
  14. package/dist/browser/stage.d.ts +28 -0
  15. package/dist/browser/stage.js +1195 -0
  16. package/dist/browser/stage.js.map +1 -0
  17. package/dist/fixture/engine.d.ts +60 -0
  18. package/dist/fixture/engine.js +164 -0
  19. package/dist/fixture/engine.js.map +1 -0
  20. package/dist/fixture/index.d.ts +52 -0
  21. package/dist/fixture/index.js +63 -0
  22. package/dist/fixture/index.js.map +1 -0
  23. package/dist/fixture/module.d.ts +2 -0
  24. package/dist/fixture/module.js +135 -0
  25. package/dist/fixture/module.js.map +1 -0
  26. package/dist/fixture/probe.d.ts +10 -0
  27. package/dist/fixture/probe.js +19 -0
  28. package/dist/fixture/probe.js.map +1 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.js +4 -0
  31. package/dist/launcher.d.ts +190 -0
  32. package/dist/launcher.js +4614 -0
  33. package/dist/launcher.js.map +1 -0
  34. package/dist/model.d.ts +431 -0
  35. package/dist/model.js +566 -0
  36. package/dist/model.js.map +1 -0
  37. package/dist/report-problem.d.ts +39 -0
  38. package/dist/report-problem.js +359 -0
  39. package/dist/report-problem.js.map +1 -0
  40. package/dist/report.d.ts +18 -0
  41. package/dist/report.js +32 -0
  42. package/dist/report.js.map +1 -0
  43. package/dist/room-drivers.d.ts +26 -0
  44. package/dist/room-drivers.js +92 -0
  45. package/dist/room-drivers.js.map +1 -0
  46. package/dist/safe-point.d.ts +33 -0
  47. package/dist/safe-point.js +44 -0
  48. package/dist/safe-point.js.map +1 -0
  49. package/dist/save-slots.d.ts +6 -0
  50. package/dist/save-slots.js +18 -0
  51. package/dist/save-slots.js.map +1 -0
  52. package/dist/theme.d.ts +35 -0
  53. package/dist/theme.js +111 -0
  54. package/dist/theme.js.map +1 -0
  55. package/package.json +67 -11
@@ -0,0 +1,26 @@
1
+ import { type Command } from '@gyral/core';
2
+ import { type OnlineRooms } from './model.js';
3
+ /**
4
+ * `GET /api/rooms/games` on this page's origin, where Traefik routes the room server
5
+ * (room-server.md section 5.4). Any failure, a refused answer or a slow one, is Unreachable:
6
+ * Online with friends shows as unavailable and the local modes play on.
7
+ */
8
+ export declare const onlineRoomsDriver: import("@gyral/core").Driver<undefined, OnlineRooms, unknown>;
9
+ export declare const checkOnline: <M>(toMsg: (online: OnlineRooms) => M) => Command<M>;
10
+ /** The `?room=CODE` of the page's URL, or null. */
11
+ export declare const inviteDriver: import("@gyral/core").Driver<undefined, string | null, unknown>;
12
+ /** Reads the invite once the page is live; a page without one sends nothing. */
13
+ export declare const readInvite: <M>(toMsg: (code: string) => M) => Command<M>;
14
+ /** Puts the room's code in the address bar (`?room=CODE`), or takes it out, without a navigation. */
15
+ export declare const roomUrlDriver: import("@gyral/core").Driver<string | null, void, unknown>;
16
+ export declare const showRoomInUrl: <M>(code: string | null) => Command<M>;
17
+ /** The clipboard. It fails without a user gesture or permission; the caller says so. */
18
+ export declare const clipboardDriver: import("@gyral/core").Driver<string, void, string>;
19
+ export declare const copyText: <M>(text: string, toMsg: (ok: boolean) => M) => Command<M>;
20
+ /**
21
+ * Daily share text (content-pipeline.md, "Share text"): the Web Share API where the device
22
+ * offers it, otherwise the clipboard. Answers how it went: shared, copied, or cancelled when
23
+ * the player closed the share sheet. Fails when neither works; the caller says so.
24
+ */
25
+ export declare const shareDriver: import("@gyral/core").Driver<string, "shared" | "copied" | "cancelled", string>;
26
+ export declare const shareText: <M>(text: string, toMsg: (how: "shared" | "copied" | "cancelled" | "failed") => M) => Command<M>;
@@ -0,0 +1,92 @@
1
+ import { ONLINE_UNREACHABLE, parseHostedGames, parseRoomCode } from "./model.js";
2
+ import { command, defineDriver } from "@gyral/core";
3
+ //#region src/room-drivers.ts
4
+ /** How long the launcher waits for the room server's answer before calling it unreachable. */
5
+ var ONLINE_TIMEOUT_MS = 1e4;
6
+ /**
7
+ * `GET /api/rooms/games` on this page's origin, where Traefik routes the room server
8
+ * (room-server.md section 5.4). Any failure, a refused answer or a slow one, is Unreachable:
9
+ * Online with friends shows as unavailable and the local modes play on.
10
+ */
11
+ var onlineRoomsDriver = defineDriver({
12
+ name: "launcher-online-rooms",
13
+ run: async (_input, { signal }) => {
14
+ try {
15
+ const response = await fetch(new URL("/api/rooms/games", location.origin), {
16
+ headers: { accept: "application/json" },
17
+ signal: AbortSignal.any([signal, AbortSignal.timeout(ONLINE_TIMEOUT_MS)])
18
+ });
19
+ if (!response.ok) return ONLINE_UNREACHABLE;
20
+ return parseHostedGames(await response.json()) ?? ONLINE_UNREACHABLE;
21
+ } catch {
22
+ return ONLINE_UNREACHABLE;
23
+ }
24
+ },
25
+ concurrency: "exhaust"
26
+ });
27
+ var checkOnline = (toMsg) => command(onlineRoomsDriver, void 0, {
28
+ onSuccess: toMsg,
29
+ onFailure: () => toMsg(ONLINE_UNREACHABLE)
30
+ });
31
+ /** The `?room=CODE` of the page's URL, or null. */
32
+ var inviteDriver = defineDriver({
33
+ name: "launcher-invite",
34
+ run: () => {
35
+ const raw = new URL(location.href).searchParams.get("room");
36
+ return raw === null ? null : parseRoomCode(raw);
37
+ }
38
+ });
39
+ /** Reads the invite once the page is live; a page without one sends nothing. */
40
+ var readInvite = (toMsg) => command(inviteDriver, void 0, { onSuccess: (code) => code === null ? void 0 : toMsg(code) });
41
+ /** Puts the room's code in the address bar (`?room=CODE`), or takes it out, without a navigation. */
42
+ var roomUrlDriver = defineDriver({
43
+ name: "launcher-room-url",
44
+ run: (code) => {
45
+ const url = new URL(location.href);
46
+ if (code === null) url.searchParams.delete("room");
47
+ else url.searchParams.set("room", code);
48
+ if (url.href !== location.href) history.replaceState(history.state, "", url);
49
+ },
50
+ concurrency: "queue"
51
+ });
52
+ var showRoomInUrl = (code) => command(roomUrlDriver, code, { onFailure: () => void 0 });
53
+ /** The clipboard. It fails without a user gesture or permission; the caller says so. */
54
+ var clipboardDriver = defineDriver({
55
+ name: "launcher-clipboard",
56
+ run: async (text) => {
57
+ await navigator.clipboard.writeText(text);
58
+ },
59
+ toError: (cause) => cause instanceof Error ? cause.message : String(cause)
60
+ });
61
+ var copyText = (text, toMsg) => command(clipboardDriver, text, {
62
+ onSuccess: () => toMsg(true),
63
+ onFailure: () => toMsg(false)
64
+ });
65
+ /**
66
+ * Daily share text (content-pipeline.md, "Share text"): the Web Share API where the device
67
+ * offers it, otherwise the clipboard. Answers how it went: shared, copied, or cancelled when
68
+ * the player closed the share sheet. Fails when neither works; the caller says so.
69
+ */
70
+ var shareDriver = defineDriver({
71
+ name: "launcher-share",
72
+ run: async (text) => {
73
+ if (typeof navigator.share === "function" && (navigator.canShare?.({ text }) ?? true)) try {
74
+ await navigator.share({ text });
75
+ return "shared";
76
+ } catch (cause) {
77
+ if (cause instanceof DOMException && cause.name === "AbortError") return "cancelled";
78
+ }
79
+ await navigator.clipboard.writeText(text);
80
+ return "copied";
81
+ },
82
+ concurrency: "exhaust",
83
+ toError: (cause) => cause instanceof Error ? cause.message : String(cause)
84
+ });
85
+ var shareText = (text, toMsg) => command(shareDriver, text, {
86
+ onSuccess: toMsg,
87
+ onFailure: () => toMsg("failed")
88
+ });
89
+ //#endregion
90
+ export { checkOnline, clipboardDriver, copyText, inviteDriver, onlineRoomsDriver, readInvite, roomUrlDriver, shareDriver, shareText, showRoomInUrl };
91
+
92
+ //# sourceMappingURL=room-drivers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-drivers.js","names":[],"sources":["../src/room-drivers.ts"],"sourcesContent":["// The launcher's room effects as Gyral drivers: asking the room server which games it hosts,\n// reading an invite code from the page's URL, keeping the room's code in the address bar (so a\n// reload rejoins and the link can be shared from there), and copying the invite. Reducers stay\n// pure; tests substitute the drivers by name.\nimport { command, defineDriver, type Command } from '@gyral/core';\nimport { ONLINE_UNREACHABLE, parseHostedGames, parseRoomCode, type OnlineRooms } from './model.ts';\n\n/** How long the launcher waits for the room server's answer before calling it unreachable. */\nconst ONLINE_TIMEOUT_MS = 10_000;\n\n/**\n * `GET /api/rooms/games` on this page's origin, where Traefik routes the room server\n * (room-server.md section 5.4). Any failure, a refused answer or a slow one, is Unreachable:\n * Online with friends shows as unavailable and the local modes play on.\n */\nexport const onlineRoomsDriver = defineDriver<undefined, OnlineRooms>({\n name: 'launcher-online-rooms',\n run: async (_input, { signal }) => {\n try {\n const response = await fetch(new URL('/api/rooms/games', location.origin), {\n headers: { accept: 'application/json' },\n signal: AbortSignal.any([signal, AbortSignal.timeout(ONLINE_TIMEOUT_MS)]),\n });\n if (!response.ok) return ONLINE_UNREACHABLE;\n return parseHostedGames(await response.json()) ?? ONLINE_UNREACHABLE;\n } catch {\n return ONLINE_UNREACHABLE;\n }\n },\n // One question at a time: a second ask while one is out is dropped.\n concurrency: 'exhaust',\n});\n\nexport const checkOnline = <M>(toMsg: (online: OnlineRooms) => M): Command<M> =>\n command<undefined, OnlineRooms, unknown, M>(onlineRoomsDriver, undefined, {\n onSuccess: toMsg,\n onFailure: () => toMsg(ONLINE_UNREACHABLE),\n });\n\n/** The `?room=CODE` of the page's URL, or null. */\nexport const inviteDriver = defineDriver<undefined, string | null>({\n name: 'launcher-invite',\n run: () => {\n const raw = new URL(location.href).searchParams.get('room');\n return raw === null ? null : parseRoomCode(raw);\n },\n});\n\n/** Reads the invite once the page is live; a page without one sends nothing. */\nexport const readInvite = <M>(toMsg: (code: string) => M): Command<M> =>\n command<undefined, string | null, unknown, M>(inviteDriver, undefined, {\n onSuccess: (code) => (code === null ? undefined : toMsg(code)),\n });\n\n/** Puts the room's code in the address bar (`?room=CODE`), or takes it out, without a navigation. */\nexport const roomUrlDriver = defineDriver<string | null, void>({\n name: 'launcher-room-url',\n run: (code) => {\n const url = new URL(location.href);\n if (code === null) url.searchParams.delete('room');\n else url.searchParams.set('room', code);\n if (url.href !== location.href) history.replaceState(history.state, '', url);\n },\n concurrency: 'queue',\n});\n\nexport const showRoomInUrl = <M>(code: string | null): Command<M> =>\n command<string | null, void, unknown, M>(roomUrlDriver, code, { onFailure: () => undefined });\n\n/** The clipboard. It fails without a user gesture or permission; the caller says so. */\nexport const clipboardDriver = defineDriver<string, void, string>({\n name: 'launcher-clipboard',\n run: async (text) => {\n await navigator.clipboard.writeText(text);\n },\n toError: (cause) => (cause instanceof Error ? cause.message : String(cause)),\n});\n\nexport const copyText = <M>(text: string, toMsg: (ok: boolean) => M): Command<M> =>\n command<string, void, string, M>(clipboardDriver, text, {\n onSuccess: () => toMsg(true),\n onFailure: () => toMsg(false),\n });\n\n/**\n * Daily share text (content-pipeline.md, \"Share text\"): the Web Share API where the device\n * offers it, otherwise the clipboard. Answers how it went: shared, copied, or cancelled when\n * the player closed the share sheet. Fails when neither works; the caller says so.\n */\nexport const shareDriver = defineDriver<string, 'shared' | 'copied' | 'cancelled', string>({\n name: 'launcher-share',\n run: async (text) => {\n if (typeof navigator.share === 'function' && (navigator.canShare?.({ text }) ?? true)) {\n try {\n await navigator.share({ text });\n return 'shared';\n } catch (cause) {\n if (cause instanceof DOMException && cause.name === 'AbortError') return 'cancelled';\n }\n }\n await navigator.clipboard.writeText(text);\n return 'copied';\n },\n concurrency: 'exhaust',\n toError: (cause) => (cause instanceof Error ? cause.message : String(cause)),\n});\n\nexport const shareText = <M>(\n text: string,\n toMsg: (how: 'shared' | 'copied' | 'cancelled' | 'failed') => M,\n): Command<M> =>\n command<string, 'shared' | 'copied' | 'cancelled', string, M>(shareDriver, text, {\n onSuccess: toMsg,\n onFailure: () => toMsg('failed'),\n });\n"],"mappings":";;;;AAQA,IAAM,oBAAoB;;;;;;AAO1B,IAAa,oBAAoB,aAAqC;CACpE,MAAM;CACN,KAAK,OAAO,QAAQ,EAAE,aAAa;EACjC,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,IAAI,IAAI,oBAAoB,SAAS,MAAM,GAAG;IACzE,SAAS,EAAE,QAAQ,mBAAmB;IACtC,QAAQ,YAAY,IAAI,CAAC,QAAQ,YAAY,QAAQ,iBAAiB,CAAC,CAAC;GAC1E,CAAC;GACD,IAAI,CAAC,SAAS,IAAI,OAAO;GACzB,OAAO,iBAAiB,MAAM,SAAS,KAAK,CAAC,KAAK;EACpD,QAAQ;GACN,OAAO;EACT;CACF;CAEA,aAAa;AACf,CAAC;AAED,IAAa,eAAkB,UAC7B,QAA4C,mBAAmB,KAAA,GAAW;CACxE,WAAW;CACX,iBAAiB,MAAM,kBAAkB;AAC3C,CAAC;;AAGH,IAAa,eAAe,aAAuC;CACjE,MAAM;CACN,WAAW;EACT,MAAM,MAAM,IAAI,IAAI,SAAS,IAAI,CAAC,CAAC,aAAa,IAAI,MAAM;EAC1D,OAAO,QAAQ,OAAO,OAAO,cAAc,GAAG;CAChD;AACF,CAAC;;AAGD,IAAa,cAAiB,UAC5B,QAA8C,cAAc,KAAA,GAAW,EACrE,YAAY,SAAU,SAAS,OAAO,KAAA,IAAY,MAAM,IAAI,EAC9D,CAAC;;AAGH,IAAa,gBAAgB,aAAkC;CAC7D,MAAM;CACN,MAAM,SAAS;EACb,MAAM,MAAM,IAAI,IAAI,SAAS,IAAI;EACjC,IAAI,SAAS,MAAM,IAAI,aAAa,OAAO,MAAM;OAC5C,IAAI,aAAa,IAAI,QAAQ,IAAI;EACtC,IAAI,IAAI,SAAS,SAAS,MAAM,QAAQ,aAAa,QAAQ,OAAO,IAAI,GAAG;CAC7E;CACA,aAAa;AACf,CAAC;AAED,IAAa,iBAAoB,SAC/B,QAAyC,eAAe,MAAM,EAAE,iBAAiB,KAAA,EAAU,CAAC;;AAG9F,IAAa,kBAAkB,aAAmC;CAChE,MAAM;CACN,KAAK,OAAO,SAAS;EACnB,MAAM,UAAU,UAAU,UAAU,IAAI;CAC1C;CACA,UAAU,UAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC5E,CAAC;AAED,IAAa,YAAe,MAAc,UACxC,QAAiC,iBAAiB,MAAM;CACtD,iBAAiB,MAAM,IAAI;CAC3B,iBAAiB,MAAM,KAAK;AAC9B,CAAC;;;;;;AAOH,IAAa,cAAc,aAAgE;CACzF,MAAM;CACN,KAAK,OAAO,SAAS;EACnB,IAAI,OAAO,UAAU,UAAU,eAAe,UAAU,WAAW,EAAE,KAAK,CAAC,KAAK,OAC9E,IAAI;GACF,MAAM,UAAU,MAAM,EAAE,KAAK,CAAC;GAC9B,OAAO;EACT,SAAS,OAAO;GACd,IAAI,iBAAiB,gBAAgB,MAAM,SAAS,cAAc,OAAO;EAC3E;EAEF,MAAM,UAAU,UAAU,UAAU,IAAI;EACxC,OAAO;CACT;CACA,aAAa;CACb,UAAU,UAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC5E,CAAC;AAED,IAAa,aACX,MACA,UAEA,QAA8D,aAAa,MAAM;CAC/E,WAAW;CACX,iBAAiB,MAAM,QAAQ;AACjC,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { type Command } from '@gyral/core';
2
+ export declare const SAFE_POINT_EVENT = "joystyk:safe-point";
3
+ export type SafePointDetail = {
4
+ /** True when an update or a reload would lose nothing. */
5
+ readonly safe: boolean;
6
+ /** The game the launcher is showing. */
7
+ readonly gameId: string;
8
+ };
9
+ /** Reports a change of safe point. Substitute it by name ('safe-point') in tests. */
10
+ export declare const safePointDriver: import("@gyral/core").Driver<SafePointDetail, void, unknown>;
11
+ export declare const reportSafePoint: <M>(detail: SafePointDetail) => Command<M>;
12
+ /**
13
+ * Calls `listener` with every safe-point report on the page (the PWA's update prompt).
14
+ * Until a launcher reports, a page without one is a safe point. Returns how to stop.
15
+ */
16
+ export declare const watchSafePoints: (listener: (detail: SafePointDetail) => void, target?: EventTarget) => (() => void);
17
+ /**
18
+ * A forced security update (offline-and-updates.md, Rule 2) can't wait for the game to reach
19
+ * a safe point, so it asks for one: every game stage on the page stops play and writes its
20
+ * position (the last completed move, or a paused checkpoint of a real-time game), then hands
21
+ * the write to `waitUntil`. Rooms and games that save themselves have nothing to add.
22
+ */
23
+ export declare const SAVE_REQUEST_EVENT = "joystyk:save-request";
24
+ export type SaveRequestDetail = {
25
+ /** Holds the request open until `work` settles. */
26
+ readonly waitUntil: (work: Promise<unknown>) => void;
27
+ };
28
+ /**
29
+ * Asks every game on the page to save now, for a forced update. Resolves once each save has
30
+ * landed or failed; at once when no game is in play. Play stays paused afterwards: the page
31
+ * is about to reload.
32
+ */
33
+ export declare const requestSave: (target?: EventTarget) => Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { command, defineDriver } from "@gyral/core";
2
+ //#region src/safe-point.ts
3
+ var SAFE_POINT_EVENT = "joystyk:safe-point";
4
+ /** Reports a change of safe point. Substitute it by name ('safe-point') in tests. */
5
+ var safePointDriver = defineDriver({
6
+ name: "safe-point",
7
+ run: (detail) => {
8
+ document.dispatchEvent(new CustomEvent(SAFE_POINT_EVENT, { detail }));
9
+ }
10
+ });
11
+ var reportSafePoint = (detail) => command(safePointDriver, detail, {});
12
+ /**
13
+ * Calls `listener` with every safe-point report on the page (the PWA's update prompt).
14
+ * Until a launcher reports, a page without one is a safe point. Returns how to stop.
15
+ */
16
+ var watchSafePoints = (listener, target = document) => {
17
+ const handler = (event) => {
18
+ const detail = event.detail;
19
+ if (typeof detail === "object" && detail !== null && typeof detail.safe === "boolean") listener(detail);
20
+ };
21
+ target.addEventListener(SAFE_POINT_EVENT, handler);
22
+ return () => target.removeEventListener(SAFE_POINT_EVENT, handler);
23
+ };
24
+ /**
25
+ * A forced security update (offline-and-updates.md, Rule 2) can't wait for the game to reach
26
+ * a safe point, so it asks for one: every game stage on the page stops play and writes its
27
+ * position (the last completed move, or a paused checkpoint of a real-time game), then hands
28
+ * the write to `waitUntil`. Rooms and games that save themselves have nothing to add.
29
+ */
30
+ var SAVE_REQUEST_EVENT = "joystyk:save-request";
31
+ /**
32
+ * Asks every game on the page to save now, for a forced update. Resolves once each save has
33
+ * landed or failed; at once when no game is in play. Play stays paused afterwards: the page
34
+ * is about to reload.
35
+ */
36
+ var requestSave = async (target = document) => {
37
+ const pending = [];
38
+ target.dispatchEvent(new CustomEvent(SAVE_REQUEST_EVENT, { detail: { waitUntil: (work) => pending.push(work) } }));
39
+ await Promise.allSettled(pending);
40
+ };
41
+ //#endregion
42
+ export { SAFE_POINT_EVENT, SAVE_REQUEST_EVENT, reportSafePoint, requestSave, safePointDriver, watchSafePoints };
43
+
44
+ //# sourceMappingURL=safe-point.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-point.js","names":[],"sources":["../src/safe-point.ts"],"sourcesContent":["// Safe points (offline-and-updates.md, Rule 2): the page may offer an update only where\n// nothing can be lost: outside play, on the results screen, or during play when the session\n// says so (`meta.safePoint`, which defaults to \"the game is over\"). The launcher reports every\n// change as a `joystyk:safe-point` event on the document; the PWA listens with\n// `watchSafePoints` and holds its update prompt until `safe` is true.\nimport { command, defineDriver, type Command } from '@gyral/core';\n\nexport const SAFE_POINT_EVENT = 'joystyk:safe-point';\n\nexport type SafePointDetail = {\n /** True when an update or a reload would lose nothing. */\n readonly safe: boolean;\n /** The game the launcher is showing. */\n readonly gameId: string;\n};\n\n/** Reports a change of safe point. Substitute it by name ('safe-point') in tests. */\nexport const safePointDriver = defineDriver<SafePointDetail, void>({\n name: 'safe-point',\n run: (detail) => {\n document.dispatchEvent(new CustomEvent<SafePointDetail>(SAFE_POINT_EVENT, { detail }));\n },\n});\n\nexport const reportSafePoint = <M>(detail: SafePointDetail): Command<M> =>\n command<SafePointDetail, void, unknown, M>(safePointDriver, detail, {});\n\n/**\n * Calls `listener` with every safe-point report on the page (the PWA's update prompt).\n * Until a launcher reports, a page without one is a safe point. Returns how to stop.\n */\nexport const watchSafePoints = (\n listener: (detail: SafePointDetail) => void,\n target: EventTarget = document,\n): (() => void) => {\n const handler = (event: Event): void => {\n const detail = (event as CustomEvent<unknown>).detail;\n if (typeof detail === 'object' && detail !== null && typeof (detail as SafePointDetail).safe === 'boolean') {\n listener(detail as SafePointDetail);\n }\n };\n target.addEventListener(SAFE_POINT_EVENT, handler);\n return () => target.removeEventListener(SAFE_POINT_EVENT, handler);\n};\n\n/**\n * A forced security update (offline-and-updates.md, Rule 2) can't wait for the game to reach\n * a safe point, so it asks for one: every game stage on the page stops play and writes its\n * position (the last completed move, or a paused checkpoint of a real-time game), then hands\n * the write to `waitUntil`. Rooms and games that save themselves have nothing to add.\n */\nexport const SAVE_REQUEST_EVENT = 'joystyk:save-request';\n\nexport type SaveRequestDetail = {\n /** Holds the request open until `work` settles. */\n readonly waitUntil: (work: Promise<unknown>) => void;\n};\n\n/**\n * Asks every game on the page to save now, for a forced update. Resolves once each save has\n * landed or failed; at once when no game is in play. Play stays paused afterwards: the page\n * is about to reload.\n */\nexport const requestSave = async (target: EventTarget = document): Promise<void> => {\n const pending: Promise<unknown>[] = [];\n target.dispatchEvent(\n new CustomEvent<SaveRequestDetail>(SAVE_REQUEST_EVENT, { detail: { waitUntil: (work) => pending.push(work) } }),\n );\n await Promise.allSettled(pending);\n};\n"],"mappings":";;AAOA,IAAa,mBAAmB;;AAUhC,IAAa,kBAAkB,aAAoC;CACjE,MAAM;CACN,MAAM,WAAW;EACf,SAAS,cAAc,IAAI,YAA6B,kBAAkB,EAAE,OAAO,CAAC,CAAC;CACvF;AACF,CAAC;AAED,IAAa,mBAAsB,WACjC,QAA2C,iBAAiB,QAAQ,CAAC,CAAC;;;;;AAMxE,IAAa,mBACX,UACA,SAAsB,aACL;CACjB,MAAM,WAAW,UAAuB;EACtC,MAAM,SAAU,MAA+B;EAC/C,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,OAAQ,OAA2B,SAAS,WAC/F,SAAS,MAAyB;CAEtC;CACA,OAAO,iBAAiB,kBAAkB,OAAO;CACjD,aAAa,OAAO,oBAAoB,kBAAkB,OAAO;AACnE;;;;;;;AAQA,IAAa,qBAAqB;;;;;;AAYlC,IAAa,cAAc,OAAO,SAAsB,aAA4B;CAClF,MAAM,UAA8B,CAAC;CACrC,OAAO,cACL,IAAI,YAA+B,oBAAoB,EAAE,QAAQ,EAAE,YAAY,SAAS,QAAQ,KAAK,IAAI,EAAE,EAAE,CAAC,CAChH;CACA,MAAM,QAAQ,WAAW,OAAO;AAClC"}
@@ -0,0 +1,6 @@
1
+ import type { Validator } from '@joystyk/contracts';
2
+ /** Boundary for an engine's public save description, before it reaches storage or the UI. */
3
+ export declare const saveDescription: Validator<{
4
+ key: string;
5
+ label: string;
6
+ }>;
@@ -0,0 +1,18 @@
1
+ //#region src/save-slots.ts
2
+ /** Boundary for an engine's public save description, before it reaches storage or the UI. */
3
+ var saveDescription = { "~standard": {
4
+ version: 1,
5
+ vendor: "joystyk",
6
+ validate: (raw) => {
7
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { issues: [{ message: "Expected a save description." }] };
8
+ const p = raw;
9
+ return typeof p["key"] === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(p["key"]) && typeof p["label"] === "string" && p["label"].trim().length > 0 && p["label"].length <= 100 ? { value: {
10
+ key: p["key"],
11
+ label: p["label"]
12
+ } } : { issues: [{ message: "Use a safe save key and a short public label." }] };
13
+ }
14
+ } };
15
+ //#endregion
16
+ export { saveDescription };
17
+
18
+ //# sourceMappingURL=save-slots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-slots.js","names":[],"sources":["../src/save-slots.ts"],"sourcesContent":["import type { Validator } from '@joystyk/contracts';\n/** Boundary for an engine's public save description, before it reaches storage or the UI. */\nexport const saveDescription: Validator<{ key: string; label: string }> = {\n '~standard': {\n version: 1,\n vendor: 'joystyk',\n validate: (raw) => {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw))\n return { issues: [{ message: 'Expected a save description.' }] };\n const p = raw as Record<string, unknown>;\n return typeof p['key'] === 'string' &&\n /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(p['key']) &&\n typeof p['label'] === 'string' &&\n p['label'].trim().length > 0 &&\n p['label'].length <= 100\n ? { value: { key: p['key'], label: p['label'] } }\n : { issues: [{ message: 'Use a safe save key and a short public label.' }] };\n },\n },\n};\n"],"mappings":";;AAEA,IAAa,kBAA6D,EACxE,aAAa;CACX,SAAS;CACT,QAAQ;CACR,WAAW,QAAQ;EACjB,IAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,MAAM,QAAQ,GAAG,GAC9D,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,+BAA+B,CAAC,EAAE;EACjE,MAAM,IAAI;EACV,OAAO,OAAO,EAAE,WAAW,YACzB,mCAAmC,KAAK,EAAE,MAAM,KAChD,OAAO,EAAE,aAAa,YACtB,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,SAAS,KAC3B,EAAE,QAAQ,CAAC,UAAU,MACnB,EAAE,OAAO;GAAE,KAAK,EAAE;GAAQ,OAAO,EAAE;EAAS,EAAE,IAC9C,EAAE,QAAQ,CAAC,EAAE,SAAS,gDAAgD,CAAC,EAAE;CAC/E;AACF,EACF"}
@@ -0,0 +1,35 @@
1
+ import type { GameTheme } from '@joystyk/contracts';
2
+ /** The values of the canvas's three theme tokens. */
3
+ export type CanvasTheme = {
4
+ readonly accent: string;
5
+ readonly surface: string;
6
+ readonly ink: string;
7
+ };
8
+ /**
9
+ * The frame's own colors, for a game without a theme or without a surface. The fallbacks are
10
+ * system colors, which also follow the canvas's color scheme together.
11
+ */
12
+ export declare const FRAME_THEME: CanvasTheme;
13
+ /**
14
+ * The surface lightness at which both inks contrast equally; darker surfaces get the light
15
+ * ink. About 0.57: mid-tones are the hardest surfaces to write on.
16
+ */
17
+ export declare const INK_CROSSOVER_L: number;
18
+ /** The OKLCH lightness (0 to 1) and hue (degrees, or null when achromatic or unknown) of a color. */
19
+ export type Lightness = {
20
+ readonly l: number;
21
+ readonly h: number | null;
22
+ };
23
+ /**
24
+ * Reads the lightness of an `oklch()` or hex color. Null for anything else (named colors,
25
+ * `rgb()`, `var()`), which the launcher leaves to CSS.
26
+ */
27
+ export declare const parseLightness: (color: string) => Lightness | null;
28
+ /**
29
+ * A readable ink for a surface: near-black or near-white, whichever contrasts more, tinted
30
+ * with the surface's hue. A surface it can't read gets the same choice made in CSS, through
31
+ * relative color syntax.
32
+ */
33
+ export declare const readableInk: (surface: string) => string;
34
+ /** The canvas's theme tokens for a manifest's theme. */
35
+ export declare const canvasTheme: (theme: GameTheme | undefined) => CanvasTheme;
package/dist/theme.js ADDED
@@ -0,0 +1,111 @@
1
+ //#region src/theme.ts
2
+ /**
3
+ * The frame's own colors, for a game without a theme or without a surface. The fallbacks are
4
+ * system colors, which also follow the canvas's color scheme together.
5
+ */
6
+ var FRAME_THEME = {
7
+ accent: "var(--jy-accent, LinkText)",
8
+ surface: "var(--jy-surface, Canvas)",
9
+ ink: "var(--jy-text, CanvasText)"
10
+ };
11
+ /** The lightness of the inks the launcher picks: near-black and near-white. */
12
+ var DARK_INK_L = .2;
13
+ var LIGHT_INK_L = .97;
14
+ /** Relative luminance of a neutral color with OKLab lightness `l` (OKLab L is the cube root of Y). */
15
+ var luminance = (l) => l ** 3;
16
+ /** The WCAG contrast ratio between two relative luminances. */
17
+ var ratio = (a, b) => (Math.max(a, b) + .05) / (Math.min(a, b) + .05);
18
+ /**
19
+ * The surface lightness at which both inks contrast equally; darker surfaces get the light
20
+ * ink. About 0.57: mid-tones are the hardest surfaces to write on.
21
+ */
22
+ var INK_CROSSOVER_L = Math.cbrt(Math.sqrt((luminance(DARK_INK_L) + .05) * (luminance(LIGHT_INK_L) + .05)) - .05);
23
+ var NUMBER = String.raw`[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?`;
24
+ var OKLCH = new RegExp(String.raw`^oklch\(\s*(none|${NUMBER}%?)\s+(none|${NUMBER}%?)\s+(none|${NUMBER}(?:deg)?)\s*(?:\/\s*[^)]*)?\)$`, "i");
25
+ var HEX = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
26
+ var toLinear = (c) => c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
27
+ /** OKLab lightness of an sRGB color with channels 0 to 1 (Björn Ottosson's matrices). */
28
+ var oklabLightness = (r, g, b) => {
29
+ const [lr, lg, lb] = [
30
+ toLinear(r),
31
+ toLinear(g),
32
+ toLinear(b)
33
+ ];
34
+ const l = Math.cbrt(.4122214708 * lr + .5363325363 * lg + .0514459929 * lb);
35
+ const m = Math.cbrt(.2119034982 * lr + .6806995451 * lg + .1073969566 * lb);
36
+ const s = Math.cbrt(.0883024619 * lr + .2817188376 * lg + .6299787005 * lb);
37
+ return .2104542553 * l + .793617785 * m - .0040720468 * s;
38
+ };
39
+ /**
40
+ * Reads the lightness of an `oklch()` or hex color. Null for anything else (named colors,
41
+ * `rgb()`, `var()`), which the launcher leaves to CSS.
42
+ */
43
+ var parseLightness = (color) => {
44
+ const text = color.trim();
45
+ const oklch = OKLCH.exec(text);
46
+ if (oklch !== null) {
47
+ const [, lightness = "none", chroma = "none", hue = "none"] = oklch;
48
+ const l = lightness === "none" ? 0 : lightness.endsWith("%") ? Number.parseFloat(lightness) / 100 : Number.parseFloat(lightness);
49
+ return {
50
+ l: Math.min(1, Math.max(0, l)),
51
+ h: hue === "none" || (chroma === "none" ? 0 : Number.parseFloat(chroma)) === 0 ? null : Number.parseFloat(hue)
52
+ };
53
+ }
54
+ const hex = HEX.exec(text)?.[1];
55
+ if (hex !== void 0) {
56
+ const [r = 0, g = 0, b = 0] = (hex.length <= 4 ? [...hex].map((d) => d + d) : hex.match(/../g) ?? []).slice(0, 3).map((d) => Number.parseInt(d, 16) / 255);
57
+ return {
58
+ l: oklabLightness(r, g, b),
59
+ h: null
60
+ };
61
+ }
62
+ return null;
63
+ };
64
+ /** An ink of lightness `l`, neutral or faintly tinted with `hue`. */
65
+ var inkOf = (l, hue) => {
66
+ const percent = `${String(Math.round(l * 100))}%`;
67
+ return hue === null ? `oklch(${percent} 0 0)` : `oklch(${percent} ${l < .5 ? "0.02" : "0.01"} ${String(hue)})`;
68
+ };
69
+ /**
70
+ * A readable ink for a surface: near-black or near-white, whichever contrasts more, tinted
71
+ * with the surface's hue. A surface it can't read gets the same choice made in CSS, through
72
+ * relative color syntax.
73
+ */
74
+ var readableInk = (surface) => {
75
+ const parsed = parseLightness(surface);
76
+ if (parsed === null) {
77
+ const threshold = INK_CROSSOVER_L.toFixed(4);
78
+ return `oklch(from ${surface} clamp(${String(DARK_INK_L)}, (${threshold} - l) * 1000, ${String(LIGHT_INK_L)}) 0 0)`;
79
+ }
80
+ const y = luminance(parsed.l);
81
+ return inkOf(ratio(y, luminance(LIGHT_INK_L)) > ratio(y, luminance(DARK_INK_L)) ? LIGHT_INK_L : DARK_INK_L, parsed.h);
82
+ };
83
+ var pair = (color) => typeof color === "string" ? {
84
+ light: color,
85
+ dark: color
86
+ } : color;
87
+ /** One value when both schemes agree, otherwise `light-dark()`, resolved by the canvas's `color-scheme`. */
88
+ var cssValue = ({ light, dark }) => light === dark ? light : `light-dark(${light}, ${dark})`;
89
+ /** The canvas's theme tokens for a manifest's theme. */
90
+ var canvasTheme = (theme) => {
91
+ if (theme === void 0) return FRAME_THEME;
92
+ const accent = cssValue(pair(theme.accent));
93
+ if (theme.surface === void 0) return {
94
+ ...FRAME_THEME,
95
+ accent
96
+ };
97
+ const surface = pair(theme.surface);
98
+ const ink = (theme.ink === void 0 || typeof theme.surface === "string" && typeof theme.ink !== "string" ? null : pair(theme.ink)) ?? {
99
+ light: readableInk(surface.light),
100
+ dark: readableInk(surface.dark)
101
+ };
102
+ return {
103
+ accent,
104
+ surface: cssValue(surface),
105
+ ink: cssValue(ink)
106
+ };
107
+ };
108
+ //#endregion
109
+ export { FRAME_THEME, INK_CROSSOVER_L, canvasTheme, parseLightness, readableInk };
110
+
111
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","names":[],"sources":["../src/theme.ts"],"sourcesContent":["// The theme handoff (game-contract.md, \"Theme\"): the CSS values the stage sets on the canvas\n// as `--game-accent`, `--game-surface` and `--game-ink`. Pure, so it runs in Node tests.\n//\n// The rule is that a surface and its ink always follow the same color scheme. A game's\n// single surface gets a single ink, its own or one picked for contrast; a pair of surfaces\n// gets an ink per scheme; and a game with no surface gets the frame's surface and text\n// tokens, which both follow the player's scheme. The system's `CanvasText` never meets a\n// game's fixed surface: on a light surface in dark mode it would be white on near-white.\nimport type { GameTheme, SchemeColors, ThemeColor } from '@joystyk/contracts';\n\n/** The values of the canvas's three theme tokens. */\nexport type CanvasTheme = {\n readonly accent: string;\n readonly surface: string;\n readonly ink: string;\n};\n\n/**\n * The frame's own colors, for a game without a theme or without a surface. The fallbacks are\n * system colors, which also follow the canvas's color scheme together.\n */\nexport const FRAME_THEME: CanvasTheme = {\n accent: 'var(--jy-accent, LinkText)',\n surface: 'var(--jy-surface, Canvas)',\n ink: 'var(--jy-text, CanvasText)',\n};\n\n/** The lightness of the inks the launcher picks: near-black and near-white. */\nconst DARK_INK_L = 0.2;\nconst LIGHT_INK_L = 0.97;\n\n/** Relative luminance of a neutral color with OKLab lightness `l` (OKLab L is the cube root of Y). */\nconst luminance = (l: number): number => l ** 3;\n\n/** The WCAG contrast ratio between two relative luminances. */\nconst ratio = (a: number, b: number): number => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);\n\n/**\n * The surface lightness at which both inks contrast equally; darker surfaces get the light\n * ink. About 0.57: mid-tones are the hardest surfaces to write on.\n */\nexport const INK_CROSSOVER_L = Math.cbrt(\n Math.sqrt((luminance(DARK_INK_L) + 0.05) * (luminance(LIGHT_INK_L) + 0.05)) - 0.05,\n);\n\n/** The OKLCH lightness (0 to 1) and hue (degrees, or null when achromatic or unknown) of a color. */\nexport type Lightness = { readonly l: number; readonly h: number | null };\n\nconst NUMBER = String.raw`[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:e[+-]?\\d+)?`;\nconst OKLCH = new RegExp(\n String.raw`^oklch\\(\\s*(none|${NUMBER}%?)\\s+(none|${NUMBER}%?)\\s+(none|${NUMBER}(?:deg)?)\\s*(?:\\/\\s*[^)]*)?\\)$`,\n 'i',\n);\nconst HEX = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;\n\nconst toLinear = (c: number): number => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);\n\n/** OKLab lightness of an sRGB color with channels 0 to 1 (Björn Ottosson's matrices). */\nconst oklabLightness = (r: number, g: number, b: number): number => {\n const [lr, lg, lb] = [toLinear(r), toLinear(g), toLinear(b)];\n const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);\n const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);\n const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);\n return 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;\n};\n\n/**\n * Reads the lightness of an `oklch()` or hex color. Null for anything else (named colors,\n * `rgb()`, `var()`), which the launcher leaves to CSS.\n */\nexport const parseLightness = (color: string): Lightness | null => {\n const text = color.trim();\n const oklch = OKLCH.exec(text);\n if (oklch !== null) {\n const [, lightness = 'none', chroma = 'none', hue = 'none'] = oklch;\n const l =\n lightness === 'none'\n ? 0\n : lightness.endsWith('%')\n ? Number.parseFloat(lightness) / 100\n : Number.parseFloat(lightness);\n const c = chroma === 'none' ? 0 : Number.parseFloat(chroma);\n const h = hue === 'none' || c === 0 ? null : Number.parseFloat(hue);\n return { l: Math.min(1, Math.max(0, l)), h };\n }\n const hex = HEX.exec(text)?.[1];\n if (hex !== undefined) {\n const digits = hex.length <= 4 ? [...hex].map((d) => d + d) : (hex.match(/../g) ?? []);\n const [r = 0, g = 0, b = 0] = digits.slice(0, 3).map((d) => Number.parseInt(d, 16) / 255);\n return { l: oklabLightness(r, g, b), h: null };\n }\n return null;\n};\n\n/** An ink of lightness `l`, neutral or faintly tinted with `hue`. */\nconst inkOf = (l: number, hue: number | null): string => {\n const percent = `${String(Math.round(l * 100))}%`;\n return hue === null ? `oklch(${percent} 0 0)` : `oklch(${percent} ${l < 0.5 ? '0.02' : '0.01'} ${String(hue)})`;\n};\n\n/**\n * A readable ink for a surface: near-black or near-white, whichever contrasts more, tinted\n * with the surface's hue. A surface it can't read gets the same choice made in CSS, through\n * relative color syntax.\n */\nexport const readableInk = (surface: string): string => {\n const parsed = parseLightness(surface);\n if (parsed === null) {\n const threshold = INK_CROSSOVER_L.toFixed(4);\n return `oklch(from ${surface} clamp(${String(DARK_INK_L)}, (${threshold} - l) * 1000, ${String(LIGHT_INK_L)}) 0 0)`;\n }\n const y = luminance(parsed.l);\n const light = ratio(y, luminance(LIGHT_INK_L)) > ratio(y, luminance(DARK_INK_L));\n return inkOf(light ? LIGHT_INK_L : DARK_INK_L, parsed.h);\n};\n\nconst pair = (color: ThemeColor): SchemeColors => (typeof color === 'string' ? { light: color, dark: color } : color);\n\n/** One value when both schemes agree, otherwise `light-dark()`, resolved by the canvas's `color-scheme`. */\nconst cssValue = ({ light, dark }: SchemeColors): string => (light === dark ? light : `light-dark(${light}, ${dark})`);\n\n/** The canvas's theme tokens for a manifest's theme. */\nexport const canvasTheme = (theme: GameTheme | undefined): CanvasTheme => {\n if (theme === undefined) return FRAME_THEME;\n const accent = cssValue(pair(theme.accent));\n if (theme.surface === undefined) return { ...FRAME_THEME, accent };\n const surface = pair(theme.surface);\n // A single surface never gets an ink that changes with the scheme.\n const own =\n theme.ink === undefined || (typeof theme.surface === 'string' && typeof theme.ink !== 'string')\n ? null\n : pair(theme.ink);\n const ink = own ?? { light: readableInk(surface.light), dark: readableInk(surface.dark) };\n return { accent, surface: cssValue(surface), ink: cssValue(ink) };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,20 +1,76 @@
1
1
  {
2
2
  "name": "@joystyk/launcher",
3
- "version": "0.0.0",
4
- "description": "Reserved for the first Joystyk library release.",
3
+ "version": "0.2.0",
4
+ "description": "The frame around a game: mode picker, local sessions, mounting, the platform overlay, the hand-off and results screens.",
5
+ "private": false,
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "**/index.[jt]s",
9
+ "**/launcher.[jt]s",
10
+ "**/report-problem.[jt]s",
11
+ "**/browser/stage.[jt]s"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./browser": {
19
+ "types": "./dist/browser/index.d.ts",
20
+ "default": "./dist/browser/index.js"
21
+ },
22
+ "./fixture": {
23
+ "types": "./dist/fixture/index.d.ts",
24
+ "default": "./dist/fixture/index.js"
25
+ },
26
+ "./report": {
27
+ "types": "./dist/report.d.ts",
28
+ "default": "./dist/report.js"
29
+ }
30
+ },
31
+ "dependencies": {
32
+ "@gyral/core": "0.3.1",
33
+ "@joystyk/contracts": "0.2.0",
34
+ "@joystyk/daily": "0.2.0",
35
+ "@joystyk/kit": "0.2.0",
36
+ "@joystyk/identity": "0.2.0",
37
+ "@joystyk/session": "0.2.0",
38
+ "@joystyk/storage": "0.2.0",
39
+ "@joystyk/ui": "0.2.0"
40
+ },
41
+ "devDependencies": {
42
+ "@gyral/testing": "0.3.1",
43
+ "parse5": "8.0.1",
44
+ "typescript": "5.9.3",
45
+ "vite": "8.3.3",
46
+ "vitest": "4.1.11",
47
+ "@joystyk/rooms": "0.2.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "files": [
53
+ "dist",
54
+ "README.md",
55
+ "LICENSE",
56
+ "NOTICE",
57
+ "CHANGELOG.md"
58
+ ],
59
+ "engines": {
60
+ "node": ">=24.15.0"
61
+ },
5
62
  "license": "MIT",
6
63
  "repository": {
7
64
  "type": "git",
8
65
  "url": "git+https://github.com/joystyk-games/game-libraries.git",
9
66
  "directory": "packages/launcher"
10
67
  },
11
- "homepage": "https://github.com/joystyk-games/game-libraries",
12
- "files": [
13
- "README.md",
14
- "LICENSE",
15
- "NOTICE"
16
- ],
17
- "publishConfig": {
18
- "access": "public"
68
+ "homepage": "https://github.com/joystyk-games/game-libraries/tree/main/packages/launcher#readme",
69
+ "bugs": {
70
+ "url": "https://github.com/joystyk-games/game-libraries/issues"
71
+ },
72
+ "scripts": {
73
+ "typecheck": "tsc --noEmit",
74
+ "build": "node ../../scripts/build-package.ts"
19
75
  }
20
- }
76
+ }