Skip to content

Commit 2d45615

Browse files
Sync packages from monorepo (ff638b7)
1 parent 39ed2eb commit 2d45615

17 files changed

Lines changed: 811 additions & 111 deletions

File tree

packages/cli/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ Save a CLI auth token. Generate one from studio.puzzmo.com.
1818
puzzmo login pzt-your-token-here
1919
```
2020

21-
### `puzzmo upload <slug> <dir>`
21+
### `puzzmo upload <dir>`
2222

23-
Upload a game build directory to Puzzmo.
23+
Upload a game build directory to Puzzmo. The game slug is read from `puzzmo.json` in the build directory.
2424

2525
```bash
26-
puzzmo upload my-game dist/
26+
puzzmo upload dist/
2727
```
2828

29-
The command collects all files in the directory, computes a SHA from git (or file contents), and uploads them in batches.
29+
The command collects all files in the directory, validates the `puzzmo.json`, computes a SHA from git (or file contents), and uploads them in batches.
3030

3131
### `puzzmo game create [token]`
3232

packages/cli/src/commands/upload.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const formatBytes = (bytes: number): string => {
4545
}
4646

4747
/** Uploads game build artifacts to Puzzmo */
48-
export const upload = async (gameSlug: string, dir: string) => {
48+
export const upload = async (dir: string) => {
4949
const token = getToken()
5050
if (!token) {
5151
console.error("Not logged in. Run `puzzmo login <token>` or set PUZZMO_TOKEN.")
@@ -87,6 +87,7 @@ export const upload = async (gameSlug: string, dir: string) => {
8787
process.exit(1)
8888
}
8989
const puzzmoFile = validation.data
90+
const gameSlug = puzzmoFile.game.slug
9091

9192
// Determine SHA
9293
const sha = getGitSHA() || hashFiles(files)

packages/cli/src/index.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const printUsage = () => {
1313
puzzmo login <token> Save a CLI auth token
1414
puzzmo game create [token] Create a new Puzzmo game project
1515
16-
puzzmo upload <slug> <dir> Upload game build from <dir>
16+
puzzmo upload <dir> Upload game build from <dir> (slug from puzzmo.json)
1717
puzzmo validate [dir] Validate puzzmo.json in a directory (default: .)
1818
puzzmo migrate List and select migration skills from dev.puzzmo.com`)
1919
}
@@ -30,13 +30,12 @@ const run = async () => {
3030
break
3131
}
3232
case "upload": {
33-
const gameSlug = args[0]
34-
const dir = args[1]
35-
if (!gameSlug || !dir) {
36-
console.error("Usage: puzzmo upload <gameSlug> <dir>")
33+
const dir = args[0]
34+
if (!dir) {
35+
console.error("Usage: puzzmo upload <dir>")
3736
process.exit(1)
3837
}
39-
await upload(gameSlug, dir)
38+
await upload(dir)
4039
break
4140
}
4241
case "game": {

packages/sdk/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,39 @@ This and the editor bundle are separate JavaScript files from your main game.
189189

190190
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.
191191

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+
192225
## Editor Integration
193226

194227
For games that support puzzle editing in Puzzmo Workshop, you will need an Editor Bundle:

packages/sdk/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
"import": "./dist/fonts.js",
3333
"require": "./dist/fonts.cjs"
3434
},
35+
"./svgJSX": {
36+
"types": "./dist/svgJSX.d.ts",
37+
"import": "./dist/svgJSX.js",
38+
"require": "./dist/svgJSX.cjs"
39+
},
3540
"./vite": {
3641
"types": "./dist/vite.d.ts",
3742
"import": "./dist/vite.js",
@@ -43,6 +48,7 @@
4348
],
4449
"scripts": {
4550
"build": "vite build && tsgo --project tsconfig.build.json",
51+
"test": "vitest run",
4652
"type-check": "tsgo --noEmit"
4753
},
4854
"keywords": [

packages/sdk/src/simulator/createSimulator.ts

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,3 @@
1-
/**
2-
* Simulator - A development UI for testing games with the Puzzmo Proto SDK.
3-
*
4-
* This script simulates the Puzzmo host environment by:
5-
* - Listening for READY messages from the game
6-
* - Sending READY_DATA with puzzle data
7-
* - Providing UI controls for START_GAME, PAUSE_GAME, RESUME_GAME, RETRY_PUZZLE
8-
*
9-
* Usage with Vite plugin (recommended):
10-
*
11-
* ```ts
12-
* // vite.config.ts
13-
* import { puzzmoSimulator } from "@puzzmo/sdk/vite"
14-
* export default defineConfig({
15-
* plugins: [puzzmoSimulator({ slug: "my-game", fixturesGlob: "/fixtures/puzzles/**\/*.json" })]
16-
* })
17-
* ```
18-
*
19-
* The plugin handles making sure it is removed on vite builds.
20-
*
21-
* Usage with manual imports:
22-
* ```html
23-
* <script type="module">
24-
* import { createSimulator } from "@puzzmo/sdk/simulator"
25-
* const fixtures = import.meta.glob("./fixtures/puzzles/**\/*.json", { eager: true })
26-
* createSimulator({ fixtures })
27-
* </script>
28-
* ```
29-
* The fixtures folder structure should be: fixtures/puzzles/{category}/{puzzle}.json
30-
* This will show dropdowns in the Ctrl tab to select category and puzzle.
31-
*/
32-
331
import type { BootstrapGameData, MessagesReceived } from "../types"
342
import type { SimulatorConfig, SimulatorContext, SimulatorState, SimulatorView, TabName, FixtureImports } from "./types"
353
import { simulatorStyles } from "./styles"
@@ -46,6 +14,7 @@ import {
4614
createThemeView,
4715
createAuthView,
4816
createFeaturesView,
17+
createKeyboardView,
4918
} from "./views"
5019

5120
// Re-export types for consumers
@@ -60,10 +29,40 @@ interface SimulatorInstance {
6029
let simulatorInstance: SimulatorInstance | null = null
6130

6231
/**
63-
* Creates the Simulator UI and message handling.
64-
* If called multiple times, updates the existing instance with new settings (e.g., fixtures).
32+
* Simulator - A development UI for testing games with the Puzzmo Proto SDK.
33+
*
34+
* This script simulates the Puzzmo host environment by:
35+
* - Listening for READY messages from the game
36+
* - Sending READY_DATA with puzzle data
37+
* - Providing UI controls for START_GAME, PAUSE_GAME, RESUME_GAME, RETRY_PUZZLE
38+
*
39+
* Usage with Vite plugin (recommended):
40+
*
41+
* ```ts
42+
* // vite.config.ts
43+
* import { puzzmoSimulator } from "@puzzmo/sdk/vite"
44+
* export default defineConfig({
45+
* plugins: [puzzmoSimulator({})]
46+
* })
47+
* ```
48+
*
49+
* The plugin automatically reads the game slug from the nearest puzzmo.json.
50+
*
51+
* The plugin handles making sure it is removed on vite builds.
52+
*
53+
* Usage with manual imports:
54+
* ```html
55+
* <script type="module">
56+
* import { createSimulator } from "@puzzmo/sdk/simulator"
57+
* const fixtures = import.meta.glob("./fixtures/puzzles/**\/*.json", { eager: true })
58+
* createSimulator({ fixtures })
59+
* </script>
60+
* ```
61+
* The fixtures folder structure should be: fixtures/puzzles/{category}/{puzzle}.json
62+
* This will show dropdowns in the Ctrl tab to select category and puzzle.
6563
*/
66-
function createSimulator(config: SimulatorConfig = {}): SimulatorInstance {
64+
65+
export function createSimulator(config: SimulatorConfig = {}): SimulatorInstance {
6766
console.log("[Simulator] createSimulator called with config:", { slug: config.slug, hasFixtures: !!config.fixtures })
6867

6968
// If instance already exists, update it with new config
@@ -94,6 +93,7 @@ function createSimulator(config: SimulatorConfig = {}): SimulatorInstance {
9493
createThemeView(),
9594
createAuthView(),
9695
createFeaturesView(),
96+
createKeyboardView(),
9797
] satisfies SimulatorView[]
9898

9999
const validTabIds = views.map((v) => v.id)
@@ -582,5 +582,3 @@ function createSimulator(config: SimulatorConfig = {}): SimulatorInstance {
582582

583583
return simulatorInstance
584584
}
585-
586-
export { createSimulator }

packages/sdk/src/simulator/styles.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const simulatorStyles = `
2828
border-radius: 4px;
2929
color: var(--sim-text);
3030
width: 420px;
31+
max-width: calc(100vw - 8px);
3132
box-shadow: 4px 4px 0 rgba(0,0,0,0.5);
3233
}
3334
#simulator-panel.collapsed {
@@ -1071,4 +1072,78 @@ export const simulatorStyles = `
10711072
color: var(--sim-text-dim);
10721073
font-size: 9px;
10731074
}
1075+
/* Keyboard view styles */
1076+
.keyboard-view-container {
1077+
padding: 4px;
1078+
}
1079+
.sim-kb-empty {
1080+
color: var(--sim-text-dim);
1081+
text-align: center;
1082+
padding: 16px 8px;
1083+
font-size: 10px;
1084+
line-height: 1.6;
1085+
}
1086+
.sim-kb-empty code {
1087+
background: var(--sim-bg);
1088+
padding: 1px 4px;
1089+
border-radius: 2px;
1090+
font-size: 10px;
1091+
}
1092+
.sim-kb {
1093+
display: flex;
1094+
flex-direction: column;
1095+
gap: 4px;
1096+
}
1097+
.sim-kb-row {
1098+
display: flex;
1099+
justify-content: center;
1100+
gap: 3px;
1101+
}
1102+
.sim-kb-key {
1103+
min-width: 28px;
1104+
height: 30px;
1105+
padding: 0 4px;
1106+
border: 1px solid var(--sim-border);
1107+
border-radius: 3px;
1108+
background: var(--sim-bg);
1109+
color: var(--sim-text);
1110+
font: inherit;
1111+
font-size: 11px;
1112+
text-transform: uppercase;
1113+
cursor: pointer;
1114+
display: flex;
1115+
align-items: center;
1116+
justify-content: center;
1117+
}
1118+
.sim-kb-key:hover {
1119+
background: var(--sim-bg-alt);
1120+
border-color: var(--sim-border-light);
1121+
}
1122+
.sim-kb-key:active {
1123+
background: var(--sim-accent);
1124+
color: var(--sim-panel);
1125+
}
1126+
.sim-kb-key.highlight {
1127+
background: var(--sim-bg-alt);
1128+
border-color: var(--sim-accent);
1129+
color: var(--sim-accent);
1130+
font-size: 9px;
1131+
}
1132+
.sim-kb-key.highlight:active {
1133+
background: var(--sim-accent);
1134+
color: var(--sim-panel);
1135+
}
1136+
.sim-kb-key.l {
1137+
min-width: 40px;
1138+
}
1139+
.sim-kb-key.xl {
1140+
min-width: 52px;
1141+
}
1142+
.sim-kb-key.grow {
1143+
flex: 1;
1144+
}
1145+
.sim-kb-key.disabled {
1146+
opacity: 0.3;
1147+
cursor: default;
1148+
}
10741149
`

0 commit comments

Comments
 (0)