@indiegems/gem-web-sdk 23.0.6 → 23.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/changelog.md CHANGED
@@ -16,6 +16,26 @@ Its entries are grouped under up to five headings:
16
16
  - **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
17
17
  - **New capability**: something a game can now do. Nothing changes unless you use it.
18
18
 
19
+ ## 23.1.0
20
+
21
+ *Written from the API facts alone; no summary was generated for this release.*
22
+
23
+ Upgrading from 23.0.6. Impact: required.
24
+
25
+ ### Must change
26
+
27
+ - PARENT_CONFORMANCE_VERSION changed its signature.
28
+ - GemCapability changed its signature.
29
+ - GemHost.sendPrefs changed its signature.
30
+ - GemPrefs.recommended changed its signature.
31
+
32
+ ### New capability
33
+
34
+ - ParentUnderTest.setTextScale was added.
35
+ - Gem.textScale was added.
36
+ - GemOptions.onTextScale was added.
37
+ - GemRecommendedSettings.textScale was added.
38
+
19
39
  ## 23.0.6
20
40
 
21
41
  Upgrading from 23.0.5. Impact: none.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "23.0.6"
53
+ "@indiegems/gem-web-sdk": "23.1.0"
54
54
  }
55
55
  ```
56
56
 
@@ -48,8 +48,8 @@
48
48
  },
49
49
  {
50
50
  "file": "changelog.md",
51
- "sha256": "412ce6f6db6c2df07e8b360f71b13635f770453fcd616b4578723a6e85906d5c",
52
- "updated": "2026-11-07T03:38:38Z"
51
+ "sha256": "08a45ac742126bf110a16e9373e09a4e06fd4ab8cd68d62309a512a1712c8c53",
52
+ "updated": "2026-11-07T14:55:28Z"
53
53
  },
54
54
  {
55
55
  "file": "daily-challenges.md",
@@ -78,8 +78,8 @@
78
78
  },
79
79
  {
80
80
  "file": "getting-started.md",
81
- "sha256": "2cba7161b4896dd4a3f448a8ace3f0f2038b9a81de20b69ac8c000b3f50676e3",
82
- "updated": "2026-11-07T03:38:38Z"
81
+ "sha256": "a711052efd2252ddda2bed1af1394f8c57692086d5e3acdd6ccf2e5b7a14b4fc",
82
+ "updated": "2026-11-07T14:55:28Z"
83
83
  },
84
84
  {
85
85
  "file": "graphics-route.md",
@@ -143,8 +143,8 @@
143
143
  },
144
144
  {
145
145
  "file": "server-bundles.md",
146
- "sha256": "58cca5bfe80a301e3b0a8fb35f38668ce8515be38eefcb13b6b2f78c9106280b",
147
- "updated": "2026-11-07T03:38:38Z"
146
+ "sha256": "e0249cef381f23d156443b6a0b73c3a9616055e5434acf39fbdcada04ec0e745",
147
+ "updated": "2026-11-07T14:55:28Z"
148
148
  },
149
149
  {
150
150
  "file": "serverless-functions-js.md",
@@ -168,8 +168,8 @@
168
168
  },
169
169
  {
170
170
  "file": "what-a-game-must-do.md",
171
- "sha256": "341a5fa4bec0435ece65183ea2edd601037767cc58cf7f0e5929547ad2253161",
172
- "updated": "2026-11-05T16:22:23Z"
171
+ "sha256": "f70fe9fad67c6b386bddc53f1f876bb78f6a5e11c35f565bb93e925d65a99874",
172
+ "updated": "2026-11-06T22:48:07Z"
173
173
  }
174
174
  ]
175
175
  }
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
80
80
  also publishes on its download host, with the hash beside it:
81
81
 
82
82
  ```bash
83
- curl -fsSL -O https://downloads.gemarcade.com/sdk/v23.0.6/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v23.0.6/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v23.1.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v23.1.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -1149,20 +1149,21 @@ The player sets their defaults once, in the Arcade's Settings, separately for ea
1149
1149
  and a television can say different things. They reach your game as `gem.prefs.recommended`, in the
1150
1150
  same push as `muted` and `reducedMotion`, and again whenever the player changes one:
1151
1151
 
1152
- | Field | Values | What it suggests |
1153
- | ------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
1154
- | `volume` | `0` to `100` | Your master volume's starting level, as a percentage — divide by 100 for a gain. A level, never a mute. |
1155
- | `batterySaver` | `true` / `false` | Spend less power: a lower frame rate, fewer effects, a lower resolution. |
1156
- | `keyboardLayout` | `'wasd'` / `'arrows'` | Which keys move by default. |
1157
- | `gamepadConfirm` | `'south'` / `'east'` | Which face button confirms, by position: the bottom one (A on an Xbox controller) or the right one (B). The other one goes back. |
1158
- | `vibration` | `true` / `false` | Your vibration toggle's starting state ([if your game vibrates](#if-your-game-vibrates)). |
1159
- | `lookSensitivity` | `20` to `400` | Look speed for touch, mouse and pen, as a percentage of the base speeds: `100` is 1×. |
1160
- | `gamepadLookSensitivity` | `20` to `400` | Look speed for a gamepad stick, as a percentage: `100` is 1×. |
1161
- | `invertLookY` | `true` / `false` | Up/down look inverted: pushing up or dragging up looks down. Pitch only, never yaw. |
1162
- | `touchMovement` | `'floating'` / `'fixed'` | On a touch screen, a movement stick that appears where the thumb lands, or one fixed in place. |
1163
- | `graphicsQuality` | `'low'` / `'medium'` / `'high'` | The graphics quality to start at on this device. |
1164
- | `graphicsQualityAuto` | `true` / `false` | `true`: the Arcade picked that tier for this device, so you may adapt from it. `false`: the player picked it, so hold it. |
1165
- | `colorHelp` | `true` / `false` / `null` | Color help: start your colour-blind aids (patterns or symbols beside colours, a colour-blind palette) on, or off. `null`: no advice. |
1152
+ | Field | Values | What it suggests |
1153
+ | ------------------------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1154
+ | `volume` | `0` to `100` | Your master volume's starting level, as a percentage — divide by 100 for a gain. A level, never a mute. |
1155
+ | `batterySaver` | `true` / `false` | Spend less power: a lower frame rate, fewer effects, a lower resolution. |
1156
+ | `keyboardLayout` | `'wasd'` / `'arrows'` | Which keys move by default. |
1157
+ | `gamepadConfirm` | `'south'` / `'east'` | Which face button confirms, by position: the bottom one (A on an Xbox controller) or the right one (B). The other one goes back. |
1158
+ | `vibration` | `true` / `false` | Your vibration toggle's starting state ([if your game vibrates](#if-your-game-vibrates)). |
1159
+ | `lookSensitivity` | `20` to `400` | Look speed for touch, mouse and pen, as a percentage of the base speeds: `100` is 1×. |
1160
+ | `gamepadLookSensitivity` | `20` to `400` | Look speed for a gamepad stick, as a percentage: `100` is 1×. |
1161
+ | `invertLookY` | `true` / `false` | Up/down look inverted: pushing up or dragging up looks down. Pitch only, never yaw. |
1162
+ | `touchMovement` | `'floating'` / `'fixed'` | On a touch screen, a movement stick that appears where the thumb lands, or one fixed in place. |
1163
+ | `graphicsQuality` | `'low'` / `'medium'` / `'high'` | The graphics quality to start at on this device. |
1164
+ | `graphicsQualityAuto` | `true` / `false` | `true`: the Arcade picked that tier for this device, so you may adapt from it. `false`: the player picked it, so hold it. |
1165
+ | `colorHelp` | `true` / `false` / `null` | Color help: start your colour-blind aids (patterns or symbols beside colours, a colour-blind palette) on, or off. `null`: no advice. |
1166
+ | `textScale` | `100` to `200` / `null` | Text size, as a percent: scale your whole interface by it. Sent only to a game that passes `onTextScale`: see [text size](#the-arcades-text-size-size-in-rem-apply-ontextscale). |
1166
1167
 
1167
1168
  `lookSensitivity` to `touchMovement` are the camera and controls settings: see
1168
1169
  [camera and look controls](#camera-and-look-controls) for `gem.controls.look()`, which applies the
@@ -1242,6 +1243,96 @@ owns — starts from `recommended` the same way and is kept in `gem.me.settings`
1242
1243
  ([sound](#sound-your-own-mute-control-and-the-arcades)), and `reducedMotion` is the player's
1243
1244
  accessibility setting, which your game honours.
1244
1245
 
1246
+ ### The Arcade's Text size: size in `rem`, apply `onTextScale`
1247
+
1248
+ The player picks a **Text size** in the Arcade's Settings, per device: 100, 115, 130, 150, 180 or
1249
+ 200%, or System, which follows the phone's own font size. The Arcade never enlarges your game's
1250
+ text for you — inside your frame, text renders at the size you chose — so a game that wants to
1251
+ honour it applies it, and applies it to **the whole interface**, not to text alone. Text that grows
1252
+ inside boxes that do not is exactly what breaks: labels overflow, overlap and clip.
1253
+
1254
+ **Pass `onTextScale` and set your root font size from it.** Passing the handler is what asks the
1255
+ Arcade for the value; a game without one is never sent it, and `gem.textScale` stays `null`:
1256
+
1257
+ ```js
1258
+ const MAX = 150; // the largest size your layout supports: at least 130, ideally 150
1259
+ let chosen = null; // the size the player picked in YOUR settings, or null: "Arcade's size"
1260
+
1261
+ const applyTextSize = (p) => {
1262
+ document.documentElement.style.fontSize = (chosen ?? Math.min(p ?? 100, MAX)) + '%';
1263
+ };
1264
+
1265
+ const gem = createGem({ allowedEmbedders, onTextScale: applyTextSize });
1266
+ ```
1267
+
1268
+ It is called on the first push — with `null` when the Arcade gave no advice — and again whenever
1269
+ the value changes. The player's own choice in your game beats the Arcade's, the Arcade's is clamped
1270
+ to what your layout supports, and with neither you render at 100%. The SDK applies nothing itself.
1271
+
1272
+ **This works only if your interface is sized in `rem`:**
1273
+
1274
+ - **Every length in your DOM UI is `rem`, `em` or `%`, never `px`:** font sizes, padding, gaps,
1275
+ widths, heights, radii and borders. Then the root font size scales them all together.
1276
+ - **Leave the root alone.** The root font size is the browser's default (`100%`), set only by the
1277
+ handler above. A `font` or `font-size` rule on `html` or `:root` breaks every `rem`: put your base
1278
+ font on `body` instead.
1279
+ - **Media queries stay in `px`**, and so do values you measure at runtime (positions, safe-area
1280
+ insets). A `rem` in a media query does not follow your root anyway.
1281
+ - **Panels that hold text are capped to the screen:** `width: min(22rem, 100% - 2rem)`, so a panel
1282
+ that grows never runs off a phone.
1283
+ - **Boxes that hold text grow with it:** `min-height` rather than `height`, and wrapping (or a
1284
+ fit-text routine whose floor you check) rather than `white-space: nowrap` with
1285
+ `overflow: hidden` or an ellipsis on an action label.
1286
+ - **Lay out against measured chrome, not constants.** Code that reserves room for a HUD bar or a
1287
+ gear button measures it (`getBoundingClientRect`, a `ResizeObserver`) instead of hard-coding
1288
+ pixels.
1289
+ - **Canvas text counts too.** A game that draws interface text on a canvas scales it from the same
1290
+ value.
1291
+ - **`text-size-adjust` is not a fix.** It does not protect your layout from any text scaling. Keep
1292
+ `100%` only as cheap protection against iOS's own inflation.
1293
+
1294
+ A board or playfield sized from the viewport (a `1/390` unit, say) may keep that unit: it follows
1295
+ the screen, which is right for the playfield. Menus, the HUD, settings and dialogs use `rem`.
1296
+
1297
+ **Support at least 130%,** with 150% the target and 200% for a game that is mostly text. Check every
1298
+ screen at 100%, 130% and your maximum on a phone held upright (390×844), a phone held sideways
1299
+ (844×390) and a desktop (1280×800), in your longest language: nothing clipped, overlapping or off
1300
+ screen. A phone held sideways runs out of height first, so every screen must fit or scroll
1301
+ vertically at your maximum. Rendered text is at least **12 CSS px**, and at least **14 CSS px** for
1302
+ what the player must read to act (costs, instructions, buttons), at a 360 CSS px wide viewport.
1303
+
1304
+ **A text-size setting of your own** is kept per device, like the Arcade setting it overrides (in
1305
+ [device settings](#device-settings-what-this-device-remembers-about-your-game)): a size chosen on a
1306
+ phone should not follow the player to a television. Declare it as a `choice` with `icon: 'text'`
1307
+ whose **first option is the Arcade's size** — the `null` state, which a slider cannot express:
1308
+
1309
+ ```js
1310
+ {
1311
+ id: 'text',
1312
+ label: t('Text size'),
1313
+ kind: 'choice',
1314
+ icon: 'text',
1315
+ value: chosen === null ? 'arcade' : String(chosen),
1316
+ options: [
1317
+ { id: 'arcade', label: t("Arcade's size") },
1318
+ { id: '100', label: '100%' },
1319
+ { id: '130', label: '130%' },
1320
+ { id: '150', label: '150%' },
1321
+ ],
1322
+ }
1323
+ ```
1324
+
1325
+ Your choice overrides the Arcade's **for your game only**; nothing you do changes the Arcade's
1326
+ setting.
1327
+
1328
+ **Two rules hold however your game uses it, because a text size hints at a player's eyesight:**
1329
+
1330
+ - **Never put a text size in analytics.** No event, dimension or measure carries `textScale`, your
1331
+ resolved size, or the value of a text-size change. Recording that the player opened your text
1332
+ settings, without the value, is fine.
1333
+ - **Save only the player's own choice.** Never store the Arcade's value as if the player had picked
1334
+ it, and a `null` choice stays `null`, so a later change to the Arcade setting still reaches them.
1335
+
1245
1336
  ### Mirror your settings into the Arcade's menu: `gem.settings`
1246
1337
 
1247
1338
  Optional, and worth doing. The Arcade's Gem menu shows your game's own settings beside the
@@ -1304,8 +1395,24 @@ const declared = () => [
1304
1395
  { id: 'music', label: t('Music'), kind: 'toggle', value: music.on, icon: 'music' }, // the quick row's Music
1305
1396
  { id: 'sfx', label: t('Sound effects'), kind: 'toggle', value: sfx.on, icon: 'sfx' },
1306
1397
  { id: 'colors', label: t('Color help'), kind: 'toggle', value: colorHelp, icon: 'eye' }, // its Color
1307
- { id: 'musicVolume', label: t('Music volume'), kind: 'slider', value: musicVolume, min: 0, max: 100, step: 5 },
1308
- { id: 'speed', label: t('Game speed'), kind: 'slider', value: speed, min: 0.5, max: 2, step: 0.25 },
1398
+ {
1399
+ id: 'musicVolume',
1400
+ label: t('Music volume'),
1401
+ kind: 'slider',
1402
+ value: musicVolume,
1403
+ min: 0,
1404
+ max: 100,
1405
+ step: 5,
1406
+ },
1407
+ {
1408
+ id: 'speed',
1409
+ label: t('Game speed'),
1410
+ kind: 'slider',
1411
+ value: speed,
1412
+ min: 0.5,
1413
+ max: 2,
1414
+ step: 0.25,
1415
+ },
1309
1416
  {
1310
1417
  id: 'difficulty',
1311
1418
  label: t('Difficulty'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "23.0.6",
3
+ "version": "23.1.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1007,6 +1007,10 @@
1007
1007
  "2026-10-23): the SDK does NOT fill it. An older parent omits it and the",
1008
1008
  "game reads `null`, \"no recommendation\", never false.",
1009
1009
  "",
1010
+ "`textScale` IS OPTIONAL INSIDE IT AND NOT FILLED either (owner",
1011
+ "decision 2026-11-05), and a parent sends it only where the game",
1012
+ "advertised `textScale`; an absent one is no advice.",
1013
+ "",
1010
1014
  "`muted`, `reducedMotion` and `showFrameStats` stay where they are, at",
1011
1015
  "the top of this message, and keep their meaning: they are the Arcade's",
1012
1016
  "own state the frame acts on, not a recommendation. The Arcade starts",
@@ -1127,6 +1131,42 @@
1127
1131
  "reports `null` and the game keeps its own default. Unlike the",
1128
1132
  "camera and controls fields there is no neutral value to invent."
1129
1133
  ]
1134
+ },
1135
+ "textScale": {
1136
+ "kind": "int",
1137
+ "min": 100,
1138
+ "max": 200,
1139
+ "optional": true,
1140
+ "$comment": [
1141
+ "The player's Arcade Text size for this device, as a percent (owner",
1142
+ "decision 2026-11-05): 100 is the size",
1143
+ "the game was designed at, 200 is twice that. A game sized in `rem`",
1144
+ "applies it by setting its ROOT font size, so its whole interface",
1145
+ "-- text, padding, boxes and icons -- scales together. The Arcade",
1146
+ "never applies it for a game: inside a game's frame, text renders at",
1147
+ "the size the game chose.",
1148
+ "",
1149
+ "A DEFAULT ONLY, like every field here: the player's own text size in",
1150
+ "the game wins, and a game clamps this to the largest size its layout",
1151
+ "supports. A game never saves it as the player's choice and never",
1152
+ "records it in analytics.",
1153
+ "",
1154
+ "BOUNDED 100..200 PERMANENTLY. Any integer in range is valid; a",
1155
+ "parent sends only the six sizes of the Arcade's own control (100,",
1156
+ "115, 130, 150, 180, 200 -- Android's font-scale ladder), its System",
1157
+ "choice resolved to one of them. Nothing below 100: games design to",
1158
+ "a 12 CSS px floor.",
1159
+ "",
1160
+ "SENT ONLY TO A FRAME THAT ADVERTISED `textScale`, which the SDK does",
1161
+ "when the game passes `onTextScale` to `createGem`. That limits which",
1162
+ "games receive the value; it is not a security control. A parent",
1163
+ "whose `recommended` would fail validation with this field in it",
1164
+ "sends `recommended` again without it, so a bad text size never costs",
1165
+ "a game its volume, Color help or graphics.",
1166
+ "",
1167
+ "ABSENT IS NO ADVICE. A parent built against an older SDK, or one",
1168
+ "that keeps no text size, omits it, and the SDK reports `null`."
1169
+ ]
1130
1170
  }
1131
1171
  }
1132
1172
  }
@@ -195,6 +195,14 @@
195
195
  }
196
196
  ]
197
197
  },
198
+ "textScale": {
199
+ "implemented": true,
200
+ "gates": [
201
+ {
202
+ "namespace": "gem.textScale"
203
+ }
204
+ ]
205
+ },
198
206
  "walletDisplay": {
199
207
  "implemented": true,
200
208
  "gates": [