@the-inclusionist/engine 10.0.0 → 11.0.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 (528) hide show
  1. package/README.md +32 -1
  2. package/app/css/style.css +216 -42
  3. package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
  4. package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
  5. package/app/public/vendor/fonts.css +11 -1175
  6. package/dist-pkg/boot/create-game.d.ts +84 -33
  7. package/dist-pkg/boot/create-game.js +708 -307
  8. package/dist-pkg/core/a11y-sr.d.ts +1 -1
  9. package/dist-pkg/core/a11y-sr.js +3 -5
  10. package/dist-pkg/core/accommodations.d.ts +26 -8
  11. package/dist-pkg/core/accommodations.js +27 -10
  12. package/dist-pkg/core/actions.d.ts +39 -10
  13. package/dist-pkg/core/actions.js +28 -6
  14. package/dist-pkg/core/contract.d.ts +0 -2
  15. package/dist-pkg/core/contract.js +0 -2
  16. package/dist-pkg/core/i18n.d.ts +36 -71
  17. package/dist-pkg/core/i18n.js +131 -198
  18. package/dist-pkg/core/session-clock.d.ts +23 -0
  19. package/dist-pkg/core/session-clock.js +37 -0
  20. package/dist-pkg/i18n/en.js +44 -49
  21. package/dist-pkg/i18n/es.js +44 -49
  22. package/dist-pkg/i18n/pt.js +49 -54
  23. package/dist-pkg/input/gamepad.d.ts +42 -11
  24. package/dist-pkg/input/gamepad.js +50 -15
  25. package/dist-pkg/input/key-default.d.ts +30 -0
  26. package/dist-pkg/input/key-default.js +60 -0
  27. package/dist-pkg/input/keyboard-runtime.d.ts +0 -4
  28. package/dist-pkg/input/keyboard.d.ts +0 -1
  29. package/dist-pkg/input/keydown.d.ts +5 -11
  30. package/dist-pkg/input/keydown.js +3 -3
  31. package/dist-pkg/input/latch-edge.d.ts +10 -2
  32. package/dist-pkg/input/latch-edge.js +4 -3
  33. package/dist-pkg/input/latch-scope.d.ts +27 -22
  34. package/dist-pkg/input/latch-scope.js +25 -37
  35. package/dist-pkg/input/latch-store.d.ts +9 -12
  36. package/dist-pkg/input/latch-store.js +13 -18
  37. package/dist-pkg/input/latch-sync.d.ts +5 -4
  38. package/dist-pkg/input/latch-sync.js +5 -5
  39. package/dist-pkg/input/pad-reading.js +7 -2
  40. package/dist-pkg/input/pad-wizard.d.ts +4 -1
  41. package/dist-pkg/input/pad-wizard.js +60 -9
  42. package/dist-pkg/input/switch-scan.d.ts +29 -12
  43. package/dist-pkg/input/switch-scan.js +15 -2
  44. package/dist-pkg/input/touch-bindings.d.ts +3 -6
  45. package/dist-pkg/input/touch.d.ts +17 -2
  46. package/dist-pkg/input/touch.js +55 -17
  47. package/dist-pkg/input/transport-in-use.d.ts +5 -5
  48. package/dist-pkg/input/transport-in-use.js +5 -5
  49. package/dist-pkg/input/transports.d.ts +0 -9
  50. package/dist-pkg/input/transports.js +1 -1
  51. package/dist-pkg/input/virtual-controller.d.ts +39 -3
  52. package/dist-pkg/input/virtual-controller.js +32 -4
  53. package/dist-pkg/input/voice-map.d.ts +27 -11
  54. package/dist-pkg/input/voice-map.js +41 -43
  55. package/dist-pkg/platform/audio-ambient.d.ts +0 -1
  56. package/dist-pkg/platform/audio-ambient.js +3 -1
  57. package/dist-pkg/platform/audio-mixer.js +4 -9
  58. package/dist-pkg/platform/audio-sonar.d.ts +8 -94
  59. package/dist-pkg/platform/audio-sonar.js +22 -207
  60. package/dist-pkg/platform/audio.d.ts +10 -1
  61. package/dist-pkg/platform/audio.js +35 -2
  62. package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
  63. package/dist-pkg/platform/choose-by-voice.js +157 -0
  64. package/dist-pkg/platform/font-library.d.ts +77 -0
  65. package/dist-pkg/platform/font-library.js +98 -0
  66. package/dist-pkg/platform/font-library.json +721 -0
  67. package/dist-pkg/platform/heavy-catalogue.d.ts +46 -0
  68. package/dist-pkg/platform/heavy-catalogue.js +47 -14
  69. package/dist-pkg/platform/heavy-mirror.d.ts +3 -9
  70. package/dist-pkg/platform/heavy-mirror.js +14 -17
  71. package/dist-pkg/platform/heavy.d.ts +40 -8
  72. package/dist-pkg/platform/heavy.js +177 -22
  73. package/dist-pkg/platform/interruptible-speech.d.ts +15 -8
  74. package/dist-pkg/platform/interruptible-speech.js +37 -13
  75. package/dist-pkg/platform/listener-scope.js +22 -13
  76. package/dist-pkg/platform/locale-host.d.ts +7 -3
  77. package/dist-pkg/platform/locale-host.js +13 -2
  78. package/dist-pkg/platform/reading-in-worker.d.ts +24 -0
  79. package/dist-pkg/platform/reading-in-worker.js +38 -0
  80. package/dist-pkg/platform/reading-model.d.ts +21 -0
  81. package/dist-pkg/platform/reading-model.js +25 -0
  82. package/dist-pkg/platform/reading-runtime.js +13 -8
  83. package/dist-pkg/platform/reading.d.ts +12 -1
  84. package/dist-pkg/platform/reading.js +9 -1
  85. package/dist-pkg/platform/speech-recognition.d.ts +10 -1
  86. package/dist-pkg/platform/speech-recognition.js +29 -9
  87. package/dist-pkg/platform/speech.d.ts +10 -1
  88. package/dist-pkg/platform/speech.js +24 -13
  89. package/dist-pkg/platform/tts.d.ts +37 -4
  90. package/dist-pkg/platform/tts.js +279 -57
  91. package/dist-pkg/platform/voice-listener.d.ts +6 -2
  92. package/dist-pkg/platform/voice-listener.js +1 -1
  93. package/dist-pkg/platform/voice-plan.d.ts +13 -0
  94. package/dist-pkg/platform/voice-plan.js +57 -2
  95. package/dist-pkg/platform/vosk-runtime.d.ts +25 -0
  96. package/dist-pkg/platform/vosk-runtime.js +59 -1
  97. package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
  98. package/dist-pkg/platform/vosk-vocabulary.js +178 -0
  99. package/dist-pkg/render/crt.d.ts +11 -9
  100. package/dist-pkg/render/crt.js +6 -10
  101. package/dist-pkg/render/cvd-matrices.d.ts +5 -1
  102. package/dist-pkg/render/cvd-matrices.js +5 -1
  103. package/dist-pkg/render/high-contrast.d.ts +1 -2
  104. package/dist-pkg/render/high-contrast.js +5 -1
  105. package/dist-pkg/render/viz-axes.d.ts +9 -0
  106. package/dist-pkg/render/viz-axes.js +15 -2
  107. package/dist-pkg/ui/aac-sets.d.ts +29 -24
  108. package/dist-pkg/ui/aac-sets.js +22 -30
  109. package/dist-pkg/ui/audio-choices.d.ts +10 -5
  110. package/dist-pkg/ui/audio-choices.js +14 -10
  111. package/dist-pkg/ui/camera-control.d.ts +47 -0
  112. package/dist-pkg/ui/camera-control.js +35 -1
  113. package/dist-pkg/ui/declared-words.d.ts +21 -0
  114. package/dist-pkg/ui/declared-words.js +66 -0
  115. package/dist-pkg/ui/drawing-problems.js +4 -3
  116. package/dist-pkg/ui/eye-control.js +17 -17
  117. package/dist-pkg/ui/face-control.js +15 -15
  118. package/dist-pkg/ui/fonts.d.ts +42 -8
  119. package/dist-pkg/ui/fonts.js +58 -24
  120. package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
  121. package/dist-pkg/ui/footer-scroll-driver.js +201 -0
  122. package/dist-pkg/ui/footer-scroll.d.ts +38 -0
  123. package/dist-pkg/ui/footer-scroll.js +55 -0
  124. package/dist-pkg/ui/game-options.d.ts +14 -9
  125. package/dist-pkg/ui/game-options.js +41 -21
  126. package/dist-pkg/ui/hand-control.js +15 -15
  127. package/dist-pkg/ui/help-panel.d.ts +19 -8
  128. package/dist-pkg/ui/help-panel.js +15 -5
  129. package/dist-pkg/ui/hud-bands.d.ts +29 -12
  130. package/dist-pkg/ui/hud-bands.js +63 -25
  131. package/dist-pkg/ui/hud-row.d.ts +34 -0
  132. package/dist-pkg/ui/hud-row.js +80 -0
  133. package/dist-pkg/ui/hud.d.ts +0 -3
  134. package/dist-pkg/ui/layout.js +5 -2
  135. package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
  136. package/dist-pkg/ui/libras-avatar-clip.js +86 -0
  137. package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
  138. package/dist-pkg/ui/libras-avatar-load.js +134 -0
  139. package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
  140. package/dist-pkg/ui/libras-avatar-plan.js +211 -0
  141. package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
  142. package/dist-pkg/ui/libras-avatar-player.js +226 -0
  143. package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
  144. package/dist-pkg/ui/libras-avatar-stage.js +208 -0
  145. package/dist-pkg/ui/libras-glosses.d.ts +28 -0
  146. package/dist-pkg/ui/libras-glosses.js +161 -0
  147. package/dist-pkg/ui/menu-intent.d.ts +29 -0
  148. package/dist-pkg/ui/menu-intent.js +35 -1
  149. package/dist-pkg/ui/menu-items.d.ts +10 -0
  150. package/dist-pkg/ui/menu-items.js +29 -0
  151. package/dist-pkg/ui/menu-nav.d.ts +40 -4
  152. package/dist-pkg/ui/menu-nav.js +132 -47
  153. package/dist-pkg/ui/panel-widgets.d.ts +34 -0
  154. package/dist-pkg/ui/panel-widgets.js +86 -0
  155. package/dist-pkg/ui/pause-icons.d.ts +30 -14
  156. package/dist-pkg/ui/pause-icons.js +106 -45
  157. package/dist-pkg/ui/scan-overlay.d.ts +3 -3
  158. package/dist-pkg/ui/scan-overlay.js +11 -3
  159. package/dist-pkg/ui/screen-text.d.ts +25 -0
  160. package/dist-pkg/ui/screen-text.js +152 -0
  161. package/dist-pkg/ui/session-clock.d.ts +32 -0
  162. package/dist-pkg/ui/session-clock.js +67 -0
  163. package/dist-pkg/ui/settings-aac.d.ts +3 -1
  164. package/dist-pkg/ui/settings-aac.js +32 -13
  165. package/dist-pkg/ui/settings-audio.d.ts +10 -5
  166. package/dist-pkg/ui/settings-audio.js +101 -35
  167. package/dist-pkg/ui/settings-controls.d.ts +0 -4
  168. package/dist-pkg/ui/settings-mobility.d.ts +3 -6
  169. package/dist-pkg/ui/settings-mobility.js +4 -46
  170. package/dist-pkg/ui/settings-motion.js +2 -2
  171. package/dist-pkg/ui/settings-panel.d.ts +10 -0
  172. package/dist-pkg/ui/settings-panel.js +25 -1
  173. package/dist-pkg/ui/settings-typo.d.ts +5 -6
  174. package/dist-pkg/ui/settings-typo.js +20 -11
  175. package/dist-pkg/ui/settings-visual.js +2 -2
  176. package/dist-pkg/ui/shell.d.ts +1 -3
  177. package/dist-pkg/ui/shell.js +8 -4
  178. package/dist-pkg/ui/title.d.ts +2 -11
  179. package/dist-pkg/ui/title.js +39 -6
  180. package/dist-pkg/ui/top-band.d.ts +5 -3
  181. package/dist-pkg/ui/top-band.js +21 -25
  182. package/dist-pkg/ui/typo-choices.d.ts +3 -3
  183. package/dist-pkg/ui/typo-choices.js +9 -8
  184. package/dist-pkg/ui/vlibras.d.ts +71 -27
  185. package/dist-pkg/ui/vlibras.js +116 -44
  186. package/dist-pkg/ui/voice-control.d.ts +23 -1
  187. package/dist-pkg/ui/voice-control.js +149 -16
  188. package/dist-pkg/ui/voice-settings.d.ts +2 -3
  189. package/dist-pkg/ui/voice-settings.js +53 -3
  190. package/dist-pkg/ui/where-the-child-is.js +2 -2
  191. package/docs/CREDITS.md +122 -10
  192. package/docs/LICENSES.md +36 -7
  193. package/package.json +34 -9
  194. package/scripts/check-cartridge.mjs +78 -0
  195. package/scripts/game-build.d.mts +25 -0
  196. package/scripts/game-build.mjs +144 -0
  197. package/scripts/heavy-into-the-delivery.mjs +284 -40
  198. package/scripts/libras-avatar.json +680 -0
  199. package/scripts/libras-avatar.mjs +246 -0
  200. package/scripts/libras-glosses/gloss.py +95 -0
  201. package/scripts/libras-glosses/pyproject.toml +32 -0
  202. package/scripts/libras-glosses/uv.lock +781 -0
  203. package/scripts/libras-glosses.mjs +351 -0
  204. package/scripts/licences/OFL-1.1.txt +86 -0
  205. package/scripts/licences/UFL-1.0.txt +96 -0
  206. package/scripts/licences/fonts.mjs +389 -0
  207. package/scripts/licences/third-party.mjs +35 -21
  208. package/app/public/vendor/fonts/bitter-var-ext.woff2 +0 -0
  209. package/app/public/vendor/fonts/bitter-var.woff2 +0 -0
  210. package/app/public/vendor/fonts/bodonimoda-var-ext.woff2 +0 -0
  211. package/app/public/vendor/fonts/bodonimoda-var.woff2 +0 -0
  212. package/app/public/vendor/fonts/cat-abril_fatface-400-ext.woff2 +0 -0
  213. package/app/public/vendor/fonts/cat-abril_fatface-400.woff2 +0 -0
  214. package/app/public/vendor/fonts/cat-adlam_display-400-ext.woff2 +0 -0
  215. package/app/public/vendor/fonts/cat-adlam_display-400.woff2 +0 -0
  216. package/app/public/vendor/fonts/cat-agu_display-400-ext.woff2 +0 -0
  217. package/app/public/vendor/fonts/cat-agu_display-400.woff2 +0 -0
  218. package/app/public/vendor/fonts/cat-alfa_slab_one-400-ext.woff2 +0 -0
  219. package/app/public/vendor/fonts/cat-alfa_slab_one-400.woff2 +0 -0
  220. package/app/public/vendor/fonts/cat-are_you_serious-400-ext.woff2 +0 -0
  221. package/app/public/vendor/fonts/cat-are_you_serious-400.woff2 +0 -0
  222. package/app/public/vendor/fonts/cat-asimovian-400-ext.woff2 +0 -0
  223. package/app/public/vendor/fonts/cat-asimovian-400.woff2 +0 -0
  224. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400-ext.woff2 +0 -0
  225. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400.woff2 +0 -0
  226. package/app/public/vendor/fonts/cat-bakbak_one-400-ext.woff2 +0 -0
  227. package/app/public/vendor/fonts/cat-bakbak_one-400.woff2 +0 -0
  228. package/app/public/vendor/fonts/cat-bangers-400-ext.woff2 +0 -0
  229. package/app/public/vendor/fonts/cat-bangers-400.woff2 +0 -0
  230. package/app/public/vendor/fonts/cat-barriecito-400-ext.woff2 +0 -0
  231. package/app/public/vendor/fonts/cat-barriecito-400.woff2 +0 -0
  232. package/app/public/vendor/fonts/cat-berkshire_swash-400-ext.woff2 +0 -0
  233. package/app/public/vendor/fonts/cat-berkshire_swash-400.woff2 +0 -0
  234. package/app/public/vendor/fonts/cat-bevan-400-ext.woff2 +0 -0
  235. package/app/public/vendor/fonts/cat-bevan-400.woff2 +0 -0
  236. package/app/public/vendor/fonts/cat-bitcount-400-ext.woff2 +0 -0
  237. package/app/public/vendor/fonts/cat-bitcount-400.woff2 +0 -0
  238. package/app/public/vendor/fonts/cat-bowlby_one_sc-400-ext.woff2 +0 -0
  239. package/app/public/vendor/fonts/cat-bowlby_one_sc-400.woff2 +0 -0
  240. package/app/public/vendor/fonts/cat-bungee_shade-400-ext.woff2 +0 -0
  241. package/app/public/vendor/fonts/cat-bungee_shade-400.woff2 +0 -0
  242. package/app/public/vendor/fonts/cat-charm-400-ext.woff2 +0 -0
  243. package/app/public/vendor/fonts/cat-charm-400.woff2 +0 -0
  244. package/app/public/vendor/fonts/cat-charm-700-ext.woff2 +0 -0
  245. package/app/public/vendor/fonts/cat-charm-700.woff2 +0 -0
  246. package/app/public/vendor/fonts/cat-cherry_bomb_one-400-ext.woff2 +0 -0
  247. package/app/public/vendor/fonts/cat-cherry_bomb_one-400.woff2 +0 -0
  248. package/app/public/vendor/fonts/cat-chokokutai-400-ext.woff2 +0 -0
  249. package/app/public/vendor/fonts/cat-chokokutai-400.woff2 +0 -0
  250. package/app/public/vendor/fonts/cat-chonburi-400-ext.woff2 +0 -0
  251. package/app/public/vendor/fonts/cat-chonburi-400.woff2 +0 -0
  252. package/app/public/vendor/fonts/cat-climate_crisis-400-ext.woff2 +0 -0
  253. package/app/public/vendor/fonts/cat-climate_crisis-400.woff2 +0 -0
  254. package/app/public/vendor/fonts/cat-cookie-400.woff2 +0 -0
  255. package/app/public/vendor/fonts/cat-dela_gothic_one-400-ext.woff2 +0 -0
  256. package/app/public/vendor/fonts/cat-dela_gothic_one-400.woff2 +0 -0
  257. package/app/public/vendor/fonts/cat-fascinate_inline-400-ext.woff2 +0 -0
  258. package/app/public/vendor/fonts/cat-fascinate_inline-400.woff2 +0 -0
  259. package/app/public/vendor/fonts/cat-ga_maamli-400-ext.woff2 +0 -0
  260. package/app/public/vendor/fonts/cat-ga_maamli-400.woff2 +0 -0
  261. package/app/public/vendor/fonts/cat-itim-400-ext.woff2 +0 -0
  262. package/app/public/vendor/fonts/cat-itim-400.woff2 +0 -0
  263. package/app/public/vendor/fonts/cat-jacquard_24-400-ext.woff2 +0 -0
  264. package/app/public/vendor/fonts/cat-jacquard_24-400.woff2 +0 -0
  265. package/app/public/vendor/fonts/cat-kablammo-400-ext.woff2 +0 -0
  266. package/app/public/vendor/fonts/cat-kablammo-400.woff2 +0 -0
  267. package/app/public/vendor/fonts/cat-kalam-400-ext.woff2 +0 -0
  268. package/app/public/vendor/fonts/cat-kalam-400.woff2 +0 -0
  269. package/app/public/vendor/fonts/cat-kalam-700-ext.woff2 +0 -0
  270. package/app/public/vendor/fonts/cat-kalam-700.woff2 +0 -0
  271. package/app/public/vendor/fonts/cat-katibeh-400-ext.woff2 +0 -0
  272. package/app/public/vendor/fonts/cat-katibeh-400.woff2 +0 -0
  273. package/app/public/vendor/fonts/cat-lemonada-400-ext.woff2 +0 -0
  274. package/app/public/vendor/fonts/cat-lemonada-400.woff2 +0 -0
  275. package/app/public/vendor/fonts/cat-lilita_one-400-ext.woff2 +0 -0
  276. package/app/public/vendor/fonts/cat-lilita_one-400.woff2 +0 -0
  277. package/app/public/vendor/fonts/cat-lobster-400-ext.woff2 +0 -0
  278. package/app/public/vendor/fonts/cat-lobster-400.woff2 +0 -0
  279. package/app/public/vendor/fonts/cat-luckiest_guy-400-ext.woff2 +0 -0
  280. package/app/public/vendor/fonts/cat-luckiest_guy-400.woff2 +0 -0
  281. package/app/public/vendor/fonts/cat-marhey-400-ext.woff2 +0 -0
  282. package/app/public/vendor/fonts/cat-marhey-400.woff2 +0 -0
  283. package/app/public/vendor/fonts/cat-modak-400-ext.woff2 +0 -0
  284. package/app/public/vendor/fonts/cat-modak-400.woff2 +0 -0
  285. package/app/public/vendor/fonts/cat-momo_signature-400-ext.woff2 +0 -0
  286. package/app/public/vendor/fonts/cat-momo_signature-400.woff2 +0 -0
  287. package/app/public/vendor/fonts/cat-monofett-400-ext.woff2 +0 -0
  288. package/app/public/vendor/fonts/cat-monofett-400.woff2 +0 -0
  289. package/app/public/vendor/fonts/cat-monomakh-400-ext.woff2 +0 -0
  290. package/app/public/vendor/fonts/cat-monomakh-400.woff2 +0 -0
  291. package/app/public/vendor/fonts/cat-monoton-400-ext.woff2 +0 -0
  292. package/app/public/vendor/fonts/cat-monoton-400.woff2 +0 -0
  293. package/app/public/vendor/fonts/cat-nerko_one-400-ext.woff2 +0 -0
  294. package/app/public/vendor/fonts/cat-nerko_one-400.woff2 +0 -0
  295. package/app/public/vendor/fonts/cat-noto_sans_adlam-400-ext.woff2 +0 -0
  296. package/app/public/vendor/fonts/cat-noto_sans_adlam-400.woff2 +0 -0
  297. package/app/public/vendor/fonts/cat-noto_sans_arabic-400-ext.woff2 +0 -0
  298. package/app/public/vendor/fonts/cat-noto_sans_arabic-400.woff2 +0 -0
  299. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400-ext.woff2 +0 -0
  300. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400.woff2 +0 -0
  301. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400-ext.woff2 +0 -0
  302. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400.woff2 +0 -0
  303. package/app/public/vendor/fonts/cat-noto_sans_jp-400-ext.woff2 +0 -0
  304. package/app/public/vendor/fonts/cat-noto_sans_jp-400.woff2 +0 -0
  305. package/app/public/vendor/fonts/cat-noto_sans_khmer-400-ext.woff2 +0 -0
  306. package/app/public/vendor/fonts/cat-noto_sans_khmer-400.woff2 +0 -0
  307. package/app/public/vendor/fonts/cat-noto_sans_kr-400-ext.woff2 +0 -0
  308. package/app/public/vendor/fonts/cat-noto_sans_kr-400.woff2 +0 -0
  309. package/app/public/vendor/fonts/cat-noto_sans_sc-400-ext.woff2 +0 -0
  310. package/app/public/vendor/fonts/cat-noto_sans_sc-400.woff2 +0 -0
  311. package/app/public/vendor/fonts/cat-noto_sans_thai-400-ext.woff2 +0 -0
  312. package/app/public/vendor/fonts/cat-noto_sans_thai-400.woff2 +0 -0
  313. package/app/public/vendor/fonts/cat-ole-400-ext.woff2 +0 -0
  314. package/app/public/vendor/fonts/cat-ole-400.woff2 +0 -0
  315. package/app/public/vendor/fonts/cat-pacifico-400-ext.woff2 +0 -0
  316. package/app/public/vendor/fonts/cat-pacifico-400.woff2 +0 -0
  317. package/app/public/vendor/fonts/cat-patrick_hand-400-ext.woff2 +0 -0
  318. package/app/public/vendor/fonts/cat-patrick_hand-400.woff2 +0 -0
  319. package/app/public/vendor/fonts/cat-petit_formal_script-400-ext.woff2 +0 -0
  320. package/app/public/vendor/fonts/cat-petit_formal_script-400.woff2 +0 -0
  321. package/app/public/vendor/fonts/cat-pixelify_sans-400-ext.woff2 +0 -0
  322. package/app/public/vendor/fonts/cat-pixelify_sans-400.woff2 +0 -0
  323. package/app/public/vendor/fonts/cat-plaster-400-ext.woff2 +0 -0
  324. package/app/public/vendor/fonts/cat-plaster-400.woff2 +0 -0
  325. package/app/public/vendor/fonts/cat-playwrite_ar_guides-400-0.woff2 +0 -0
  326. package/app/public/vendor/fonts/cat-playwrite_at-400-0.woff2 +0 -0
  327. package/app/public/vendor/fonts/cat-playwrite_at_guides-400-0.woff2 +0 -0
  328. package/app/public/vendor/fonts/cat-playwrite_au_nsw-400-0.woff2 +0 -0
  329. package/app/public/vendor/fonts/cat-playwrite_au_nsw_guides-400-0.woff2 +0 -0
  330. package/app/public/vendor/fonts/cat-playwrite_au_qld-400-0.woff2 +0 -0
  331. package/app/public/vendor/fonts/cat-playwrite_au_qld_guides-400-0.woff2 +0 -0
  332. package/app/public/vendor/fonts/cat-playwrite_au_sa-400-0.woff2 +0 -0
  333. package/app/public/vendor/fonts/cat-playwrite_au_sa_guides-400-0.woff2 +0 -0
  334. package/app/public/vendor/fonts/cat-playwrite_au_tas-400-0.woff2 +0 -0
  335. package/app/public/vendor/fonts/cat-playwrite_au_tas_guides-400-0.woff2 +0 -0
  336. package/app/public/vendor/fonts/cat-playwrite_au_vic-400-0.woff2 +0 -0
  337. package/app/public/vendor/fonts/cat-playwrite_au_vic_guides-400-0.woff2 +0 -0
  338. package/app/public/vendor/fonts/cat-playwrite_be_vlg-400-0.woff2 +0 -0
  339. package/app/public/vendor/fonts/cat-playwrite_be_vlg_guides-400-0.woff2 +0 -0
  340. package/app/public/vendor/fonts/cat-playwrite_be_wal-400-0.woff2 +0 -0
  341. package/app/public/vendor/fonts/cat-playwrite_be_wal_guides-400-0.woff2 +0 -0
  342. package/app/public/vendor/fonts/cat-playwrite_br_guides-400-0.woff2 +0 -0
  343. package/app/public/vendor/fonts/cat-playwrite_ca_guides-400-0.woff2 +0 -0
  344. package/app/public/vendor/fonts/cat-playwrite_cl_guides-400-0.woff2 +0 -0
  345. package/app/public/vendor/fonts/cat-playwrite_co_guides-400-0.woff2 +0 -0
  346. package/app/public/vendor/fonts/cat-playwrite_cu_guides-400-0.woff2 +0 -0
  347. package/app/public/vendor/fonts/cat-playwrite_cz-400-0.woff2 +0 -0
  348. package/app/public/vendor/fonts/cat-playwrite_cz_guides-400-0.woff2 +0 -0
  349. package/app/public/vendor/fonts/cat-playwrite_de_grund-400-0.woff2 +0 -0
  350. package/app/public/vendor/fonts/cat-playwrite_de_grund_guides-400-0.woff2 +0 -0
  351. package/app/public/vendor/fonts/cat-playwrite_de_la-400-0.woff2 +0 -0
  352. package/app/public/vendor/fonts/cat-playwrite_de_la_guides-400-0.woff2 +0 -0
  353. package/app/public/vendor/fonts/cat-playwrite_de_sas-400-0.woff2 +0 -0
  354. package/app/public/vendor/fonts/cat-playwrite_de_sas_guides-400-0.woff2 +0 -0
  355. package/app/public/vendor/fonts/cat-playwrite_de_va-400-0.woff2 +0 -0
  356. package/app/public/vendor/fonts/cat-playwrite_de_va_guides-400-0.woff2 +0 -0
  357. package/app/public/vendor/fonts/cat-playwrite_dk_loopet-400-0.woff2 +0 -0
  358. package/app/public/vendor/fonts/cat-playwrite_dk_loopet_guides-400-0.woff2 +0 -0
  359. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet-400-0.woff2 +0 -0
  360. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet_guides-400-0.woff2 +0 -0
  361. package/app/public/vendor/fonts/cat-playwrite_es_deco_guides-400-0.woff2 +0 -0
  362. package/app/public/vendor/fonts/cat-playwrite_es_guides-400-0.woff2 +0 -0
  363. package/app/public/vendor/fonts/cat-playwrite_fr_moderne-400-0.woff2 +0 -0
  364. package/app/public/vendor/fonts/cat-playwrite_fr_moderne_guides-400-0.woff2 +0 -0
  365. package/app/public/vendor/fonts/cat-playwrite_fr_trad-400-0.woff2 +0 -0
  366. package/app/public/vendor/fonts/cat-playwrite_fr_trad_guides-400-0.woff2 +0 -0
  367. package/app/public/vendor/fonts/cat-playwrite_gb_j_guides-400-0.woff2 +0 -0
  368. package/app/public/vendor/fonts/cat-playwrite_gb_s_guides-400-0.woff2 +0 -0
  369. package/app/public/vendor/fonts/cat-playwrite_hr-400-0.woff2 +0 -0
  370. package/app/public/vendor/fonts/cat-playwrite_hr_guides-400-0.woff2 +0 -0
  371. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva-400-0.woff2 +0 -0
  372. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva_guides-400-0.woff2 +0 -0
  373. package/app/public/vendor/fonts/cat-playwrite_hu-400-0.woff2 +0 -0
  374. package/app/public/vendor/fonts/cat-playwrite_hu_guides-400-0.woff2 +0 -0
  375. package/app/public/vendor/fonts/cat-playwrite_id-400-0.woff2 +0 -0
  376. package/app/public/vendor/fonts/cat-playwrite_id_guides-400-0.woff2 +0 -0
  377. package/app/public/vendor/fonts/cat-playwrite_ie-400-0.woff2 +0 -0
  378. package/app/public/vendor/fonts/cat-playwrite_ie_guides-400-0.woff2 +0 -0
  379. package/app/public/vendor/fonts/cat-playwrite_in-400-0.woff2 +0 -0
  380. package/app/public/vendor/fonts/cat-playwrite_in_guides-400-0.woff2 +0 -0
  381. package/app/public/vendor/fonts/cat-playwrite_is-400-0.woff2 +0 -0
  382. package/app/public/vendor/fonts/cat-playwrite_is_guides-400-0.woff2 +0 -0
  383. package/app/public/vendor/fonts/cat-playwrite_it_moderna-400-0.woff2 +0 -0
  384. package/app/public/vendor/fonts/cat-playwrite_it_moderna_guides-400-0.woff2 +0 -0
  385. package/app/public/vendor/fonts/cat-playwrite_it_trad-400-0.woff2 +0 -0
  386. package/app/public/vendor/fonts/cat-playwrite_it_trad_guides-400-0.woff2 +0 -0
  387. package/app/public/vendor/fonts/cat-playwrite_mx_guides-400-0.woff2 +0 -0
  388. package/app/public/vendor/fonts/cat-playwrite_ng_modern-400-0.woff2 +0 -0
  389. package/app/public/vendor/fonts/cat-playwrite_ng_modern_guides-400-0.woff2 +0 -0
  390. package/app/public/vendor/fonts/cat-playwrite_nl-400-0.woff2 +0 -0
  391. package/app/public/vendor/fonts/cat-playwrite_nl_guides-400-0.woff2 +0 -0
  392. package/app/public/vendor/fonts/cat-playwrite_no-400-0.woff2 +0 -0
  393. package/app/public/vendor/fonts/cat-playwrite_no_guides-400-0.woff2 +0 -0
  394. package/app/public/vendor/fonts/cat-playwrite_nz-400-0.woff2 +0 -0
  395. package/app/public/vendor/fonts/cat-playwrite_nz_basic-400-0.woff2 +0 -0
  396. package/app/public/vendor/fonts/cat-playwrite_nz_basic_guides-400-0.woff2 +0 -0
  397. package/app/public/vendor/fonts/cat-playwrite_nz_guides-400-0.woff2 +0 -0
  398. package/app/public/vendor/fonts/cat-playwrite_pe_guides-400-0.woff2 +0 -0
  399. package/app/public/vendor/fonts/cat-playwrite_pl-400-0.woff2 +0 -0
  400. package/app/public/vendor/fonts/cat-playwrite_pl_guides-400-0.woff2 +0 -0
  401. package/app/public/vendor/fonts/cat-playwrite_pt_guides-400-0.woff2 +0 -0
  402. package/app/public/vendor/fonts/cat-playwrite_ro-400-0.woff2 +0 -0
  403. package/app/public/vendor/fonts/cat-playwrite_ro_guides-400-0.woff2 +0 -0
  404. package/app/public/vendor/fonts/cat-playwrite_sk-400-0.woff2 +0 -0
  405. package/app/public/vendor/fonts/cat-playwrite_sk_guides-400-0.woff2 +0 -0
  406. package/app/public/vendor/fonts/cat-playwrite_tz-400-0.woff2 +0 -0
  407. package/app/public/vendor/fonts/cat-playwrite_tz_guides-400-0.woff2 +0 -0
  408. package/app/public/vendor/fonts/cat-playwrite_us_modern_guides-400-0.woff2 +0 -0
  409. package/app/public/vendor/fonts/cat-playwrite_us_trad_guides-400-0.woff2 +0 -0
  410. package/app/public/vendor/fonts/cat-playwrite_vn-400-0.woff2 +0 -0
  411. package/app/public/vendor/fonts/cat-playwrite_vn_guides-400-0.woff2 +0 -0
  412. package/app/public/vendor/fonts/cat-playwrite_za-400-0.woff2 +0 -0
  413. package/app/public/vendor/fonts/cat-playwrite_za_guides-400-0.woff2 +0 -0
  414. package/app/public/vendor/fonts/cat-potta_one-400-ext.woff2 +0 -0
  415. package/app/public/vendor/fonts/cat-potta_one-400.woff2 +0 -0
  416. package/app/public/vendor/fonts/cat-ranchers-400-ext.woff2 +0 -0
  417. package/app/public/vendor/fonts/cat-ranchers-400.woff2 +0 -0
  418. package/app/public/vendor/fonts/cat-ranga-400-ext.woff2 +0 -0
  419. package/app/public/vendor/fonts/cat-ranga-400.woff2 +0 -0
  420. package/app/public/vendor/fonts/cat-ranga-700-ext.woff2 +0 -0
  421. package/app/public/vendor/fonts/cat-ranga-700.woff2 +0 -0
  422. package/app/public/vendor/fonts/cat-risque-400-ext.woff2 +0 -0
  423. package/app/public/vendor/fonts/cat-risque-400.woff2 +0 -0
  424. package/app/public/vendor/fonts/cat-rowdies-400-ext.woff2 +0 -0
  425. package/app/public/vendor/fonts/cat-rowdies-400.woff2 +0 -0
  426. package/app/public/vendor/fonts/cat-rowdies-700-ext.woff2 +0 -0
  427. package/app/public/vendor/fonts/cat-rowdies-700.woff2 +0 -0
  428. package/app/public/vendor/fonts/cat-ruge_boogie-400-ext.woff2 +0 -0
  429. package/app/public/vendor/fonts/cat-ruge_boogie-400.woff2 +0 -0
  430. package/app/public/vendor/fonts/cat-ruslan_display-400-ext.woff2 +0 -0
  431. package/app/public/vendor/fonts/cat-ruslan_display-400.woff2 +0 -0
  432. package/app/public/vendor/fonts/cat-sacramento-400-ext.woff2 +0 -0
  433. package/app/public/vendor/fonts/cat-sacramento-400.woff2 +0 -0
  434. package/app/public/vendor/fonts/cat-sancreek-400-ext.woff2 +0 -0
  435. package/app/public/vendor/fonts/cat-sancreek-400.woff2 +0 -0
  436. package/app/public/vendor/fonts/cat-sankofa_display-400-ext.woff2 +0 -0
  437. package/app/public/vendor/fonts/cat-sankofa_display-400.woff2 +0 -0
  438. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400-ext.woff2 +0 -0
  439. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400.woff2 +0 -0
  440. package/app/public/vendor/fonts/cat-shantell_sans-400-ext.woff2 +0 -0
  441. package/app/public/vendor/fonts/cat-shantell_sans-400.woff2 +0 -0
  442. package/app/public/vendor/fonts/cat-smokum-400-ext.woff2 +0 -0
  443. package/app/public/vendor/fonts/cat-smokum-400.woff2 +0 -0
  444. package/app/public/vendor/fonts/cat-solitreo-400-ext.woff2 +0 -0
  445. package/app/public/vendor/fonts/cat-solitreo-400.woff2 +0 -0
  446. package/app/public/vendor/fonts/cat-spicy_rice-400-ext.woff2 +0 -0
  447. package/app/public/vendor/fonts/cat-spicy_rice-400.woff2 +0 -0
  448. package/app/public/vendor/fonts/cat-sriracha-400-ext.woff2 +0 -0
  449. package/app/public/vendor/fonts/cat-sriracha-400.woff2 +0 -0
  450. package/app/public/vendor/fonts/cat-srisakdi-400-ext.woff2 +0 -0
  451. package/app/public/vendor/fonts/cat-srisakdi-400.woff2 +0 -0
  452. package/app/public/vendor/fonts/cat-srisakdi-700-ext.woff2 +0 -0
  453. package/app/public/vendor/fonts/cat-srisakdi-700.woff2 +0 -0
  454. package/app/public/vendor/fonts/cat-stix_two_math-400-0.woff2 +0 -0
  455. package/app/public/vendor/fonts/cat-tillana-400-ext.woff2 +0 -0
  456. package/app/public/vendor/fonts/cat-tillana-400.woff2 +0 -0
  457. package/app/public/vendor/fonts/cat-tillana-700-ext.woff2 +0 -0
  458. package/app/public/vendor/fonts/cat-tillana-700.woff2 +0 -0
  459. package/app/public/vendor/fonts/cat-titan_one-400-ext.woff2 +0 -0
  460. package/app/public/vendor/fonts/cat-titan_one-400.woff2 +0 -0
  461. package/app/public/vendor/fonts/cat-yatra_one-400-ext.woff2 +0 -0
  462. package/app/public/vendor/fonts/cat-yatra_one-400.woff2 +0 -0
  463. package/app/public/vendor/fonts/cat-yomogi-400-ext.woff2 +0 -0
  464. package/app/public/vendor/fonts/cat-yomogi-400.woff2 +0 -0
  465. package/app/public/vendor/fonts/dmserifdisplay-400-ext.woff2 +0 -0
  466. package/app/public/vendor/fonts/dmserifdisplay-400.woff2 +0 -0
  467. package/app/public/vendor/fonts/domine-var-ext.woff2 +0 -0
  468. package/app/public/vendor/fonts/domine-var.woff2 +0 -0
  469. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  470. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  471. package/app/public/vendor/fonts/fraunces-var-ext.woff2 +0 -0
  472. package/app/public/vendor/fonts/fraunces-var.woff2 +0 -0
  473. package/app/public/vendor/fonts/fredoka-var-ext.woff2 +0 -0
  474. package/app/public/vendor/fonts/fredoka-var.woff2 +0 -0
  475. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  476. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  477. package/app/public/vendor/fonts/jakarta-var-ext.woff2 +0 -0
  478. package/app/public/vendor/fonts/jakarta-var.woff2 +0 -0
  479. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  480. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  481. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  482. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  483. package/app/public/vendor/fonts/lora-var-ext.woff2 +0 -0
  484. package/app/public/vendor/fonts/lora-var.woff2 +0 -0
  485. package/app/public/vendor/fonts/merriweather-var-ext.woff2 +0 -0
  486. package/app/public/vendor/fonts/merriweather-var.woff2 +0 -0
  487. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  488. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  489. package/app/public/vendor/fonts/notosans-var-ext.woff2 +0 -0
  490. package/app/public/vendor/fonts/notosans-var.woff2 +0 -0
  491. package/app/public/vendor/fonts/nunito-var-ext.woff2 +0 -0
  492. package/app/public/vendor/fonts/nunito-var.woff2 +0 -0
  493. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  494. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  495. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  496. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  497. package/app/public/vendor/fonts/playfair-var-ext.woff2 +0 -0
  498. package/app/public/vendor/fonts/playfair-var.woff2 +0 -0
  499. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  500. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  501. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  502. package/app/public/vendor/fonts/quicksand-var-ext.woff2 +0 -0
  503. package/app/public/vendor/fonts/quicksand-var.woff2 +0 -0
  504. package/app/public/vendor/fonts/robotoflex-var-ext.woff2 +0 -0
  505. package/app/public/vendor/fonts/robotoflex-var.woff2 +0 -0
  506. package/app/public/vendor/fonts/sora-var-ext.woff2 +0 -0
  507. package/app/public/vendor/fonts/sora-var.woff2 +0 -0
  508. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  509. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  510. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  511. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  512. package/app/public/vendor/fonts/spacegrotesk-var-ext.woff2 +0 -0
  513. package/app/public/vendor/fonts/spacegrotesk-var.woff2 +0 -0
  514. package/app/public/vendor/fonts/spectral-400-ext.woff2 +0 -0
  515. package/app/public/vendor/fonts/spectral-400.woff2 +0 -0
  516. package/app/public/vendor/fonts/spectral-700-ext.woff2 +0 -0
  517. package/app/public/vendor/fonts/spectral-700.woff2 +0 -0
  518. package/app/public/vendor/fonts/teachers-var-ext.woff2 +0 -0
  519. package/app/public/vendor/fonts/teachers-var.woff2 +0 -0
  520. package/app/public/vendor/fonts/ubuntu-400-ext.woff2 +0 -0
  521. package/app/public/vendor/fonts/ubuntu-400.woff2 +0 -0
  522. package/app/public/vendor/fonts/ubuntu-700-ext.woff2 +0 -0
  523. package/app/public/vendor/fonts/ubuntu-700.woff2 +0 -0
  524. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  525. package/dist-pkg/platform/guide-intensity.d.ts +0 -39
  526. package/dist-pkg/platform/guide-intensity.js +0 -71
  527. package/dist-pkg/ui/latch-refusal.d.ts +0 -33
  528. package/dist-pkg/ui/latch-refusal.js +0 -60
@@ -1,64 +1,136 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  /**
3
- * Builds a deaf mode. The PERSON's choice, persisted — not an inference about a third-party widget — read from the store
4
- * here, at build, never at import (ADR-0232).
3
+ * Why a delivery that carries no Libras player cannot sign, and the fix — the ONE reason both `NO_INTERPRETER` and the free
4
+ * player give for it, so `problems` says the same thing whichever of them answered.
5
5
  */
6
- export function createLibras({ doc, win, store, now }) {
7
- let open = store.getBool('incl_libras', false);
8
- let vlOpen = false, node = null, busyUntil = 0, next = null;
6
+ export const LIBRAS_NOT_INSTALLED = 'a Libras player is not installed in this delivery — build it with `inclusionist-heavy <folder> --libras`';
7
+ /** The interpreter of a delivery that carries no Libras player (ADR-0234): it signs nothing and says why, and how to fix it. */
8
+ export const NO_INTERPRETER = {
9
+ sign: () => Promise.resolve({ signed: false, reason: LIBRAS_NOT_INSTALLED }),
10
+ hide: () => { },
11
+ dispose: () => { },
12
+ };
13
+ /** The line `problems` gets when the interpreter cannot sign: the subject, the child's cost and the fix (ADR-0169). */
14
+ export function signingUnavailableLine(reason) {
15
+ return `deaf mode's sign-language interpreter could not sign: ${reason}. A deaf child keeps the captions and the written `
16
+ + 'text, but nothing is signed when she presses the sonar — give deaf mode a Libras player that can sign here (ADR-0234)';
17
+ }
18
+ /** The line `problems` gets when the interpreter signed a text but left part of it out: what, the child's cost, the fix. */
19
+ export function signedInPartLine(unsigned) {
20
+ return `deaf mode's sign-language interpreter signed only part of what the sonar found: ${unsigned}. A deaf child reads `
21
+ + 'those words in the captions and is not signed them — give the player the signs or the letters it lacks (ADR-0234)';
22
+ }
23
+ /** Builds a deaf mode. The stored choice is read here, at build, never at import (ADR-0232). */
24
+ export function createDeafMode({ store, captionsSetting, t, interpreter, speak, caption, tell, report }) {
25
+ let on = store.getBool('incl_libras', false);
26
+ let disposed = false;
27
+ /** Whether the child has been told, since the mode last came on, that signing is unavailable — told once, not per press. */
28
+ let told = false;
29
+ /** The lines already in `problems`: a child pressing the sonar twenty times is one diagnosis, not twenty. */
30
+ const reported = new Set();
9
31
  const changeListeners = new Set();
10
- const say = (text) => {
11
- if (!vlOpen || !text)
32
+ /**
33
+ * The answer to one request, heard only while the mode that asked is still on and the root still alive. The first «no»
34
+ * is told through the announcer AND written after the sonar's text: she is deaf, and the announcer alone is heard.
35
+ */
36
+ const reportOnce = (line) => { if (!reported.has(line)) {
37
+ reported.add(line);
38
+ report(line);
39
+ } };
40
+ /** Signed, perhaps in part: what was left out is a diagnosis, not a notice — the child saw the rest signed. */
41
+ const signedPart = (unsigned) => { if (unsigned)
42
+ reportOnce(signedInPartLine(unsigned)); };
43
+ const heard = (text, result) => {
44
+ if (disposed || !on)
12
45
  return;
13
- const at = now();
14
- if (at < busyUntil) {
15
- next = text;
46
+ if (result.signed) {
47
+ signedPart(result.unsigned);
16
48
  return;
17
49
  }
18
- busyUntil = at + 4000;
19
- if (!node) {
20
- node = doc.createElement('span');
21
- node.setAttribute('aria-hidden', 'true');
22
- node.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;overflow:hidden;opacity:.01;z-index:1;pointer-events:auto';
23
- doc.body.appendChild(node);
24
- }
25
- node.textContent = text;
50
+ reportOnce(signingUnavailableLine(result.reason));
51
+ if (told)
52
+ return;
53
+ told = true;
54
+ const notice = t('sr.deaf.noSigning');
55
+ tell(notice);
56
+ caption(`${text} ${notice}`);
57
+ };
58
+ /** One request signed: captioned when its turn comes, so the caption and the hands say the same thing; ends when it is heard. */
59
+ const signOne = (text) => {
60
+ caption(text);
61
+ let answer;
26
62
  try {
27
- node.click();
63
+ answer = interpreter.sign(text);
64
+ }
65
+ catch (failure) {
66
+ answer = Promise.reject(failure);
28
67
  }
29
- catch (e) { /* noop */ }
30
- win.setTimeout(() => { const nx = next; next = null; busyUntil = 0; if (nx)
31
- say(nx); }, 4100);
68
+ return answer.then((result) => { heard(text, result); }, (failure) => {
69
+ heard(text, { signed: false, reason: failure instanceof Error ? failure.message : String(failure) });
70
+ });
32
71
  };
33
- const toggle = (t) => {
34
- open = !open;
35
- vlOpen = open;
36
- store.setBool('incl_libras', open);
37
- // A BEST-EFFORT attempt to wake the VLibras widget, if it is loaded. Failing here must not stop the mode from
38
- // turning on: the state is the person's choice, and the widget is only one possible translator for them.
39
- const b = doc.querySelector('[vw-access-button]');
40
- if (open && b) {
41
- try {
42
- b.click();
43
- }
44
- catch (e) { /* noop */ }
72
+ /** The players' requests waiting behind the one being signed, at most one per player, in the order they asked. */
73
+ const waiting = [];
74
+ /** Whose request is being signed, when it is a player's; `null` when none is, or when the one being signed named no player. */
75
+ let seatSigning = null;
76
+ /** Bumped by every request that takes the line, so one that ends after being overtaken does not sign the next in line. */
77
+ let line = 0;
78
+ /** Ends the line: nothing waits and nobody is being signed (the mode went off, or the root went away). */
79
+ const endLine = () => { line++; waiting.length = 0; seatSigning = null; };
80
+ const signInTurn = async (seat, text) => {
81
+ const mine = ++line;
82
+ for (let next = { seat, text }; next; next = waiting.shift()) {
83
+ seatSigning = next.seat;
84
+ await signOne(next.text);
85
+ if (mine !== line)
86
+ return; // overtaken: the line is the newer request's now
45
87
  }
46
- else if (!open) {
47
- try {
48
- win.dispatchEvent(new CustomEvent('vp-widget-close'));
49
- }
50
- catch (e) { /* noop */ }
88
+ seatSigning = null;
89
+ };
90
+ const toggle = () => {
91
+ on = !on;
92
+ store.setBool('incl_libras', on);
93
+ if (on)
94
+ told = false;
95
+ else {
96
+ endLine();
97
+ interpreter.hide();
51
98
  }
52
99
  for (const fn of [...changeListeners])
53
100
  fn();
54
- if (open)
55
- say(t('sr.libras.on'));
101
+ };
102
+ /**
103
+ * THE INTERPRETER TAKES TURNS, as the narration does (ADR-0234, errata 2026-09-26 — the Dev: «Sim»): a request from ANOTHER
104
+ * player waits until the one being signed ends — replacing that player's own waiting one, in its place — and a player's newer
105
+ * request replaces that player's own, at once, the others waiting behind it. A request that names no player takes the line.
106
+ */
107
+ const sonar = (text, seat) => {
108
+ if (!on) {
109
+ speak(text, seat);
110
+ return;
111
+ }
112
+ if (seat !== undefined && seatSigning !== null && seat !== seatSigning) {
113
+ const own = waiting.find((w) => w.seat === seat);
114
+ if (own)
115
+ own.text = text;
116
+ else
117
+ waiting.push({ seat, text });
118
+ return;
119
+ }
120
+ if (seat === undefined || seatSigning === null)
121
+ waiting.length = 0;
122
+ void signInTurn(seat ?? null, text);
56
123
  };
57
124
  return {
58
- isOpen: () => open,
125
+ isOn: () => on,
59
126
  toggle,
60
- say,
61
- tick: () => { vlOpen = open; },
127
+ captionsOn: () => on || captionsSetting(),
128
+ sonar,
62
129
  onChange: (fn) => { changeListeners.add(fn); return () => { changeListeners.delete(fn); }; },
130
+ dispose: () => {
131
+ disposed = true;
132
+ endLine();
133
+ interpreter.dispose();
134
+ },
63
135
  };
64
136
  }
@@ -13,8 +13,18 @@ export interface VoiceControlDeps {
13
13
  /** The child's language (`core/i18n.bcp47`): it chooses the model AND the words. */
14
14
  readonly language: () => string;
15
15
  readonly controller: VirtualController;
16
- /** The names the open menu is showing right now, if any (ADR-0194). */
16
+ /** The names the open menu is showing right now, if any (ADR-0194) — `ui/menu-nav.itemNames`. */
17
17
  readonly menuWords: () => readonly string[];
18
+ /**
19
+ * Puts the open menu's cursor on the item with this name, WITHOUT activating it — `ui/menu-nav.pointAt`. REQUIRED: it is
20
+ * what makes a name heard reach its item. `false` when that item is no longer there; then nothing is confirmed.
21
+ */
22
+ readonly pointAt: (name: string) => boolean;
23
+ /**
24
+ * ONE BUTTON ONLY is on (ADR-0218 §4): whatever the child says is her switch, one press the controller takes as «take the one
25
+ * shown» — so a name heard is not a place and nothing is pointed at. REQUIRED: without it a name is a cursor move AND a take.
26
+ */
27
+ readonly oneButtonOnly: () => boolean;
18
28
  readonly say: (text: string) => void;
19
29
  readonly alert: (text: string) => void;
20
30
  readonly report: (line: string) => void;
@@ -23,6 +33,8 @@ export interface VoiceControlDeps {
23
33
  readonly after: (fn: () => void, ms: number) => void;
24
34
  readonly hasFile: VoskDeps['hasFile'];
25
35
  readonly loadBundle: VoskDeps['loadBundle'];
36
+ /** The host's `fetch`, handed on to read the loaded model's vocabulary (ADR-0194 §4). Without it an unsayable name goes unreported. */
37
+ readonly fetch?: VoskDeps['fetch'];
26
38
  readonly getUserMedia: VoiceListenerDeps['getUserMedia'];
27
39
  readonly createContext: VoiceListenerDeps['createContext'];
28
40
  readonly loadRuntime?: (deps: VoskDeps) => Promise<VoskLoad>;
@@ -42,5 +54,15 @@ export interface VoiceControl extends SwitchableControl {
42
54
  * because the next start already reads the new language.
43
55
  */
44
56
  languageChanged(): Promise<void>;
57
+ /**
58
+ * LENDS THIS EAR TO A CHOICE AMONG THE GAME'S OPTIONS (ADR-0256): while it is listening in `language`, the grammar becomes
59
+ * `grammar` and every text heard goes to `onHeard` instead of being obeyed; `stop()` gives the menu's grammar back. `null` when
60
+ * it is not listening, or listens in another language — then the choice opens an ear of its own.
61
+ * 📏 Without it every choice paused this recogniser and opened another: two workers and two microphones per press, 40 MiB of
62
+ * model unpacked each time, measured on 2026-09-27 — and the Dev's machine froze.
63
+ */
64
+ borrow(language: string, grammar: readonly string[], onHeard: (text: string, final: boolean) => void): {
65
+ stop(): Promise<void>;
66
+ } | null;
45
67
  }
46
68
  export declare function createVoiceControl(d: VoiceControlDeps): VoiceControl;
@@ -9,14 +9,29 @@
9
9
  // 📌 THE GRAMMAR FOLLOWS THE MENU (ADR-0194): saying the name of an item activates it, so the words the open menu is showing
10
10
  // join the closed grammar while it is open and leave with it.
11
11
  //
12
- // ⚠️ A SPOKEN COMMAND IS A TAP, NOT A HOLD. The word arrives, the position is pressed and let go — and the latch (ADR-0211,
13
- // always on for speech) is what keeps a direction held afterwards. That division is the whole reason the latch is forced there:
14
- // a child who says «acima» cannot also say «and keep holding it».
12
+ // 🎯 A NAME HEARD IS A CURSOR AND A CONFIRM, NOT A CLICK (ADR-0194 §2): the root's menu navigation puts the cursor on the
13
+ // named item (`pointAt`), and the confirm position is pressed on the virtual controller like any other spoken word — so the
14
+ // item is activated by the same path, with the same spoken feedback, as a child confirming it with the cursor on it. WHEN a
15
+ // name fires is the reader's (`input/voice-map` over `platform/speech-recognition`): at once, unless another item's name
16
+ // continues it («voltar» / «voltar ao jogo»), which waits for the end of the utterance (§3). A LOCKED item's name is heard
17
+ // too and takes the same path: the confirm reaches the item's own press, which says its reason and does nothing (§5, ADR-0161).
18
+ // A name with a word the loaded model lacks cannot be heard at all, and is a line of `problems` (§4).
19
+ // ⚠️ EXCEPT WITH ONE BUTTON ONLY ON (ADR-0218 §4): then every word heard, a name included, is the child's switch and nothing more
20
+ // — one press, which the controller takes as «take the one shown», and no cursor put anywhere first.
21
+ //
22
+ // ⚠️ A SPOKEN COMMAND IS A TAP, NOT A HOLD. The word arrives, the position is pressed and let go — one press, one release.
23
+ // Whether that tap keeps a direction going is the latch's, and on speech the latch starts as the GAME answers `holdsKeys()`
24
+ // (ADR-0249, resolved by the root when the press arrives): a platform game keeps walking on «direita», a quiz moves once on
25
+ // «abaixo», and the child may change either for her voice.
15
26
  import { voiceGrammar, createVoiceCommands } from '../input/voice-map.js';
16
27
  import { loadVoskRuntime } from '../platform/vosk-runtime.js';
17
28
  import { startVoiceListening } from '../platform/voice-listener.js';
29
+ import { spokenText } from '../platform/speech-recognition.js';
30
+ import { MENU_CONFIRM } from './menu-intent.js';
18
31
  /** How long a spoken position stays pressed. The same pulse the scan uses: long enough for a game to see a press and a release. */
19
32
  export const VOICE_PULSE_MS = 400;
33
+ /** The base of a language tag: `en-US` and `en` are the same model. */
34
+ const baseOf = (tag) => tag.split('-')[0].toLowerCase();
20
35
  export function createVoiceControl(d) {
21
36
  const { t } = d;
22
37
  const loadRuntime = d.loadRuntime ?? loadVoskRuntime;
@@ -25,15 +40,77 @@ export function createVoiceControl(d) {
25
40
  let on = false, starting = null;
26
41
  let listener = null;
27
42
  let commands = null;
28
- const grammarNow = () => voiceGrammar(d.language(), d.menuWords());
43
+ let grammarGiven = '';
44
+ /** The words the model now listening knows, once read (`platform/vosk-vocabulary`); `null` while unknown. */
45
+ let vocabulary = null;
46
+ /** Bumped by every stop, so a vocabulary that arrives for a recogniser already replaced is dropped. */
47
+ let generation = 0;
48
+ /**
49
+ * 🔴 THE LANGUAGE THE LISTENING STARTED IN, read ONCE. The model and the grammar must come from the same reading: `start()` used
50
+ * to read the language again after the model had loaded, and a page booting in English (pt-BR until its stored language
51
+ * applies) opened the Portuguese model with the English grammar — 15 «missing in vocabulary», measured on 2026-09-27.
52
+ */
53
+ let listeningLanguage = '';
54
+ /** A choice among the game's options borrowing this ear (ADR-0256): what is heard goes to it, and no command is obeyed. */
55
+ let lent = null;
56
+ /** The open menu's names that can enter the grammar. A one-letter name stays out: in a closed grammar every short noise lands on it. */
57
+ const sayableNames = () => d.menuWords().filter((n) => spokenText(n).length > 1);
58
+ /**
59
+ * 🔴 A NAME THE MODEL CANNOT HEAR IS SAID TO THE ADULT (ADR-0194 §4, ADR-0169): Kaldi drops a grammar word its vocabulary lacks,
60
+ * in silence, and the item goes mute for the child — she can still walk to it with the direction words. One line per item and
61
+ * language, naming the words, the cost and the fix. Nothing is said while the vocabulary is unknown: no line beats a false one.
62
+ */
63
+ const reportUnsayable = (names) => {
64
+ const known = vocabulary;
65
+ if (!known)
66
+ return;
67
+ const language = listeningLanguage;
68
+ for (const name of names) {
69
+ const lacking = [...new Set(spokenText(name).split(' '))].filter((w) => w && !known.has(w));
70
+ const kind = `unsayable:${language}:${name}`;
71
+ if (!lacking.length || said.has(kind))
72
+ continue;
73
+ said.add(kind);
74
+ d.report(`voice control: the menu item "${name}" has ${lacking.length === 1 ? 'a word' : 'words'} the ${language} speech `
75
+ + `model does not know (${lacking.map((w) => `"${w}"`).join(', ')}) — the child cannot choose this item by saying its name, `
76
+ + `only by walking to it with the direction words; reword the item's label in the ${language} dictionary with words the `
77
+ + 'model knows (ADR-0194 §4)');
78
+ }
79
+ };
80
+ /** The open menu's names, handed to the reader and checked against the model, and the grammar with them. */
81
+ const followMenu = () => {
82
+ const names = sayableNames();
83
+ commands?.items(names);
84
+ reportUnsayable(names);
85
+ return voiceGrammar(listeningLanguage, names);
86
+ };
29
87
  const command = (action) => {
30
88
  d.controller.press(action, 'fala');
31
89
  d.after(() => d.controller.release(action, 'fala'), VOICE_PULSE_MS);
32
90
  };
91
+ /*
92
+ * 🔴 ONE WORD HEARD IS ONE ACTION (ADR-0111 errata, ADR-0218 §4). With one button only on, the controller takes every press as
93
+ * «take the one shown», so a word may do nothing BESIDES its press: asked here, first, for every word — a position, a name, any
94
+ * kind a later reader adds. 📏 Before, a name was still pointed at: with the chip on «próximo» the cursor jumped to the named
95
+ * item and then stepped on from it; on «cancelar» the press meant nothing and the cursor had moved anyway.
96
+ */
97
+ const obey = (heard) => {
98
+ for (const c of heard ?? []) {
99
+ if (d.oneButtonOnly())
100
+ command(c.kind === 'position' ? c.action : MENU_CONFIRM);
101
+ else if (c.kind === 'position')
102
+ command(c.action);
103
+ else if (d.pointAt(c.name))
104
+ command(MENU_CONFIRM);
105
+ }
106
+ };
33
107
  const stop = () => {
34
108
  const going = listener;
35
109
  listener = null;
36
110
  commands = null;
111
+ lent = null;
112
+ vocabulary = null;
113
+ generation += 1;
37
114
  void going?.stop();
38
115
  };
39
116
  /**
@@ -60,7 +137,9 @@ export function createVoiceControl(d) {
60
137
  let load;
61
138
  const language = d.language();
62
139
  try {
63
- load = await loadRuntime({ base: d.base, language, hasFile: d.hasFile, loadBundle: d.loadBundle });
140
+ load = await loadRuntime({
141
+ base: d.base, language, hasFile: d.hasFile, loadBundle: d.loadBundle, ...(d.fetch ? { fetch: d.fetch } : {}),
142
+ });
64
143
  }
65
144
  catch (e) {
66
145
  failed('runtime', `voice control: the recogniser did not open (${e instanceof Error ? e.message : String(e)}) — the child `
@@ -69,26 +148,40 @@ export function createVoiceControl(d) {
69
148
  }
70
149
  if (!load.ok) {
71
150
  /*
72
- * 📌 ONE LINE PER LANGUAGE, NAMING THAT LANGUAGE'S FIX (ADR-0225, ADR-0169). The install fetches the command model of the
73
- * BOOT language only, so a child who switches can ask for one the delivery never carried; «open it online» alone sends
74
- * the adult after a download that cannot happen. The reading's line names `--reading <language>` the same way.
151
+ * 📌 ONE LINE PER LANGUAGE, NAMING THAT LANGUAGE'S FIX (ADR-0225 and its erratum, ADR-0169). The delivery carries every
152
+ * language unless its `--commands` list narrowed it, and the install fetches every one the delivery carries — so a model
153
+ * missing here was left out of the delivery or has not come down yet, and «open it online» alone sends the adult after a
154
+ * download that cannot happen in the first case. The reading's line names its two fixes the same way.
75
155
  */
76
156
  const lang = language.split('-')[0].toLowerCase();
77
157
  failed(`files:${lang}`, `voice control: ${load.missing.join(', ')} not on this device for ${language} — the child cannot play `
78
- + `by speaking in this language; build the delivery with \`npx inclusionist-heavy --commands ${lang}\` and open the game `
79
- + 'once online in this language so the install fetches them', t('sr.voice.needsInternet'));
158
+ + `by speaking in this language; build the delivery with \`npx inclusionist-heavy\` without \`--commands\` (it carries every `
159
+ + `language) or with \`--commands ${lang}\` in its list, and open the game once online so the install fetches them`, t('sr.voice.needsInternet'));
160
+ return;
161
+ }
162
+ // the language changed while the model loaded (a page booting before its stored language applies): start again in it
163
+ if (baseOf(d.language()) !== baseOf(language)) {
164
+ await start();
80
165
  return;
81
166
  }
82
- commands = createVoiceCommands(d.language());
167
+ listeningLanguage = language;
168
+ commands = createVoiceCommands(language);
169
+ const grammar = followMenu();
170
+ grammarGiven = grammar.join('\n');
83
171
  try {
84
172
  listener = await listen({
85
173
  model: load.model,
86
- grammar: grammarNow(),
174
+ grammar,
87
175
  getUserMedia: d.getUserMedia,
88
176
  createContext: d.createContext,
89
- onPartial: (text) => { const a = commands?.partial(text); if (a)
90
- command(a); },
91
- onFinal: () => commands?.reset(),
177
+ onPartial: (text) => { if (lent)
178
+ lent(text, false);
179
+ else
180
+ obey(commands?.partial(text)); },
181
+ onFinal: (text) => { if (lent)
182
+ lent(text, true);
183
+ else
184
+ obey(commands?.final(text)); },
92
185
  });
93
186
  }
94
187
  catch {
@@ -102,7 +195,18 @@ export function createVoiceControl(d) {
102
195
  stop();
103
196
  return;
104
197
  }
198
+ // a menu that opened or closed while the microphone was opening changed the names after the grammar above was built
199
+ api.refreshGrammar();
105
200
  d.say(t('sr.voice.ready'));
201
+ // 📌 THE VOCABULARY IS READ AFTER THE CHILD CAN SPEAK, never before: it is a report for the adult, and reading the model's
202
+ // archive a second time must not delay the first command. It checks the names showing when it arrives and every later menu.
203
+ const mine = generation;
204
+ void load.vocabulary?.().then((words) => {
205
+ if (mine !== generation || !listener)
206
+ return;
207
+ vocabulary = words;
208
+ reportUnsayable(sayableNames());
209
+ });
106
210
  };
107
211
  const api = {
108
212
  async apply(next) {
@@ -117,7 +221,17 @@ export function createVoiceControl(d) {
117
221
  await starting;
118
222
  },
119
223
  refreshGrammar() {
120
- listener?.setGrammar(grammarNow());
224
+ // lent to a choice: the options are the grammar until it gives the ear back
225
+ if (!listener || lent)
226
+ return;
227
+ // 📌 A new grammar is a new recogniser (`platform/voice-listener`), and the root asks on every menu change it sees —
228
+ // most of which change no name. Only a grammar that differs is handed on.
229
+ const grammar = followMenu();
230
+ const given = grammar.join('\n');
231
+ if (given === grammarGiven)
232
+ return;
233
+ grammarGiven = given;
234
+ listener.setGrammar(grammar);
121
235
  },
122
236
  async languageChanged() {
123
237
  /*
@@ -135,9 +249,28 @@ export function createVoiceControl(d) {
135
249
  // ⚠️ A START IN FLIGHT IS IN THE OLD LANGUAGE. Letting it finish and replacing it costs one opening; skipping it would
136
250
  // leave `starting` to install the old model on top of the new one, which is the race the `if (!on)` in `start` exists for.
137
251
  await starting;
252
+ // a start that noticed the change itself is already listening in the new language: nothing to redo
253
+ if (listener && baseOf(listeningLanguage) === baseOf(d.language()))
254
+ return;
138
255
  stop();
139
256
  await api.apply(true);
140
257
  },
258
+ borrow(language, grammar, onHeard) {
259
+ if (!listener || lent || baseOf(language) !== baseOf(listeningLanguage))
260
+ return null;
261
+ const held = listener;
262
+ lent = onHeard;
263
+ held.setGrammar(grammar);
264
+ grammarGiven = '';
265
+ return {
266
+ async stop() {
267
+ if (lent !== onHeard)
268
+ return;
269
+ lent = null;
270
+ api.refreshGrammar(); // the menu's grammar back
271
+ },
272
+ };
273
+ },
141
274
  };
142
275
  return api;
143
276
  }
@@ -47,8 +47,7 @@ export interface VoicePorts {
47
47
  /** Speak the sample through the BROWSER's own synthesiser — the neural engine speaks through `tts` instead. */
48
48
  readonly speakSample: (text: string, chosen: SpeechSynthesisVoice | null) => void;
49
49
  /** Called again whenever the browser finishes loading its voice list: they arrive late, and often empty first.
50
- * ⚠️ The parameter is gain and not
51
- un, which is a GAME ACTION word this layer does not say (the boundary of 2026-09-06). */
50
+ * ⚠️ The parameter is `again` and not `run`, which is a GAME ACTION word this layer does not say (the boundary of 2026-09-06). */
52
51
  readonly whenVoicesChange: (again: () => void) => void;
53
52
  }
54
53
  /**
@@ -90,7 +89,7 @@ export interface VoiceSettingsCtx {
90
89
  export interface VoiceSettings {
91
90
  /** Everything the section shows, refreshed from the stored state. Called by the panel's own render. */
92
91
  render: () => void;
93
- /** Just the narration switch and the chosen engine. Exported because the quick bar's icon reflects it from outside. */
92
+ /** Just the narration switch, its volume and the chosen engine. Exported because the root reflects it from outside. */
94
93
  reflectTts: () => void;
95
94
  }
96
95
  export declare function createVoiceSettings(ctx: VoiceSettingsCtx, ports: VoicePorts): VoiceSettings;
@@ -34,8 +34,11 @@ function voiceName(v) {
34
34
  function panelVoiceLabel(v) {
35
35
  return (v.recommended ? '❤️ ' : '') + voiceName(v);
36
36
  }
37
- /** The rows a missing voice locks (ADR-0185 §4): narration, its volume and rate, the spoken index, and the voice. */
38
- const SPEECH_ROWS = ['#opt-tts', '#tts-vol', '#tts-ppm', '#opt-menuindex', '#tts-voz'];
37
+ /**
38
+ * The rows a missing voice locks (ADR-0185 §4): narration, its volume and rate, the spoken index, the voice — and the «test voice»
39
+ * button a page may carry, which with no voice of the language would ask the browser for one of another (ADR-0243 §3).
40
+ */
41
+ const SPEECH_ROWS = ['#opt-tts', '#tts-vol', '#tts-ppm', '#opt-menuindex', '#tts-voz', '#opt-tts-test'];
39
42
  export function createVoiceSettings(ctx, ports) {
40
43
  const { t, bcp47 } = ctx.translator;
41
44
  function reflectTts() {
@@ -46,6 +49,10 @@ export function createVoiceSettings(ctx, ports) {
46
49
  ctx.toggleBtn(b, on);
47
50
  b.textContent = toggleLabel(t, on);
48
51
  }
52
+ // and its volume: the slider was never drawn from the mixer, so it opened at the range's middle whatever was stored
53
+ const vol = ctx.$('#tts-vol');
54
+ if (vol && cat?.tts)
55
+ vol.value = String(volPercent(cat.tts.vol));
49
56
  const e = ctx.$('#tts-engine');
50
57
  if (e)
51
58
  e.value = ctx.tts.getEngineSel();
@@ -95,6 +102,8 @@ export function createVoiceSettings(ctx, ports) {
95
102
  ctx.tts.setVoiceObj(pick);
96
103
  }
97
104
  const noVoice = () => !!ctx.tts.voices && ctx.tts.voices().length === 0;
105
+ /** The lock last drawn on the speech rows; `null` before it is first drawn. Compared when the voices change. */
106
+ let lockDrawn = null;
98
107
  /** The speech rate list: each step «N PPM», the stored one selected (ADR-0183 §1, ADR-0196). */
99
108
  function renderRate() {
100
109
  const sel = ctx.$('#tts-ppm');
@@ -110,11 +119,15 @@ export function createVoiceSettings(ctx, ports) {
110
119
  }
111
120
  sel.value = String(ctx.settings.speechPpm);
112
121
  }
122
+ /** The voices of the language as ids, one per line: what the «Voz» list was last drawn from. */
123
+ const voiceIds = () => (ctx.tts.voices?.() ?? []).map((v) => v.voice).join('\n');
124
+ let listDrawn = null;
113
125
  function renderVoiceList() {
114
126
  const sel = ctx.$('#tts-voz');
115
127
  if (!sel || !ctx.tts.voices)
116
128
  return;
117
129
  const list = ctx.tts.voices();
130
+ listDrawn = voiceIds();
118
131
  sel.replaceChildren(); // options built node by node: no markup sink
119
132
  if (!list.length) {
120
133
  const o = ports.newOption();
@@ -136,6 +149,7 @@ export function createVoiceSettings(ctx, ports) {
136
149
  */
137
150
  function lockSpeechRows() {
138
151
  const lock = noVoice();
152
+ lockDrawn = lock;
139
153
  for (const id of SPEECH_ROWS) {
140
154
  const el = ctx.$(id);
141
155
  if (!el)
@@ -284,7 +298,43 @@ export function createVoiceSettings(ctx, ports) {
284
298
  });
285
299
  }
286
300
  wireControls();
287
- ports.whenVoicesChange(populateTtsVoices);
301
+ /**
302
+ * A LOCK THAT FLIPS UNDER THE CURSOR IS TOLD, and only there (ADR-0185 erratum 2026-09-26). The footer is written with what
303
+ * focusing the row would show NOW — the reason, or the row's own explanation — because it would otherwise go on saying «no
304
+ * voice» under a row that works. Locking is heard once, through the footer, which is `aria-live`, as when a locked row is
305
+ * focused. Unlocking gets one sentence more: the footer then gives the row's explanation, which does not say the state
306
+ * changed, and whether a removed `aria-disabled` on the focused control is spoken is left to each screen reader.
307
+ */
308
+ function tellUnderCursor() {
309
+ const el = SPEECH_ROWS.map((id) => ctx.$(id)).find((e) => !!e && e.ownerDocument?.activeElement === e);
310
+ if (!el)
311
+ return;
312
+ const footer = el.closest('.overlay__card')?.querySelector('.opt-explain');
313
+ if (lockDrawn) {
314
+ if (footer)
315
+ footer.textContent = el.dataset.motivo ?? '';
316
+ return;
317
+ }
318
+ if (footer)
319
+ footer.textContent = el.closest('.ctrl-row')?.dataset.explain || (footer.dataset.idle ?? '');
320
+ ctx.srSay(t('audio.comVoz'));
321
+ }
322
+ /**
323
+ * 🔴 THE DEVICE'S VOICES CHANGED (ADR-0185 §4, erratum 2026-09-26): a device may list them a moment after load — empty first,
324
+ * then full — and may lose them. The lock is a fact about those voices, so it is evaluated again here, both ways, IN PLACE:
325
+ * attributes on the same nodes, never a redraw, so the focus stays where the child left it. Only a change is drawn or told:
326
+ * the voice list when its voices differ, the rows and the cursor's row when the lock flips.
327
+ */
328
+ function voicesChanged() {
329
+ populateTtsVoices();
330
+ if (voiceIds() !== listDrawn)
331
+ renderVoiceList(); // the same voices keep the same options, which the child may be reading
332
+ const before = lockDrawn;
333
+ lockSpeechRows();
334
+ if (lockDrawn !== before)
335
+ tellUnderCursor();
336
+ }
337
+ ports.whenVoicesChange(voicesChanged);
288
338
  return {
289
339
  render: () => {
290
340
  reflectTts();
@@ -21,7 +21,7 @@
21
21
  import { navigableItems } from './menu-items.js';
22
22
  import { announceItem } from './item-announcement.js';
23
23
  import { controlParts } from './menu-nav.js';
24
- import { PM_VISIBLE_ITEMS } from './pause-icons.js';
24
+ import { PM_VISIBLE_ITEMS, LIST_DOOR } from './pause-icons.js';
25
25
  import { accessibleLabel } from '../core/accessible-label.js';
26
26
  /** Title and item joined the way a screen reader reads them: the place first, then where the cursor is in it. */
27
27
  const say = (title, item) => [title, item].filter(Boolean).join('. ');
@@ -41,7 +41,7 @@ function panelPlace(panel, ctx) {
41
41
  function cardPlace(card, ctx) {
42
42
  const sub = card.querySelector('.pause-menu:not([hidden])')?.dataset.sub ?? 'raiz';
43
43
  // a submenu is named by the item that opens it; the root by the card's title
44
- const door = sub === 'opcoes' ? 'options' : sub === 'jogo' ? 'opcoesdojogo' : null;
44
+ const door = LIST_DOOR[sub] ?? null;
45
45
  const button = door ? card.querySelector(`.pm-btn[data-act="${door}"]`) : null;
46
46
  const title = button ? accessibleLabel(button) : (card.querySelector('h2')?.textContent?.trim() ?? '');
47
47
  const items = [...card.querySelectorAll(PM_VISIBLE_ITEMS)];