@puzzmo/sdk 1.0.16 → 1.0.17

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/README.md CHANGED
@@ -189,6 +189,39 @@ This and the editor bundle are separate JavaScript files from your main game.
189
189
 
190
190
  There are Vite plugins to make this easy, but otherwise, they should be files in your upload named `app-bundle.js` and `editor-bundle.js` with ESM exports which match the shapes of the TypeScript types.
191
191
 
192
+ ### Thumbnail JSX
193
+
194
+ We have found over time that using JSX for thumbnails makes it a lot easier to ensure correct SVG output, but React/Preact are big runtimes, so we have a smaller JSX runtime built just for non-interactive SVGs based on [understated](https://github.com/callmecavs/understated).
195
+
196
+ To use it, configure your file's JSX pragma to use `h` and `render` from `@puzzmo/sdk/svgJSX`:
197
+
198
+ ```tsx
199
+ /** @jsxRuntime classic @jsx h */
200
+ import { h, render } from "@puzzmo/sdk/svgJSX"
201
+
202
+ // Needed for fragments
203
+ const React = { Fragment: "g" }
204
+
205
+ export function renderThumbnail(puzzleStr: string, inputStr?: string, config?: ThumbnailConfig): string {
206
+ const puzzle = JSON.parse(puzzleStr)
207
+ const size = 200
208
+
209
+ const svg = (
210
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${size} ${size}`}>
211
+ <rect width={size} height={size} fill={config?.theme?.g_bg ?? "#1a1a2e"} />
212
+ <text x={size / 2} y={size / 2} textAnchor="middle" fill={config?.theme?.fg ?? "#fff"} fontSize="24">
213
+ {puzzle.title}
214
+ </text>
215
+ </svg>
216
+ )
217
+
218
+ const el = render(svg)
219
+ return el instanceof Element ? el.outerHTML : ""
220
+ }
221
+ ```
222
+
223
+ The `h` function is a JSX factory that creates virtual DOM nodes, and `render` converts them into real DOM elements. Since thumbnails can run server-side or in a DOM-shimmed environment, the result is serialized to an SVG string via `outerHTML`.
224
+
192
225
  ## Editor Integration
193
226
 
194
227
  For games that support puzzle editing in Puzzmo Workshop, you will need an Editor Bundle:
@@ -993,10 +993,7 @@ var F = "puzzmo_sim_api_mode", I = "http://localhost:8911", L = "https://api.puz
993
993
  return crypto.getRandomValues(e), Array.from(e, (e) => e.toString(16).padStart(2, "0")).join("");
994
994
  }, W = "puzzmo_sim_oauth_token", G = "puzzmo_sim_oauth_refresh_token", K = (e) => localStorage.setItem(W, e), ue = (e) => localStorage.setItem(G, e), q = () => {
995
995
  let e = localStorage.getItem(W);
996
- return console.log("[AuthView] getAccessToken:", {
997
- TOKEN_KEY: W,
998
- token: e ? `${e.substring(0, 20)}...` : null
999
- }), e;
996
+ return e && `${e.substring(0, 20)}`, e;
1000
997
  }, J = () => localStorage.getItem(G), Y = () => {
1001
998
  localStorage.removeItem(W), localStorage.removeItem(G);
1002
999
  }, de = () => {
@@ -1016,7 +1013,7 @@ var F = "puzzmo_sim_api_mode", I = "http://localhost:8911", L = "https://api.puz
1016
1013
  }, pe = function() {
1017
1014
  var t = e(function* () {
1018
1015
  let e = J();
1019
- if (!e) return console.log("[AuthView] No refresh token available"), !1;
1016
+ if (!e) return !1;
1020
1017
  let t = H();
1021
1018
  try {
1022
1019
  let n = new URLSearchParams({
@@ -1028,14 +1025,14 @@ var F = "puzzmo_sim_api_mode", I = "http://localhost:8911", L = "https://api.puz
1028
1025
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
1029
1026
  body: n.toString()
1030
1027
  });
1031
- if (!r.ok) return console.error("[AuthView] Failed to refresh token:", r.statusText), !1;
1028
+ if (!r.ok) return r.statusText, !1;
1032
1029
  let i = yield r.json(), a = i.access_token || i.accessToken;
1033
- if (!a) return console.error("[AuthView] No access token in refresh response"), !1;
1030
+ if (!a) return !1;
1034
1031
  K(a);
1035
1032
  let o = i.refresh_token || i.refreshToken;
1036
- return o && ue(o), console.log("[AuthView] Successfully refreshed access token"), !0;
1033
+ return o && ue(o), !0;
1037
1034
  } catch (e) {
1038
- return console.error("[AuthView] Error refreshing token:", e), !1;
1035
+ return !1;
1039
1036
  }
1040
1037
  });
1041
1038
  return function() {
@@ -1044,7 +1041,7 @@ var F = "puzzmo_sim_api_mode", I = "http://localhost:8911", L = "https://api.puz
1044
1041
  }(), me = function() {
1045
1042
  var t = e(function* (e, t) {
1046
1043
  let n = H(), r = sessionStorage.getItem("oauth_state");
1047
- if (!r || r !== t) return console.error("OAuth state mismatch - possible CSRF attack"), null;
1044
+ if (!r || r !== t) return null;
1048
1045
  sessionStorage.removeItem("oauth_state");
1049
1046
  try {
1050
1047
  let t = new URLSearchParams({
@@ -1057,9 +1054,9 @@ var F = "puzzmo_sim_api_mode", I = "http://localhost:8911", L = "https://api.puz
1057
1054
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
1058
1055
  body: t.toString()
1059
1056
  });
1060
- return r.ok ? yield r.json() : (console.error("Failed to exchange code for token:", r.statusText), null);
1057
+ return r.ok ? yield r.json() : (r.statusText, null);
1061
1058
  } catch (e) {
1062
- return console.error("Error exchanging code for token:", e), null;
1059
+ return null;
1063
1060
  }
1064
1061
  });
1065
1062
  return function(e, n) {
@@ -1069,7 +1066,7 @@ var F = "puzzmo_sim_api_mode", I = "http://localhost:8911", L = "https://api.puz
1069
1066
  var t = e(function* (e, t = {}) {
1070
1067
  let n = H(), r = q();
1071
1068
  if (!r) throw Error("Not authenticated");
1072
- if (fe(r)) if (console.log("[AuthView] Access token expired, attempting refresh..."), yield pe()) {
1069
+ if (fe(r)) if (yield pe()) {
1073
1070
  if (r = q(), !r) throw Error("Token refresh succeeded but no token available");
1074
1071
  } else throw Y(), Error("Session expired. Please log in again.");
1075
1072
  let i = yield fetch(`${n.apiURL}/graphql`, {
@@ -1092,13 +1089,10 @@ var F = "puzzmo_sim_api_mode", I = "http://localhost:8911", L = "https://api.puz
1092
1089
  };
1093
1090
  }(), ge = (e) => {
1094
1091
  try {
1095
- console.log("[AuthView] decodeJWT input:", e);
1096
1092
  let t = e.split(".");
1097
- if (console.log("[AuthView] JWT parts:", t.length), t.length !== 3) return null;
1098
- let n = JSON.parse(atob(t[1]));
1099
- return console.log("[AuthView] JWT payload:", n), n;
1093
+ return t.length, t.length === 3 ? JSON.parse(atob(t[1])) : null;
1100
1094
  } catch (e) {
1101
- return console.error("[AuthView] decodeJWT error:", e), null;
1095
+ return null;
1102
1096
  }
1103
1097
  };
1104
1098
  function _e() {
@@ -1110,7 +1104,7 @@ function _e() {
1110
1104
  let e = q(), t = !!e, n = R(), r = n === "dev" ? I : L, i = `<button class="simulator-btn tiny" id="auth-dev-toggle" style="display: none;">${n === "dev" ? "Using Dev" : "Dev"}</button>`;
1111
1105
  if (t) {
1112
1106
  let t = ge(e), n = t != null && t.exp ? (/* @__PURE__ */ new Date(t.exp * 1e3)).toLocaleString() : "Unknown", a = !!J(), o = a ? ge(J()) : null, s = o != null && o.exp ? (/* @__PURE__ */ new Date(o.exp * 1e3)).toLocaleString() : null;
1113
- return console.log({ decoded: t }), `
1107
+ return `
1114
1108
  <div class="simulator-section">
1115
1109
  <div class="simulator-section-title auth-title-row">
1116
1110
  <span>Puzzmo Authentication</span>
@@ -1408,12 +1402,91 @@ function Ne() {
1408
1402
  }
1409
1403
  };
1410
1404
  }
1405
+ /** Renders a single keyboard key as an HTML button string */
1406
+ var Pe = (e, t) => {
1407
+ var n, r;
1408
+ let i = (n = t.symbols[e]) == null ? e : n, a = t.disabled.includes(e), o = t.highlight.includes(e), s = t.xl.includes(e), c = t.l.includes(e), l = (r = t.flexGrowSymbols) == null ? void 0 : r.includes(e);
1409
+ return `<button class="${[
1410
+ "sim-kb-key",
1411
+ a ? "disabled" : "",
1412
+ o ? "highlight" : "",
1413
+ s ? "xl" : "",
1414
+ c ? "l" : "",
1415
+ l ? "grow" : ""
1416
+ ].filter(Boolean).join(" ")}" data-key="${e}" ${a ? "disabled" : ""}>${i}</button>`;
1417
+ }, Fe = (e) => `<div class="sim-kb">${e.layout.filter((e) => e != null).map((t) => `<div class="sim-kb-row">${[...t].map((t) => Pe(t, e)).join("")}</div>`).join("")}</div>`;
1418
+ function Ie() {
1419
+ let e = null, t = () => e ? Fe(e) : "<div class=\"sim-kb-empty\">No keyboard config received from game yet.<br>The game calls <code>sdk.keyboard.show(config)</code> to display a keyboard.</div>";
1420
+ return {
1421
+ id: "kbd",
1422
+ label: "Kbd",
1423
+ render() {
1424
+ return `
1425
+ <div class="keyboard-view-container">
1426
+ <div id="sim-kb-content">
1427
+ ${t()}
1428
+ </div>
1429
+ </div>
1430
+ `;
1431
+ },
1432
+ bind(e) {
1433
+ Le(e);
1434
+ },
1435
+ onMessage(n, r, i) {
1436
+ var a;
1437
+ if (n !== "KEYBOARD_UPDATE_CONFIG") return;
1438
+ e = !(!(r == null || (a = r.layout) == null) && a.length) || r.layout.every((e) => !e) ? null : r;
1439
+ let o = i.getElement("#sim-kb-content");
1440
+ o && (o.innerHTML = t(), Le(i)), i.updateBadge("kbd", void 0);
1441
+ }
1442
+ };
1443
+ }
1444
+ /** Attach click handlers to all rendered keys */
1445
+ function Le(e) {
1446
+ var t;
1447
+ let n = (t = e.getElement("#sim-kb-content")) == null ? void 0 : t.querySelectorAll(".sim-kb-key");
1448
+ n == null || n.forEach((t) => {
1449
+ t.addEventListener("click", () => {
1450
+ let n = t.getAttribute("data-key");
1451
+ n && e.sendToGame("KEYBOARD_KEY_PRESS", { key: n });
1452
+ });
1453
+ });
1454
+ }
1411
1455
  var $ = null;
1412
1456
  /**
1413
- * Creates the Simulator UI and message handling.
1414
- * If called multiple times, updates the existing instance with new settings (e.g., fixtures).
1457
+ * Simulator - A development UI for testing games with the Puzzmo Proto SDK.
1458
+ *
1459
+ * This script simulates the Puzzmo host environment by:
1460
+ * - Listening for READY messages from the game
1461
+ * - Sending READY_DATA with puzzle data
1462
+ * - Providing UI controls for START_GAME, PAUSE_GAME, RESUME_GAME, RETRY_PUZZLE
1463
+ *
1464
+ * Usage with Vite plugin (recommended):
1465
+ *
1466
+ * ```ts
1467
+ * // vite.config.ts
1468
+ * import { puzzmoSimulator } from "@puzzmo/sdk/vite"
1469
+ * export default defineConfig({
1470
+ * plugins: [puzzmoSimulator({})]
1471
+ * })
1472
+ * ```
1473
+ *
1474
+ * The plugin automatically reads the game slug from the nearest puzzmo.json.
1475
+ *
1476
+ * The plugin handles making sure it is removed on vite builds.
1477
+ *
1478
+ * Usage with manual imports:
1479
+ * ```html
1480
+ * <script type="module">
1481
+ * import { createSimulator } from "@puzzmo/sdk/simulator"
1482
+ * const fixtures = import.meta.glob("./fixtures/puzzles/**\/*.json", { eager: true })
1483
+ * createSimulator({ fixtures })
1484
+ * <\/script>
1485
+ * ```
1486
+ * The fixtures folder structure should be: fixtures/puzzles/{category}/{puzzle}.json
1487
+ * This will show dropdowns in the Ctrl tab to select category and puzzle.
1415
1488
  */
1416
- function Pe(t = {}) {
1489
+ function Re(t = {}) {
1417
1490
  var n, r;
1418
1491
  if (console.log("[Simulator] createSimulator called with config:", {
1419
1492
  slug: t.slug,
@@ -1428,7 +1501,8 @@ function Pe(t = {}) {
1428
1501
  c,
1429
1502
  le(),
1430
1503
  _e(),
1431
- Ne()
1504
+ Ne(),
1505
+ Ie()
1432
1506
  ], p = f.map((e) => e.id), m = l(t, o, p), h = {
1433
1507
  pause: "<svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"currentColor\"><rect x=\"1\" y=\"1\" width=\"3\" height=\"8\"/><rect x=\"6\" y=\"1\" width=\"3\" height=\"8\"/></svg>",
1434
1508
  play: "<svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"currentColor\"><polygon points=\"2,1 9,5 2,9\"/></svg>",
@@ -1468,6 +1542,7 @@ function Pe(t = {}) {
1468
1542
  border-radius: 4px;
1469
1543
  color: var(--sim-text);
1470
1544
  width: 420px;
1545
+ max-width: calc(100vw - 8px);
1471
1546
  box-shadow: 4px 4px 0 rgba(0,0,0,0.5);
1472
1547
  }
1473
1548
  #simulator-panel.collapsed {
@@ -2511,6 +2586,80 @@ function Pe(t = {}) {
2511
2586
  color: var(--sim-text-dim);
2512
2587
  font-size: 9px;
2513
2588
  }
2589
+ /* Keyboard view styles */
2590
+ .keyboard-view-container {
2591
+ padding: 4px;
2592
+ }
2593
+ .sim-kb-empty {
2594
+ color: var(--sim-text-dim);
2595
+ text-align: center;
2596
+ padding: 16px 8px;
2597
+ font-size: 10px;
2598
+ line-height: 1.6;
2599
+ }
2600
+ .sim-kb-empty code {
2601
+ background: var(--sim-bg);
2602
+ padding: 1px 4px;
2603
+ border-radius: 2px;
2604
+ font-size: 10px;
2605
+ }
2606
+ .sim-kb {
2607
+ display: flex;
2608
+ flex-direction: column;
2609
+ gap: 4px;
2610
+ }
2611
+ .sim-kb-row {
2612
+ display: flex;
2613
+ justify-content: center;
2614
+ gap: 3px;
2615
+ }
2616
+ .sim-kb-key {
2617
+ min-width: 28px;
2618
+ height: 30px;
2619
+ padding: 0 4px;
2620
+ border: 1px solid var(--sim-border);
2621
+ border-radius: 3px;
2622
+ background: var(--sim-bg);
2623
+ color: var(--sim-text);
2624
+ font: inherit;
2625
+ font-size: 11px;
2626
+ text-transform: uppercase;
2627
+ cursor: pointer;
2628
+ display: flex;
2629
+ align-items: center;
2630
+ justify-content: center;
2631
+ }
2632
+ .sim-kb-key:hover {
2633
+ background: var(--sim-bg-alt);
2634
+ border-color: var(--sim-border-light);
2635
+ }
2636
+ .sim-kb-key:active {
2637
+ background: var(--sim-accent);
2638
+ color: var(--sim-panel);
2639
+ }
2640
+ .sim-kb-key.highlight {
2641
+ background: var(--sim-bg-alt);
2642
+ border-color: var(--sim-accent);
2643
+ color: var(--sim-accent);
2644
+ font-size: 9px;
2645
+ }
2646
+ .sim-kb-key.highlight:active {
2647
+ background: var(--sim-accent);
2648
+ color: var(--sim-panel);
2649
+ }
2650
+ .sim-kb-key.l {
2651
+ min-width: 40px;
2652
+ }
2653
+ .sim-kb-key.xl {
2654
+ min-width: 52px;
2655
+ }
2656
+ .sim-kb-key.grow {
2657
+ flex: 1;
2658
+ }
2659
+ .sim-kb-key.disabled {
2660
+ opacity: 0.3;
2661
+ cursor: default;
2662
+ }
2514
2663
  </style>
2515
2664
  <div id="simulator-panel" class="${m.isCollapsed ? "collapsed" : ""}">
2516
2665
  <div id="simulator-header">
@@ -2743,6 +2892,6 @@ function Pe(t = {}) {
2743
2892
  loadPuzzle: I
2744
2893
  }, $;
2745
2894
  }
2746
- export { Pe as t };
2895
+ export { Re as t };
2747
2896
 
2748
- //# sourceMappingURL=createSimulator-IMuPxYe-.js.map
2897
+ //# sourceMappingURL=createSimulator-BwucCTnM.js.map