@the-inclusionist/engine 9.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 (532) hide show
  1. package/README.md +120 -78
  2. package/app/css/style.css +761 -377
  3. package/app/public/vendor/fonts/playwrite-cu.woff2 +0 -0
  4. package/app/public/vendor/fonts/playwrite-es-deco.woff2 +0 -0
  5. package/app/public/vendor/fonts/playwrite-es.woff2 +0 -0
  6. package/app/public/vendor/fonts/playwrite-gb-j.woff2 +0 -0
  7. package/app/public/vendor/fonts/playwrite-gb-s.woff2 +0 -0
  8. package/app/public/vendor/fonts/playwrite-pe.woff2 +0 -0
  9. package/app/public/vendor/fonts/playwrite-pt.woff2 +0 -0
  10. package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
  11. package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
  12. package/app/public/vendor/fonts.css +36 -84
  13. package/dist-pkg/boot/create-game.d.ts +499 -219
  14. package/dist-pkg/boot/create-game.js +3944 -562
  15. package/dist-pkg/core/a11y-sr.d.ts +25 -3
  16. package/dist-pkg/core/a11y-sr.js +38 -19
  17. package/dist-pkg/core/accessible-label.d.ts +17 -0
  18. package/dist-pkg/core/accessible-label.js +37 -0
  19. package/dist-pkg/core/accommodation-subjects.d.ts +26 -0
  20. package/dist-pkg/core/accommodation-subjects.js +32 -0
  21. package/dist-pkg/core/accommodations.d.ts +125 -0
  22. package/dist-pkg/core/accommodations.js +195 -0
  23. package/dist-pkg/core/actions.d.ts +108 -55
  24. package/dist-pkg/core/actions.js +122 -71
  25. package/dist-pkg/core/calm-mode.d.ts +31 -0
  26. package/dist-pkg/core/calm-mode.js +47 -0
  27. package/dist-pkg/core/camera-cycle.d.ts +15 -0
  28. package/dist-pkg/core/camera-cycle.js +7 -0
  29. package/dist-pkg/core/caption-duration.d.ts +15 -0
  30. package/dist-pkg/core/caption-duration.js +19 -0
  31. package/dist-pkg/core/cartridge-problems.d.ts +34 -0
  32. package/dist-pkg/core/cartridge-problems.js +53 -0
  33. package/dist-pkg/core/constants.d.ts +0 -54
  34. package/dist-pkg/core/constants.js +9 -76
  35. package/dist-pkg/core/contract.d.ts +142 -164
  36. package/dist-pkg/core/contract.js +222 -208
  37. package/dist-pkg/core/dom-query.d.ts +4 -4
  38. package/dist-pkg/core/dom-query.js +12 -20
  39. package/dist-pkg/core/entity.d.ts +62 -77
  40. package/dist-pkg/core/entity.js +18 -32
  41. package/dist-pkg/core/escape-html.d.ts +9 -10
  42. package/dist-pkg/core/escape-html.js +16 -20
  43. package/dist-pkg/core/flash-threshold.d.ts +17 -0
  44. package/dist-pkg/core/flash-threshold.js +113 -0
  45. package/dist-pkg/core/game-speed.d.ts +11 -0
  46. package/dist-pkg/core/game-speed.js +16 -0
  47. package/dist-pkg/core/genres.d.ts +20 -0
  48. package/dist-pkg/core/genres.js +44 -0
  49. package/dist-pkg/core/i18n.d.ts +71 -60
  50. package/dist-pkg/core/i18n.js +186 -200
  51. package/dist-pkg/core/loop.d.ts +17 -6
  52. package/dist-pkg/core/loop.js +26 -26
  53. package/dist-pkg/core/pause-icon-catalogue.d.ts +15 -0
  54. package/dist-pkg/core/pause-icon-catalogue.js +54 -0
  55. package/dist-pkg/core/ring.d.ts +17 -0
  56. package/dist-pkg/core/ring.js +29 -0
  57. package/dist-pkg/core/rng.d.ts +7 -12
  58. package/dist-pkg/core/rng.js +23 -53
  59. package/dist-pkg/core/route.d.ts +26 -26
  60. package/dist-pkg/core/route.js +126 -121
  61. package/dist-pkg/core/scenes.d.ts +37 -40
  62. package/dist-pkg/core/scenes.js +41 -43
  63. package/dist-pkg/core/screens.d.ts +4 -4
  64. package/dist-pkg/core/screens.js +11 -16
  65. package/dist-pkg/core/session-clock.d.ts +23 -0
  66. package/dist-pkg/core/session-clock.js +37 -0
  67. package/dist-pkg/core/setting-defaults.d.ts +61 -0
  68. package/dist-pkg/core/setting-defaults.js +66 -0
  69. package/dist-pkg/core/speech-rate.d.ts +29 -0
  70. package/dist-pkg/core/speech-rate.js +47 -0
  71. package/dist-pkg/core/state.d.ts +118 -118
  72. package/dist-pkg/core/state.js +322 -297
  73. package/dist-pkg/core/visual-cycles.d.ts +30 -0
  74. package/dist-pkg/core/visual-cycles.js +52 -0
  75. package/dist-pkg/core/visual-state.d.ts +13 -0
  76. package/dist-pkg/core/visual-state.js +6 -0
  77. package/dist-pkg/educational/activities-registry.d.ts +1 -1
  78. package/dist-pkg/educational/activities-registry.js +9 -8
  79. package/dist-pkg/educational/adaptive-engine.d.ts +11 -11
  80. package/dist-pkg/educational/adaptive-engine.js +17 -17
  81. package/dist-pkg/educational/segment-bar.d.ts +12 -12
  82. package/dist-pkg/educational/segment-bar.js +9 -9
  83. package/dist-pkg/i18n/en.js +324 -223
  84. package/dist-pkg/i18n/es.js +323 -223
  85. package/dist-pkg/i18n/pt.js +348 -226
  86. package/dist-pkg/input/default-bindings.d.ts +37 -38
  87. package/dist-pkg/input/default-bindings.js +127 -144
  88. package/dist-pkg/input/devices.d.ts +7 -7
  89. package/dist-pkg/input/devices.js +17 -16
  90. package/dist-pkg/input/edges.d.ts +23 -32
  91. package/dist-pkg/input/edges.js +23 -28
  92. package/dist-pkg/input/empathy-filter.d.ts +22 -0
  93. package/dist-pkg/input/empathy-filter.js +34 -0
  94. package/dist-pkg/input/face-map.d.ts +96 -0
  95. package/dist-pkg/input/face-map.js +255 -0
  96. package/dist-pkg/input/face-signals.d.ts +40 -0
  97. package/dist-pkg/input/face-signals.js +48 -0
  98. package/dist-pkg/input/gamepad.d.ts +195 -145
  99. package/dist-pkg/input/gamepad.js +383 -556
  100. package/dist-pkg/input/gaze-cycle.d.ts +64 -0
  101. package/dist-pkg/input/gaze-cycle.js +133 -0
  102. package/dist-pkg/input/gaze-relative.d.ts +68 -0
  103. package/dist-pkg/input/gaze-relative.js +142 -0
  104. package/dist-pkg/input/hand-map.d.ts +28 -0
  105. package/dist-pkg/input/hand-map.js +142 -0
  106. package/dist-pkg/input/input-cooldown.d.ts +17 -0
  107. package/dist-pkg/input/input-cooldown.js +39 -0
  108. package/dist-pkg/input/key-default.d.ts +30 -0
  109. package/dist-pkg/input/key-default.js +60 -0
  110. package/dist-pkg/input/keyboard-runtime.d.ts +15 -21
  111. package/dist-pkg/input/keyboard-runtime.js +23 -27
  112. package/dist-pkg/input/keyboard.d.ts +68 -63
  113. package/dist-pkg/input/keyboard.js +83 -105
  114. package/dist-pkg/input/keydown.d.ts +123 -137
  115. package/dist-pkg/input/keydown.js +284 -289
  116. package/dist-pkg/input/latch-edge.d.ts +33 -21
  117. package/dist-pkg/input/latch-edge.js +34 -35
  118. package/dist-pkg/input/latch-scope.d.ts +51 -50
  119. package/dist-pkg/input/latch-scope.js +61 -82
  120. package/dist-pkg/input/latch-store.d.ts +26 -30
  121. package/dist-pkg/input/latch-store.js +42 -50
  122. package/dist-pkg/input/latch-sync.d.ts +29 -29
  123. package/dist-pkg/input/latch-sync.js +25 -25
  124. package/dist-pkg/input/latch.d.ts +14 -17
  125. package/dist-pkg/input/latch.js +29 -37
  126. package/dist-pkg/input/pad-defaults.d.ts +13 -16
  127. package/dist-pkg/input/pad-defaults.js +21 -28
  128. package/dist-pkg/input/pad-reading.d.ts +76 -0
  129. package/dist-pkg/input/pad-reading.js +169 -0
  130. package/dist-pkg/input/pad-wizard.d.ts +99 -0
  131. package/dist-pkg/input/pad-wizard.js +256 -0
  132. package/dist-pkg/input/pointer-space.d.ts +19 -20
  133. package/dist-pkg/input/pointer-space.js +27 -32
  134. package/dist-pkg/input/pointer.d.ts +39 -40
  135. package/dist-pkg/input/pointer.js +42 -45
  136. package/dist-pkg/input/state.d.ts +101 -89
  137. package/dist-pkg/input/state.js +54 -134
  138. package/dist-pkg/input/switch-scan.d.ts +59 -0
  139. package/dist-pkg/input/switch-scan.js +67 -0
  140. package/dist-pkg/input/synthetic-source.d.ts +42 -0
  141. package/dist-pkg/input/synthetic-source.js +57 -0
  142. package/dist-pkg/input/touch-bindings.d.ts +127 -116
  143. package/dist-pkg/input/touch-bindings.js +158 -173
  144. package/dist-pkg/input/touch.d.ts +147 -72
  145. package/dist-pkg/input/touch.js +362 -126
  146. package/dist-pkg/input/transport-in-use.d.ts +107 -0
  147. package/dist-pkg/input/transport-in-use.js +130 -0
  148. package/dist-pkg/input/transports.d.ts +90 -117
  149. package/dist-pkg/input/transports.js +104 -106
  150. package/dist-pkg/input/virtual-controller.d.ts +82 -0
  151. package/dist-pkg/input/virtual-controller.js +69 -0
  152. package/dist-pkg/input/vocabulary-migration.d.ts +50 -49
  153. package/dist-pkg/input/vocabulary-migration.js +80 -79
  154. package/dist-pkg/input/voice-map.d.ts +59 -0
  155. package/dist-pkg/input/voice-map.js +123 -0
  156. package/dist-pkg/platform/audio-ambient.d.ts +13 -2
  157. package/dist-pkg/platform/audio-ambient.js +14 -14
  158. package/dist-pkg/platform/audio-earcons.d.ts +20 -19
  159. package/dist-pkg/platform/audio-earcons.js +51 -42
  160. package/dist-pkg/platform/audio-jingles.js +9 -10
  161. package/dist-pkg/platform/audio-mixer.d.ts +14 -2
  162. package/dist-pkg/platform/audio-mixer.js +31 -39
  163. package/dist-pkg/platform/audio-sonar.d.ts +49 -152
  164. package/dist-pkg/platform/audio-sonar.js +124 -302
  165. package/dist-pkg/platform/audio.d.ts +62 -19
  166. package/dist-pkg/platform/audio.js +193 -168
  167. package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
  168. package/dist-pkg/platform/choose-by-voice.js +157 -0
  169. package/dist-pkg/platform/flash-sampler.d.ts +19 -0
  170. package/dist-pkg/platform/flash-sampler.js +79 -0
  171. package/dist-pkg/platform/font-library.d.ts +77 -0
  172. package/dist-pkg/platform/font-library.js +98 -0
  173. package/dist-pkg/platform/font-library.json +721 -0
  174. package/dist-pkg/platform/heavy-catalogue.d.ts +77 -0
  175. package/dist-pkg/platform/heavy-catalogue.js +256 -0
  176. package/dist-pkg/platform/heavy-mirror.d.ts +20 -0
  177. package/dist-pkg/platform/heavy-mirror.js +73 -0
  178. package/dist-pkg/platform/heavy.d.ts +136 -0
  179. package/dist-pkg/platform/heavy.js +324 -0
  180. package/dist-pkg/platform/interruptible-speech.d.ts +23 -16
  181. package/dist-pkg/platform/interruptible-speech.js +77 -54
  182. package/dist-pkg/platform/kokoro-port.d.ts +27 -0
  183. package/dist-pkg/platform/kokoro-port.js +50 -0
  184. package/dist-pkg/platform/kokoro-runtime.d.ts +20 -0
  185. package/dist-pkg/platform/kokoro-runtime.js +52 -0
  186. package/dist-pkg/platform/kokoro.d.ts +70 -0
  187. package/dist-pkg/platform/kokoro.js +133 -0
  188. package/dist-pkg/platform/listener-scope.d.ts +12 -0
  189. package/dist-pkg/platform/listener-scope.js +90 -0
  190. package/dist-pkg/platform/locale-host.d.ts +14 -0
  191. package/dist-pkg/platform/locale-host.js +45 -0
  192. package/dist-pkg/platform/microphone.d.ts +51 -0
  193. package/dist-pkg/platform/microphone.js +118 -0
  194. package/dist-pkg/platform/onnx-runtime.d.ts +34 -0
  195. package/dist-pkg/platform/onnx-runtime.js +42 -0
  196. package/dist-pkg/platform/reading-in-worker.d.ts +64 -0
  197. package/dist-pkg/platform/reading-in-worker.js +125 -0
  198. package/dist-pkg/platform/reading-model.d.ts +94 -0
  199. package/dist-pkg/platform/reading-model.js +272 -0
  200. package/dist-pkg/platform/reading-runtime.d.ts +24 -0
  201. package/dist-pkg/platform/reading-runtime.js +164 -0
  202. package/dist-pkg/platform/reading-worker.d.ts +41 -0
  203. package/dist-pkg/platform/reading-worker.js +58 -0
  204. package/dist-pkg/platform/reading.d.ts +80 -0
  205. package/dist-pkg/platform/reading.js +136 -0
  206. package/dist-pkg/platform/speech-recognition.d.ts +60 -0
  207. package/dist-pkg/platform/speech-recognition.js +104 -0
  208. package/dist-pkg/platform/speech.d.ts +23 -1
  209. package/dist-pkg/platform/speech.js +29 -18
  210. package/dist-pkg/platform/storage-keys.d.ts +96 -0
  211. package/dist-pkg/platform/storage-keys.js +106 -0
  212. package/dist-pkg/platform/storage.d.ts +46 -111
  213. package/dist-pkg/platform/storage.js +104 -169
  214. package/dist-pkg/platform/tts.d.ts +85 -39
  215. package/dist-pkg/platform/tts.js +447 -127
  216. package/dist-pkg/platform/vision-loop.d.ts +23 -0
  217. package/dist-pkg/platform/vision-loop.js +56 -0
  218. package/dist-pkg/platform/vision.d.ts +132 -0
  219. package/dist-pkg/platform/vision.js +117 -0
  220. package/dist-pkg/platform/voice-listener.d.ts +40 -0
  221. package/dist-pkg/platform/voice-listener.js +88 -0
  222. package/dist-pkg/platform/voice-plan.d.ts +15 -98
  223. package/dist-pkg/platform/voice-plan.js +56 -143
  224. package/dist-pkg/platform/vosk-runtime.d.ts +96 -0
  225. package/dist-pkg/platform/vosk-runtime.js +151 -0
  226. package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
  227. package/dist-pkg/platform/vosk-vocabulary.js +178 -0
  228. package/dist-pkg/render/canvas.d.ts +13 -7
  229. package/dist-pkg/render/canvas.js +13 -13
  230. package/dist-pkg/render/crt.d.ts +48 -12
  231. package/dist-pkg/render/crt.js +78 -80
  232. package/dist-pkg/render/cvd-matrices.d.ts +22 -18
  233. package/dist-pkg/render/cvd-matrices.js +37 -38
  234. package/dist-pkg/render/hc-role-data.d.ts +7 -7
  235. package/dist-pkg/render/hc-role-data.js +14 -14
  236. package/dist-pkg/render/high-contrast.d.ts +67 -56
  237. package/dist-pkg/render/high-contrast.js +226 -217
  238. package/dist-pkg/render/low-vision-drawing.d.ts +5 -0
  239. package/dist-pkg/render/low-vision-drawing.js +46 -0
  240. package/dist-pkg/render/lq-filter.d.ts +32 -20
  241. package/dist-pkg/render/lq-filter.js +59 -65
  242. package/dist-pkg/render/port.d.ts +86 -88
  243. package/dist-pkg/render/port.js +17 -23
  244. package/dist-pkg/render/screen-pipeline.d.ts +42 -42
  245. package/dist-pkg/render/screen-pipeline.js +80 -87
  246. package/dist-pkg/render/sprite-fx.d.ts +2 -1
  247. package/dist-pkg/render/sprite-fx.js +16 -14
  248. package/dist-pkg/render/viewports.d.ts +20 -15
  249. package/dist-pkg/render/viewports.js +83 -114
  250. package/dist-pkg/render/viz-axes-labels.d.ts +47 -0
  251. package/dist-pkg/render/viz-axes-labels.js +88 -0
  252. package/dist-pkg/render/viz-axes.d.ts +89 -86
  253. package/dist-pkg/render/viz-axes.js +128 -116
  254. package/dist-pkg/render/viz-modes.d.ts +28 -29
  255. package/dist-pkg/render/viz-modes.js +40 -45
  256. package/dist-pkg/render/viz-refusal.d.ts +32 -0
  257. package/dist-pkg/render/viz-refusal.js +57 -0
  258. package/dist-pkg/render/viz-setters.d.ts +104 -66
  259. package/dist-pkg/render/viz-setters.js +182 -191
  260. package/dist-pkg/ui/aac-sets.d.ts +45 -0
  261. package/dist-pkg/ui/aac-sets.js +41 -0
  262. package/dist-pkg/ui/audio-choices.d.ts +68 -0
  263. package/dist-pkg/ui/audio-choices.js +98 -0
  264. package/dist-pkg/ui/audio-rows-that-apply.d.ts +12 -0
  265. package/dist-pkg/ui/audio-rows-that-apply.js +43 -0
  266. package/dist-pkg/ui/camera-control.d.ts +53 -0
  267. package/dist-pkg/ui/camera-control.js +48 -0
  268. package/dist-pkg/ui/changed-mark.d.ts +9 -8
  269. package/dist-pkg/ui/changed-mark.js +22 -53
  270. package/dist-pkg/ui/control-choices.d.ts +37 -0
  271. package/dist-pkg/ui/control-choices.js +84 -0
  272. package/dist-pkg/ui/debug-panel.d.ts +59 -43
  273. package/dist-pkg/ui/debug-panel.js +68 -69
  274. package/dist-pkg/ui/declared-words.d.ts +21 -0
  275. package/dist-pkg/ui/declared-words.js +66 -0
  276. package/dist-pkg/ui/dom.d.ts +10 -14
  277. package/dist-pkg/ui/dom.js +10 -47
  278. package/dist-pkg/ui/drawing-problems.d.ts +25 -0
  279. package/dist-pkg/ui/drawing-problems.js +94 -0
  280. package/dist-pkg/ui/eye-control.d.ts +36 -0
  281. package/dist-pkg/ui/eye-control.js +178 -0
  282. package/dist-pkg/ui/face-control.d.ts +25 -0
  283. package/dist-pkg/ui/face-control.js +183 -0
  284. package/dist-pkg/ui/focus-trap.d.ts +39 -49
  285. package/dist-pkg/ui/focus-trap.js +56 -65
  286. package/dist-pkg/ui/fonts.d.ts +157 -68
  287. package/dist-pkg/ui/fonts.js +305 -115
  288. package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
  289. package/dist-pkg/ui/footer-scroll-driver.js +201 -0
  290. package/dist-pkg/ui/footer-scroll.d.ts +38 -0
  291. package/dist-pkg/ui/footer-scroll.js +55 -0
  292. package/dist-pkg/ui/game-options.d.ts +47 -0
  293. package/dist-pkg/ui/game-options.js +158 -0
  294. package/dist-pkg/ui/gaze-overlay.d.ts +64 -0
  295. package/dist-pkg/ui/gaze-overlay.js +165 -0
  296. package/dist-pkg/ui/hand-control.d.ts +25 -0
  297. package/dist-pkg/ui/hand-control.js +143 -0
  298. package/dist-pkg/ui/help-panel.d.ts +87 -0
  299. package/dist-pkg/ui/help-panel.js +242 -0
  300. package/dist-pkg/ui/hud-bands.d.ts +62 -0
  301. package/dist-pkg/ui/hud-bands.js +146 -0
  302. package/dist-pkg/ui/hud-row.d.ts +34 -0
  303. package/dist-pkg/ui/hud-row.js +80 -0
  304. package/dist-pkg/ui/hud.d.ts +73 -73
  305. package/dist-pkg/ui/hud.js +94 -98
  306. package/dist-pkg/ui/item-announcement.d.ts +13 -12
  307. package/dist-pkg/ui/item-announcement.js +25 -26
  308. package/dist-pkg/ui/layout.d.ts +103 -28
  309. package/dist-pkg/ui/layout.js +117 -99
  310. package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
  311. package/dist-pkg/ui/libras-avatar-clip.js +86 -0
  312. package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
  313. package/dist-pkg/ui/libras-avatar-load.js +134 -0
  314. package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
  315. package/dist-pkg/ui/libras-avatar-plan.js +211 -0
  316. package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
  317. package/dist-pkg/ui/libras-avatar-player.js +226 -0
  318. package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
  319. package/dist-pkg/ui/libras-avatar-stage.js +208 -0
  320. package/dist-pkg/ui/libras-glosses.d.ts +28 -0
  321. package/dist-pkg/ui/libras-glosses.js +161 -0
  322. package/dist-pkg/ui/locale-flags.d.ts +16 -0
  323. package/dist-pkg/ui/locale-flags.js +30 -0
  324. package/dist-pkg/ui/loop-crash.d.ts +17 -17
  325. package/dist-pkg/ui/loop-crash.js +33 -62
  326. package/dist-pkg/ui/menu-intent.d.ts +61 -0
  327. package/dist-pkg/ui/menu-intent.js +88 -0
  328. package/dist-pkg/ui/menu-items.d.ts +20 -0
  329. package/dist-pkg/ui/menu-items.js +51 -0
  330. package/dist-pkg/ui/menu-nav.d.ts +98 -92
  331. package/dist-pkg/ui/menu-nav.js +398 -286
  332. package/dist-pkg/ui/mobility-choices.d.ts +26 -0
  333. package/dist-pkg/ui/mobility-choices.js +41 -0
  334. package/dist-pkg/ui/motion-choices.d.ts +31 -0
  335. package/dist-pkg/ui/motion-choices.js +68 -0
  336. package/dist-pkg/ui/motion-scene.d.ts +24 -19
  337. package/dist-pkg/ui/motion-scene.js +29 -61
  338. package/dist-pkg/ui/mount-panel.d.ts +81 -0
  339. package/dist-pkg/ui/mount-panel.js +61 -0
  340. package/dist-pkg/ui/panel-shell.d.ts +65 -31
  341. package/dist-pkg/ui/panel-shell.js +92 -66
  342. package/dist-pkg/ui/panel-widgets.d.ts +150 -0
  343. package/dist-pkg/ui/panel-widgets.js +294 -0
  344. package/dist-pkg/ui/pause-buttons.d.ts +35 -0
  345. package/dist-pkg/ui/pause-buttons.js +66 -0
  346. package/dist-pkg/ui/pause-icons.d.ts +261 -347
  347. package/dist-pkg/ui/pause-icons.js +671 -601
  348. package/dist-pkg/ui/pause-markup.d.ts +86 -0
  349. package/dist-pkg/ui/pause-markup.js +125 -0
  350. package/dist-pkg/ui/reach-notice.d.ts +22 -23
  351. package/dist-pkg/ui/reach-notice.js +55 -58
  352. package/dist-pkg/ui/scan-overlay.d.ts +23 -0
  353. package/dist-pkg/ui/scan-overlay.js +50 -0
  354. package/dist-pkg/ui/screen-text.d.ts +25 -0
  355. package/dist-pkg/ui/screen-text.js +152 -0
  356. package/dist-pkg/ui/session-clock.d.ts +32 -0
  357. package/dist-pkg/ui/session-clock.js +67 -0
  358. package/dist-pkg/ui/settings-aac.d.ts +70 -0
  359. package/dist-pkg/ui/settings-aac.js +208 -0
  360. package/dist-pkg/ui/settings-audio.d.ts +97 -114
  361. package/dist-pkg/ui/settings-audio.js +493 -425
  362. package/dist-pkg/ui/settings-controls.d.ts +53 -96
  363. package/dist-pkg/ui/settings-controls.js +143 -215
  364. package/dist-pkg/ui/settings-empathy.d.ts +39 -30
  365. package/dist-pkg/ui/settings-empathy.js +53 -60
  366. package/dist-pkg/ui/settings-mobility.d.ts +192 -0
  367. package/dist-pkg/ui/settings-mobility.js +332 -0
  368. package/dist-pkg/ui/settings-motion.d.ts +91 -70
  369. package/dist-pkg/ui/settings-motion.js +287 -198
  370. package/dist-pkg/ui/settings-panel.d.ts +58 -45
  371. package/dist-pkg/ui/settings-panel.js +189 -94
  372. package/dist-pkg/ui/settings-typo.d.ts +29 -78
  373. package/dist-pkg/ui/settings-typo.js +158 -129
  374. package/dist-pkg/ui/settings-visual.d.ts +29 -87
  375. package/dist-pkg/ui/settings-visual.js +240 -193
  376. package/dist-pkg/ui/shell.d.ts +112 -135
  377. package/dist-pkg/ui/shell.js +148 -217
  378. package/dist-pkg/ui/simulation-list.d.ts +27 -0
  379. package/dist-pkg/ui/simulation-list.js +76 -0
  380. package/dist-pkg/ui/simulation-over-the-world.d.ts +23 -0
  381. package/dist-pkg/ui/simulation-over-the-world.js +131 -0
  382. package/dist-pkg/ui/simulation-refusal.d.ts +5 -31
  383. package/dist-pkg/ui/simulation-refusal.js +8 -56
  384. package/dist-pkg/ui/switchable-control.d.ts +11 -0
  385. package/dist-pkg/ui/switchable-control.js +19 -0
  386. package/dist-pkg/ui/title.d.ts +3 -12
  387. package/dist-pkg/ui/title.js +49 -18
  388. package/dist-pkg/ui/top-band.d.ts +26 -0
  389. package/dist-pkg/ui/top-band.js +96 -0
  390. package/dist-pkg/ui/typo-choices.d.ts +71 -0
  391. package/dist-pkg/ui/typo-choices.js +85 -0
  392. package/dist-pkg/ui/visual-axes-panel.d.ts +11 -47
  393. package/dist-pkg/ui/visual-axes-panel.js +12 -94
  394. package/dist-pkg/ui/visual-choices.d.ts +77 -0
  395. package/dist-pkg/ui/visual-choices.js +107 -0
  396. package/dist-pkg/ui/vlibras.d.ts +77 -10
  397. package/dist-pkg/ui/vlibras.js +130 -81
  398. package/dist-pkg/ui/voice-control.d.ts +68 -0
  399. package/dist-pkg/ui/voice-control.js +276 -0
  400. package/dist-pkg/ui/voice-settings.d.ts +95 -0
  401. package/dist-pkg/ui/voice-settings.js +352 -0
  402. package/dist-pkg/ui/where-the-child-is.d.ts +31 -0
  403. package/dist-pkg/ui/where-the-child-is.js +70 -0
  404. package/docs/CREDITS.md +300 -44
  405. package/docs/LICENSES.md +189 -145
  406. package/package.json +51 -27
  407. package/scripts/check-cartridge.mjs +78 -0
  408. package/scripts/game-build.d.mts +25 -0
  409. package/scripts/game-build.mjs +144 -0
  410. package/scripts/heavy-into-the-delivery.mjs +388 -0
  411. package/scripts/libras-avatar.json +680 -0
  412. package/scripts/libras-avatar.mjs +246 -0
  413. package/scripts/libras-glosses/gloss.py +95 -0
  414. package/scripts/libras-glosses/pyproject.toml +32 -0
  415. package/scripts/libras-glosses/uv.lock +781 -0
  416. package/scripts/libras-glosses.mjs +351 -0
  417. package/scripts/licences/Apache-2.0.txt +176 -0
  418. package/scripts/licences/GPL-3.0.txt +674 -0
  419. package/scripts/licences/MIT.txt +17 -0
  420. package/scripts/licences/OFL-1.1.txt +86 -0
  421. package/scripts/licences/UFL-1.0.txt +96 -0
  422. package/scripts/licences/fonts.mjs +389 -0
  423. package/scripts/licences/third-party.mjs +301 -0
  424. package/scripts/licences/vosk-browser.NOTICE.txt +268 -0
  425. package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
  426. package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
  427. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  428. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  429. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  430. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  431. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  432. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  433. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  434. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  435. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  436. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  437. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  438. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  439. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  440. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  441. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  442. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  443. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  444. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  445. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  446. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  447. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  448. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  449. package/dist-pkg/core/anel.d.ts +0 -17
  450. package/dist-pkg/core/anel.js +0 -31
  451. package/dist-pkg/core/collision.d.ts +0 -21
  452. package/dist-pkg/core/collision.js +0 -62
  453. package/dist-pkg/core/layers.d.ts +0 -63
  454. package/dist-pkg/core/layers.js +0 -95
  455. package/dist-pkg/core/letter-grid.d.ts +0 -55
  456. package/dist-pkg/core/letter-grid.js +0 -116
  457. package/dist-pkg/core/password.d.ts +0 -34
  458. package/dist-pkg/core/password.js +0 -205
  459. package/dist-pkg/core/rotulo-acessivel.d.ts +0 -17
  460. package/dist-pkg/core/rotulo-acessivel.js +0 -41
  461. package/dist-pkg/core/run-state.d.ts +0 -115
  462. package/dist-pkg/core/run-state.js +0 -40
  463. package/dist-pkg/core/tiles.d.ts +0 -12
  464. package/dist-pkg/core/tiles.js +0 -61
  465. package/dist-pkg/core/world.d.ts +0 -4
  466. package/dist-pkg/core/world.js +0 -58
  467. package/dist-pkg/input/origem-sintetica.d.ts +0 -44
  468. package/dist-pkg/input/origem-sintetica.js +0 -60
  469. package/dist-pkg/input/transporte-em-uso.d.ts +0 -101
  470. package/dist-pkg/input/transporte-em-uso.js +0 -130
  471. package/dist-pkg/platform/audio-nav.d.ts +0 -51
  472. package/dist-pkg/platform/audio-nav.js +0 -99
  473. package/dist-pkg/platform/guide-intensity.d.ts +0 -40
  474. package/dist-pkg/platform/guide-intensity.js +0 -73
  475. package/dist-pkg/platform/pesados-catalogo.d.ts +0 -14
  476. package/dist-pkg/platform/pesados-catalogo.js +0 -177
  477. package/dist-pkg/platform/pesados.d.ts +0 -31
  478. package/dist-pkg/platform/pesados.js +0 -75
  479. package/dist-pkg/platform/vozes-prontas.d.ts +0 -28
  480. package/dist-pkg/platform/vozes-prontas.js +0 -61
  481. package/dist-pkg/render/camera.d.ts +0 -58
  482. package/dist-pkg/render/camera.js +0 -105
  483. package/dist-pkg/render/cenario-data.d.ts +0 -127
  484. package/dist-pkg/render/cenario-data.js +0 -145
  485. package/dist-pkg/render/city-tex.d.ts +0 -63
  486. package/dist-pkg/render/city-tex.js +0 -228
  487. package/dist-pkg/render/city-tiles.d.ts +0 -11
  488. package/dist-pkg/render/city-tiles.js +0 -140
  489. package/dist-pkg/render/draw.d.ts +0 -149
  490. package/dist-pkg/render/draw.js +0 -231
  491. package/dist-pkg/render/fx.d.ts +0 -63
  492. package/dist-pkg/render/fx.js +0 -123
  493. package/dist-pkg/render/minimap.d.ts +0 -11
  494. package/dist-pkg/render/minimap.js +0 -94
  495. package/dist-pkg/render/parallax.d.ts +0 -85
  496. package/dist-pkg/render/parallax.js +0 -158
  497. package/dist-pkg/render/player-anim.d.ts +0 -71
  498. package/dist-pkg/render/player-anim.js +0 -152
  499. package/dist-pkg/render/recycling-tex.d.ts +0 -48
  500. package/dist-pkg/render/recycling-tex.js +0 -164
  501. package/dist-pkg/render/scene-city.d.ts +0 -77
  502. package/dist-pkg/render/scene-city.js +0 -181
  503. package/dist-pkg/render/scene-parallax.d.ts +0 -72
  504. package/dist-pkg/render/scene-parallax.js +0 -426
  505. package/dist-pkg/render/scene-sky.d.ts +0 -126
  506. package/dist-pkg/render/scene-sky.js +0 -295
  507. package/dist-pkg/render/set-cenario.d.ts +0 -54
  508. package/dist-pkg/render/set-cenario.js +0 -103
  509. package/dist-pkg/render/textures.d.ts +0 -75
  510. package/dist-pkg/render/textures.js +0 -240
  511. package/dist-pkg/render/title-scene.d.ts +0 -46
  512. package/dist-pkg/render/title-scene.js +0 -75
  513. package/dist-pkg/render/weather.d.ts +0 -96
  514. package/dist-pkg/render/weather.js +0 -171
  515. package/dist-pkg/render/wheelchair-sprites.d.ts +0 -32
  516. package/dist-pkg/render/wheelchair-sprites.js +0 -69
  517. package/dist-pkg/render/world-tex.d.ts +0 -18
  518. package/dist-pkg/render/world-tex.js +0 -87
  519. package/dist-pkg/ui/activities-menu.d.ts +0 -258
  520. package/dist-pkg/ui/activities-menu.js +0 -648
  521. package/dist-pkg/ui/caa-sets.d.ts +0 -39
  522. package/dist-pkg/ui/caa-sets.js +0 -65
  523. package/dist-pkg/ui/latch-refusal.d.ts +0 -32
  524. package/dist-pkg/ui/latch-refusal.js +0 -60
  525. package/dist-pkg/ui/map-hub.d.ts +0 -56
  526. package/dist-pkg/ui/map-hub.js +0 -138
  527. package/dist-pkg/ui/settings-caa.d.ts +0 -45
  528. package/dist-pkg/ui/settings-caa.js +0 -137
  529. package/dist-pkg/ui/settings-motor.d.ts +0 -175
  530. package/dist-pkg/ui/settings-motor.js +0 -342
  531. package/dist-pkg/ui/webcam.d.ts +0 -7
  532. package/dist-pkg/ui/webcam.js +0 -93
@@ -1,137 +1,213 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- // ui/settings-audio — Audio panel (Estágio 4, #audio overlay): extracted from game.js's renderAudio()/
3
- // renderAudioSinks()/catRowHTML/wireCatControls/renderNavSound/reflectAudioMaster + the TTS-panel functions that
4
- // platform/tts.ts already documents as belonging here (reflectTTS/populateTTSEngines/populateTTSVoices). Pure
5
- // logic (category markup, volume->percent, sink option/label, cane-div validation, pt-BR voice filtering) is
6
- // separated from the thin DOM-touching render/wire functions. DI via initSettingsAudio(ctx): `$`, `srSay`,
7
- // `store` (narrow get/set), the live sound/mixer primitives from platform/audio.ts (getSoundOn/setSoundOn/
8
- // getVolume/setVolume/getAudioCat/setCatGain), the injected `tts` panel API (platform/tts.ts), and the SHARED
9
- // game.js helpers other panels also use (`toggleBtn`, `getNumPlayers`/`getPlayers`) or that live outside audio
10
- // entirely (`getModoCego`/`setModoCego`, `getCaneBlockDiv`/`setCaneBlockDiv` — core collision state; the widgets
11
- // live in this overlay, the state does not). Overlay open/close plumbing (#audio hidden toggle, frontOverlay,
12
- // focus management, Escape, `ensureAC()`) is the shared infra every settings panel uses and stays in game.js,
13
- // which calls `renderAudio()` from its `openAudio()`. `reflectModoCego`/`reflectTts` are also exported because
14
- // game.js's own `setModoCego()` and the pause-menu icon bar (`iconAct('tts'|'blind', …)`) call them directly.
2
+ // ui/settings-audio — the hearing accessibility and audio panels (#audio overlay): the thin DOM-touching render/wire
3
+ // functions; what a choice IS lives in ./audio-choices.js and the voice section in ./voice-settings.js. DI via
4
+ // initSettingsAudio(ctx): `$`, `srSay`, `store` (narrow get/set), the browser through three required ports
5
+ // (`newElement`, `speech`, `audioOutputs`), the live sound/mixer primitives from platform/audio.ts
6
+ // (getSoundOn/setSoundOn/getVolume/setVolume/getAudioCat/setCatGain), the injected `tts` panel API (platform/tts.ts), and
7
+ // the host's shared helpers (`toggleBtn`, `getNumPlayers`/`getPlayers`) plus state that lives outside audio
8
+ // (`getBlindMode`/`setBlindMode`, `getCaneBlockDiv`/`setCaneBlockDiv` — the widgets live in this overlay, the state does
9
+ // not). Overlay open/close plumbing (frontOverlay, focus management, Escape) is the shared infrastructure every settings
10
+ // panel uses. `reflectBlindMode`/`reflectTts` are exported because the pause-menu icon bar calls them directly.
15
11
  /** Minimal DOM-selector shape (matches ui/dom.ts's `$`). */
16
12
  import { toggleLabel } from './dom.js';
17
- import { t, bcp47 } from '../core/i18n.js';
18
- import { DEFAULTS } from '../core/state.js';
19
- // O módulo INTEIRO, e não os nomes soltos: `menuIndexOn` é ligação viva e `setMenuIndexOnValue` a muda — ler
20
- // pelo namespace deixa isso à vista em cada uso, em vez de parecer uma constante importada.
21
- import * as state from '../core/state.js';
13
+ import { DEFAULTS } from '../core/setting-defaults.js';
22
14
  import { markChanged, markMenuChanged } from './changed-mark.js';
23
15
  import { defaultAudioCat } from '../platform/audio-mixer.js';
24
- // ---------------------------------------------------------------------------------------------
25
- // Pure logic (no `document`, testable in node)
26
- // ---------------------------------------------------------------------------------------------
27
- /** Categorias de NAVEGAÇÃO SONORA (bengala/sonar/guarda/guia) — volume geral separado do som do jogo. */
28
- export const NAV_CATS = ['sonar', 'guard', 'guide'];
29
- /** Categorias GERAIS do jogo (TTS fica na seção Voz, fora desta lista). */
30
- export const GEN_CATS = ['music', 'ambient', 'interact', 'earcons', 'other'];
31
- /** 0..1 -> 0..100 rounded (slider display value). */
32
- export function volPercent(v) {
33
- return Math.round(v * 100);
34
- }
35
- /** One category row's markup (volume slider + on/off switch). Pure — resolves label/state from the given data,
36
- * never from a global. Returns '' for an unknown/missing category (defensive; never hit with real catalogs). */
37
- export function catRowHTML(k, cats, state) {
38
- const c = cats.find((x) => x.k === k);
39
- const a = state[k];
40
- if (!c || !a)
41
- return '';
42
- return `<div class="ctrl-row"><span>${c.lbl}</span><span style="display:flex;gap:.5rem;align-items:center;flex-shrink:0">` +
43
- `<input class="vol" type="range" min="0" max="100" step="5" value="${volPercent(a.vol)}" data-avol="${k}" aria-label="Volume de ${c.lbl}">` +
44
- `<button class="mode-btn switch${a.on ? ' is-on' : ''}" data-acat="${k}" type="button" aria-pressed="${a.on}" aria-label="${c.lbl}"></button></span></div>`;
45
- }
46
- /** Full innerHTML for a category list (#audio-list or #navsound-list). Pure string building — no DOM. */
47
- export function catsListHTML(keys, cats, state) {
48
- return keys.map((k) => catRowHTML(k, cats, state)).join('');
49
- }
50
- /** #navsound-master's value: the loudest of the nav categories, as a 0..100 slider value. */
51
- export function navMasterVolume(state, navCats = NAV_CATS) {
52
- return volPercent(Math.max(...navCats.map((k) => state[k].vol)));
53
- }
54
- /** Cane-hit spacing select -> validated int (garbage/empty -> 1, "a batida por bloco"). */
55
- export function parseCaneDiv(raw) {
56
- return (+raw) || 1;
57
- }
58
- /** srSay text for a cane-hit spacing choice. The two halves are ONE sentence per case, not a shared prefix
59
- * plus a tail: a language that renders this as "One tap per block (cane)" needs to move the word "cane". */
60
- export function caneDivMessage(div) {
61
- return t(div === 2 ? 'sr.audio.caneHalfBlock' : 'sr.audio.canePerBlock');
16
+ import { controlRow, labelRow, mountChoice, updateChoice } from './panel-widgets.js';
17
+ /*
18
+ * 🔴 THE VOICE SECTION LIVES IN `ui/voice-settings` (ADR-0221, issue #203): speech — the narration switch, the engine, the
19
+ * voice, the rate, the spoken index and the test button — and the sound categories, cane, blind mode and outputs never
20
+ * needed each other.
21
+ */
22
+ import { createVoiceSettings } from './voice-settings.js';
23
+ /*
24
+ * 🔴 THE PURE HALF LIVES IN `ui/audio-choices` (ADR-0221, issue #203): what a choice IS — the category list, the volume
25
+ * arithmetic, the engine catalogue, the voice filter, an output's label — needs no document, and this file is about
26
+ * FINDING the controls and wiring them. The suite had already made the cut: the node test imported exactly those names
27
+ * and the browser test drove the rest.
28
+ */
29
+ import { NAV_CATS, GEN_CATS, volPercent, navMasterVolume, parseCaneDiv, caneDivMessage, sinksSupported, sinkOptionLabel, sinkSelectValue, } from './audio-choices.js';
30
+ /** The cane's two positions — one tap per block, one every half block — in the page's language. */
31
+ function canePositions(t) {
32
+ return [{ value: '1', label: t('audio.cane.block') }, { value: '2', label: t('audio.cane.halfBlock') }];
62
33
  }
63
- /** #tts-engine's option catalog: (value, i18n KEY of the label). The engine NAMES are proper nouns and stay
64
- * put; what translates is the parenthetical that explains each one. Keys, not text — see input/devices. */
65
- export const TTS_ENGINE_OPTIONS = [
66
- ['webspeech', 'tts.engine.webspeech'],
67
- ['piper', 'tts.engine.piper'],
68
- ['kokoro', 'tts.engine.kokoro'],
69
- ['kitten', 'tts.engine.kitten'],
70
- ['espeak', 'tts.engine.espeak'],
71
- ];
72
34
  /**
73
- * Os motores que ESTA MONTAGEM pode de facto oferecer (ADR-0094).
35
+ * THE CANE ROW, a CYCLE ROW «◀ Batida da bengala: uma batida por bloco ▶» (ADR-0130 rule 3: two positions).
74
36
  *
75
- * ⚠️ Desde que o motor neural passou a chegar por PORTA (`ctx.carregarVozNeural`), «o Piper existe» deixou
76
- * de ser verdade sobre a engine e passou a ser verdade sobre o JOGO. Oferecer uma opção que não pode
77
- * funcionar é pior que uma opção a menos: quem a escolhe fica à espera de um download que nunca começa, e
78
- * quem navega por escuta não tem como ver que não começou.
37
+ * 🔴 It was the kit's `escolha`, a `<select>` nobody filled: offered to every game with a walker, it opened on nothing to
38
+ * choose. Two positions are a cycle, and the positions are the engine's words.
79
39
  *
80
- * Só o `piper` é filtrado. `kokoro`/`kitten`/`espeak` também não funcionam hoje — «ainda não entraram» —,
81
- * mas isso é anterior a este registro e escondê-los aqui mudaria comportamento que ninguém pediu para mudar.
40
+ * 📌 THE `<strong>` STAYS, HIDDEN: it is where the game's word for the cane lands (`ui/audio-rows-that-apply`, ADR-0153),
41
+ * and `renderAudio` reads it into the cycle's own text. `hidden` keeps it out of the accessibility tree, so the name is
42
+ * heard once — from the control.
82
43
  */
83
- export function opcoesDeMotor(neuralDisponivel) {
84
- return neuralDisponivel ? TTS_ENGINE_OPTIONS : TTS_ENGINE_OPTIONS.filter(([v]) => v !== 'piper');
44
+ function caneRow(t, ctx, spec) {
45
+ const row = ctx.create('div');
46
+ row.className = 'ctrl-row ctrl-row--passos';
47
+ const text = ctx.create('span');
48
+ const name = ctx.create('strong');
49
+ name.textContent = spec.label;
50
+ name.hidden = true;
51
+ text.appendChild(name);
52
+ if (spec.hint) {
53
+ const explanation = ctx.create('span');
54
+ explanation.className = 'opt-hint';
55
+ explanation.textContent = spec.hint;
56
+ text.appendChild(explanation);
57
+ }
58
+ row.appendChild(text);
59
+ // the pick is `initSettingsAudio`'s, which listens for the `change` every choice fires: this builder has no setter
60
+ const control = mountChoice(ctx, { label: spec.label, values: canePositions(t), current: '1' }, () => { });
61
+ control.id = spec.id;
62
+ row.appendChild(control);
63
+ return row;
85
64
  }
86
65
  /**
87
- * As vozes do sistema no idioma pedido; sem nenhuma, a lista inteira.
66
+ * MOUNTS THIS PANEL'S INSIDE — the controls and containers it reaches.
67
+ *
68
+ * 🔴 IT IS THE LARGEST INVISIBLE CONTRACT OF THE PANELS: this file looks for `#opt-modocego`, `#cane-div`,
69
+ * `#opt-menuindex`, `#opt-tts`, `#tts-vol` and more, and nothing in the type says so — which is why it builds them.
70
+ *
71
+ * ⚠️ AND EACH ONE'S TAG MATTERS, which makes this the worst place to guess: the voice selects have to be `<select>` — the
72
+ * panel writes `.value` into them —, the volumes `<input type=range>`, and `#cane-div` is the cycle row `caneRow` builds
73
+ * (a host's own `<select>` is still read). On a `<button>`,
74
+ * writing `.value` gives no error at all: it creates a property nobody reads, and the child's choice vanishes silently.
88
75
  *
89
- * Chamava-se `pickPtVoices` e filtrava `/^pt/i` fixo, de modo que o jogo em inglês oferecia à pessoa uma
90
- * lista de vozes PORTUGUESAS para ler texto em inglês. O nome dizia a verdade sobre o que fazia e mentia
91
- * sobre o que devia fazer.
76
+ * 📌 THE ORDER IS THE DECISION (ADR-0044 §2), from general to particular (see the composition below).
92
77
  *
93
- * A comparação é pelo PREFIXO de idioma, não pela etiqueta inteira: quem joga em pt-BR também deve poder
94
- * escolher uma voz pt-PT se for a única instalada, e o navegador de uma escola raramente tem a variante
95
- * exata. O recuo para a lista inteira fica: uma lista vazia seria pior que uma lista no idioma errado, que
96
- * ao menos a pessoa pode ouvir e rejeitar.
78
+ * ⚠️ `#opt-sound` IS NOT CREATED HERE, deliberately: it is this setting's mirror on the quick bar, outside the panel.
79
+ * `reflectMaster` reaches it with a guard (`if (sb)`), because a game may have no bar.
80
+ *
81
+ * Idempotent: calling it twice reuses what exists instead of duplicating it.
97
82
  */
98
- export function pickVoicesFor(voices, lang) {
99
- const pref = lang.slice(0, 2).toLowerCase();
100
- const iguais = voices.filter((v) => v.lang.slice(0, 2).toLowerCase() === pref);
101
- return iguais.length ? iguais : voices;
102
- }
103
- /** "<name> (<lang>)" option label. */
104
- export function voiceLabel(v) {
105
- return v.name + ' (' + v.lang + ')';
106
- }
107
- /** Whether the browser can list/switch audio outputs at all (gates the #audio-sinks hint copy). */
108
- export function sinksSupported(hasEnumerateDevices, hasAudioContextCtor) {
109
- return hasEnumerateDevices && hasAudioContextCtor;
110
- }
111
- /** A device's option label, falling back to a 1-based "Saída N" when the browser withholds the real label
112
- * (no getUserMedia permission granted yet). */
113
- export function sinkOptionLabel(d, index) {
114
- return d.label || t('audio.sinkFallback', { n: index + 1 });
83
+ export function mountAudioInside(t, ctx, card, list) {
84
+ // Each entry is either a control row or a CONTAINER the panel fills with rows of its own.
85
+ const pieces = [
86
+ /*
87
+ * 🔴 THE COMPOSITION OF ADR-0151 AND ITS ERRATA, from general to particular:
88
+ * · BLIND MODE first, because it is the mode where the other sounds become the screen — and WITHOUT the hint: «é
89
+ * redundante. Quem precisa sabe o que é»;
90
+ * · the CANE beside it (and only in a game that answers that someone walks, ADR-0153);
91
+ * · the SONAR, GUARD and GUIDE, each with its switch and volume: they are the shell's list;
92
+ * · NARRATION and its volume, with the SPOKEN INDEX right after, because narration is what it shortens.
93
+ * 🔴 The GENERAL sound and volume live in the audio panel (`mountSoundInside`) — «toggle + barra para som geral
94
+ * voltam», the Dev said, and they came back to the sound panel, not the accessibility one. There is no separate
95
+ * navigation volume: it would be a second place for the list's three volumes (one place per choice, D2 of the record).
96
+ */
97
+ // ⚠️ Their own short keys, not the bar's `icon.blind`/`icon.tts`: those carry «(navegação sonora)» and «(TTS)», and
98
+ // a row keeps no explanation in parentheses (ADR-0158).
99
+ { id: 'opt-modocego', label: t('audio.modocego') },
100
+ // two positions, so a cycle row (ADR-0130 rule 3): built by `caneRow`, not by the kit
101
+ { id: 'cane-div', label: t('audio.cane'), hint: t('audio.cane.dica') },
102
+ { container: '@lista' }, // the shell's list: sonar, guard and guide
103
+ { id: 'opt-tts', label: t('audio.narracao'), hint: t('audio.tts.dica') },
104
+ { id: 'tts-vol', label: t('audio.ttsVol'), shape: 'cursor' },
105
+ // ADR-0183 §1, ADR-0196: the speech rate, 254 to 504 by 50 — six positions, so a list (ADR-0130 erratum)
106
+ { id: 'tts-ppm', label: t('audio.ttsPpm'), hint: t('audio.ttsPpm.dica'), shape: 'escolha' },
107
+ // ADR-0185: the child picks the voice, among the voices that speak the language — a list, since English passes five
108
+ { id: 'tts-voz', label: t('audio.voz'), shape: 'escolha' },
109
+ { id: 'opt-menuindex', label: t('audio.menuindex'), hint: t('audio.menuindex.dica') },
110
+ /*
111
+ * 🔴 NOT OFFERED by the engine (ADR-0151), in the Dev's words:
112
+ * · the ENGINE, the VOICE and the voice TEST — «quem escolhe a voz é o jogo (cartucho), não o jogador. Jogador só
113
+ * habilita/desabilita o TTS»;
114
+ * · the PER-PLAYER AUDIO OUTPUTS and their detect button — «navegadores não são bons nisso».
115
+ * 📌 `initSettingsAudio`'s wiring for those ids STAYS and is guarded (`if (el)`): a game with markup of its own does
116
+ * not break. What changed is that the engine no longer OFFERS the choice to the child.
117
+ */
118
+ ];
119
+ /*
120
+ * ⚠️ IT RELABELS INSTEAD OF SKIPPING what already exists, which is why this function is also called from every open's
121
+ * `render()`: anything built once at boot captures the fallback language's text — `initI18n` applies the fallback
122
+ * synchronously and REQUESTS the preferred one, which arrives later — and a panel would serve its TITLE in one
123
+ * language and its ROWS in another. No unit test catches it: they all run in one language.
124
+ */
125
+ // ADR-0158: the rows go BEFORE the reset, never after it — the reset is the panel's last item, and «Voltar» its first.
126
+ const actions = card.querySelector(':scope > .overlay__actions');
127
+ for (const piece of pieces) {
128
+ if ('container' in piece) {
129
+ if (piece.container === '@lista') {
130
+ card.insertBefore(list, actions);
131
+ continue;
132
+ }
133
+ const jaHa = ctx.find('#' + piece.container);
134
+ if (jaHa) {
135
+ if (piece.label)
136
+ jaHa.setAttribute('aria-label', piece.label);
137
+ continue;
138
+ }
139
+ const c = ctx.create('div');
140
+ c.id = piece.container;
141
+ c.className = 'ctrl-list';
142
+ c.setAttribute('role', 'group');
143
+ if (piece.label)
144
+ c.setAttribute('aria-label', piece.label);
145
+ card.insertBefore(c, actions);
146
+ continue;
147
+ }
148
+ const alreadyThere = ctx.find('#' + piece.id);
149
+ if (alreadyThere) {
150
+ const rowNode = alreadyThere.closest('.ctrl-row');
151
+ if (rowNode)
152
+ labelRow(rowNode, piece);
153
+ continue;
154
+ }
155
+ card.insertBefore(piece.id === 'cane-div' ? caneRow(t, ctx, piece) : controlRow(ctx, piece).row, actions);
156
+ }
115
157
  }
116
- /** A player's current sink select value ('' = default/shared). */
117
- export function sinkSelectValue(p) {
118
- return (p && p.audioSink) || '';
158
+ /**
159
+ * MOUNTS THE AUDIO PANEL'S INSIDE (ADR-0151 §2 item 4): the general sound — switch and volume — and then the shell's
160
+ * list with the four taste categories (music, ambience, interaction, earcons).
161
+ *
162
+ * ⚠️ THE IDS ARE THE ONES `initSettingsAudio` ALREADY LISTENS TO (`#audio-master`, `#audio-master-vol`, `#audio-list`). And
163
+ * the same order rule: mount BEFORE `init`. Idempotent and relabellable, like its hearing sibling.
164
+ */
165
+ export function mountSoundInside(t, ctx, card, list) {
166
+ const actions = card.querySelector(':scope > .overlay__actions');
167
+ const rows = [
168
+ { id: 'audio-master', label: t('audio.som'), hint: t('audio.som.dica') },
169
+ { id: 'audio-master-vol', label: t('audio.volume'), shape: 'cursor' },
170
+ ];
171
+ for (const piece of rows) {
172
+ const alreadyThere = ctx.find('#' + piece.id);
173
+ if (alreadyThere) {
174
+ const rowNode = alreadyThere.closest('.ctrl-row');
175
+ if (rowNode)
176
+ labelRow(rowNode, piece);
177
+ continue;
178
+ }
179
+ card.insertBefore(controlRow(ctx, piece).row, list.parentNode === card ? list : actions);
180
+ }
181
+ if (list.parentNode !== card)
182
+ card.insertBefore(list, actions);
119
183
  }
120
184
  // ---------------------------------------------------------------------------------------------
121
185
  // DOM-facing (thin) — requires `document`/injected ctx
122
186
  // ---------------------------------------------------------------------------------------------
123
187
  export function initSettingsAudio(ctx) {
124
- // ⚠️ PADRÃO DA ENGINE (ADR-0106 §4): quem injecta manda; quem não injecta deixa de ficar sem modo cego.
125
- // O `setModoCegoValue` faz as três coisas que o `core/state` diz que um setter faz — grava, persiste, avisa
126
- // — e nada mais: os efeitos (refazer os extras do nível) são reação, e quem reage assina o evento.
127
- const setModoCego = ctx.setModoCego ?? state.setModoCegoValue;
188
+ const { t } = ctx.translator;
189
+ // ⚠️ THE ENGINE'S DEFAULT (ADR-0106 §4): whoever injects rules; whoever does not still gets blind mode.
190
+ // `setBlindModeValue` does the three things `core/state` says a setter does — store, persist, notify — and nothing
191
+ // more: the effects are reactions, and whoever reacts subscribes to the event.
192
+ const writeBlindMode = ctx.setBlindMode ?? ((on) => { ctx.settings.setBlindModeValue(on); });
128
193
  let audioDevices = [];
194
+ /*
195
+ * ⚠️ THE BROWSER TRAVELS AS FOUR FUNCTIONS, not as a global reached inside: a module that reaches `document` or
196
+ * `window` is refused by the ratchet (the reach ceiling is ZERO). What this file received through its own ports it
197
+ * passes down as ports.
198
+ */
199
+ const voice = createVoiceSettings(ctx, {
200
+ newOption: () => ctx.newElement('option'),
201
+ systemVoices: () => [...ctx.speech.voices()],
202
+ speakSample: (sample, chosen) => ctx.speech.speakSample(sample, chosen),
203
+ whenVoicesChange: (again) => ctx.speech.whenVoicesChange(again),
204
+ });
129
205
  function reflectMaster() {
130
206
  const b = ctx.$('#audio-master');
131
207
  if (b) {
132
208
  b.classList.toggle('is-on', ctx.getSoundOn());
133
209
  b.setAttribute('aria-pressed', String(ctx.getSoundOn()));
134
- b.textContent = ctx.getSoundOn() ? '🔊 Ligado' : '🔇 Desligado';
210
+ b.textContent = toggleLabel(t, ctx.getSoundOn()); // the dictionary's words, no glyph in the name (ADR-0159 rule 12)
135
211
  }
136
212
  const v = ctx.$('#audio-master-vol');
137
213
  if (v)
@@ -140,40 +216,144 @@ export function initSettingsAudio(ctx) {
140
216
  if (sb)
141
217
  ctx.toggleBtn(sb, ctx.getSoundOn());
142
218
  }
219
+ /**
220
+ * The kit's ctx comes from the LIST NODE itself, not from a global `document` nor from a new contract field —
221
+ * `ownerDocument` is the document that list lives in, which is where its rows have to be born. Same shape as
222
+ * `ui/settings-aac`, and it is what removes this file's reach to `document` without touching `SettingsAudioCtx`.
223
+ */
224
+ const kitCtx = (list) => ({
225
+ find: (sel) => ctx.$(sel),
226
+ create: (tag) => list.ownerDocument.createElement(tag),
227
+ });
228
+ /**
229
+ * The same document, but KEEPING the element type.
230
+ *
231
+ * 📌 `PanelShellCtx.create` is `string → HTMLElement` on purpose: the shell only ever appends generic nodes, and
232
+ * widening it would be a one-way door on a published shape (ADR-0172). The sinks section needs a `<select>` that
233
+ * answers `.value`, so the typed factory lives HERE, over the same `ownerDocument`, instead of the contract
234
+ * growing a field for one caller.
235
+ */
236
+ const makerFor = (host) => (tag) => host.ownerDocument.createElement(tag);
237
+ /**
238
+ * ONE CATEGORY ROW, in nodes: label, volume slider and toggle.
239
+ *
240
+ * 📌 NOT A KIT ROW, and not by taste: 📏 measured on 2026-09-23, `controlRow` builds «a label, a hint and ONE
241
+ * control» and this row carries TWO — the volume and the on/off of the same category. Same reason the visual
242
+ * panel's four paper-colour row stayed hand-built: inventing a shape for it would be giving a second answer to
243
+ * the question «what is a row».
244
+ */
245
+ function buildCatRow(kit, k) {
246
+ const row = kit.create('div');
247
+ row.className = 'ctrl-row';
248
+ const text = kit.create('span');
249
+ text.appendChild(kit.create('strong'));
250
+ row.appendChild(text);
251
+ const box = kit.create('span');
252
+ box.style.cssText = 'display:flex;gap:.5rem;align-items:center;flex-shrink:0';
253
+ const vol = kit.create('input');
254
+ vol.className = 'vol';
255
+ vol.setAttribute('type', 'range');
256
+ vol.setAttribute('min', '0');
257
+ vol.setAttribute('max', '100');
258
+ vol.setAttribute('step', '5');
259
+ vol.setAttribute('data-avol', k);
260
+ box.appendChild(vol);
261
+ const toggle = kit.create('button');
262
+ toggle.className = 'mode-btn switch';
263
+ toggle.setAttribute('type', 'button');
264
+ toggle.setAttribute('aria-pressed', 'false');
265
+ toggle.setAttribute('data-acat', k);
266
+ box.appendChild(toggle);
267
+ row.appendChild(box);
268
+ return row;
269
+ }
270
+ /** Creates what is missing IN THE RIGHT POSITION, rewrites the words of what stayed, removes what is no longer asked for. */
271
+ function mountCatRows(list, keys) {
272
+ const kit = kitCtx(list);
273
+ let previous = null;
274
+ // ⚠️ WHAT SURVIVES IS WHAT THE HOST STILL NAMES, not simply what is in `keys`. A row whose category lost its
275
+ // name would otherwise stay on screen with the old label — asked for by the key list, offered by nobody.
276
+ const named = new Set();
277
+ for (const k of keys) {
278
+ const c = ctx.audioCats.find((x) => x.k === k);
279
+ if (!c)
280
+ continue; // a category this host does not name: what has no name is not offered
281
+ named.add(k);
282
+ let row = list.querySelector(`[data-acat="${k}"]`)?.closest('.ctrl-row') ?? null;
283
+ if (!row) {
284
+ row = buildCatRow(kit, k);
285
+ // ⚠️ INSERTED in place, never appended and reordered: a node that changes parent is removed and put back,
286
+ // and the browser blurs it on removal — that would take the cursor from whoever is setting the volume.
287
+ list.insertBefore(row, previous ? previous.nextSibling : list.firstChild);
288
+ }
289
+ const label = t(c.lbl);
290
+ const strong = row.querySelector('strong');
291
+ if (strong)
292
+ strong.textContent = label;
293
+ row.querySelector(`[data-avol="${k}"]`)?.setAttribute('aria-label', t('audio.cat.volumeDe', { c: label }));
294
+ row.querySelector(`[data-acat="${k}"]`)?.setAttribute('aria-label', label);
295
+ previous = row;
296
+ }
297
+ for (const b of [...list.querySelectorAll('[data-acat]')]) {
298
+ if (!named.has(b.dataset.acat ?? ''))
299
+ b.closest('.ctrl-row')?.remove();
300
+ }
301
+ }
302
+ /** The STATE of each row — the volume where it is and the toggle saying what it says. */
303
+ function reflectCatRows(list, keys) {
304
+ const state = ctx.getAudioCat();
305
+ if (!state)
306
+ return;
307
+ for (const k of keys) {
308
+ const a = state[k];
309
+ if (!a)
310
+ continue;
311
+ const vol = list.querySelector(`input[data-avol="${k}"]`);
312
+ if (vol)
313
+ vol.value = String(volPercent(a.vol));
314
+ const toggle = list.querySelector(`button[data-acat="${k}"]`);
315
+ if (toggle) {
316
+ toggle.classList.toggle('is-on', a.on);
317
+ toggle.setAttribute('aria-pressed', String(a.on));
318
+ }
319
+ }
320
+ }
321
+ /** The listeners, ONCE per list and by delegation — rows come and go, the list stays. */
143
322
  function wireCatControls(el) {
144
- el.querySelectorAll('button[data-acat]').forEach((b) => {
145
- b.addEventListener('click', () => {
146
- const k = b.dataset.acat;
147
- if (!k)
148
- return;
149
- const state = ctx.getAudioCat();
150
- if (!state || !state[k])
151
- return;
152
- state[k].on = !state[k].on;
153
- ctx.setCatGain(k);
154
- b.classList.toggle('is-on', state[k].on);
155
- b.setAttribute('aria-pressed', String(state[k].on));
156
- refreshMarks(); // a marca acompanha a MUDANÇA, não só o redesenho — ver a nota em refreshMarks
157
- });
323
+ if (el.dataset.catsWired)
324
+ return;
325
+ el.dataset.catsWired = '1';
326
+ el.addEventListener('click', (ev) => {
327
+ const b = ev.target?.closest('button[data-acat]');
328
+ const k = b?.dataset.acat;
329
+ if (!b || !k)
330
+ return;
331
+ const state = ctx.getAudioCat();
332
+ if (!state || !state[k])
333
+ return;
334
+ state[k].on = !state[k].on;
335
+ ctx.setCatGain(k);
336
+ b.classList.toggle('is-on', state[k].on);
337
+ b.setAttribute('aria-pressed', String(state[k].on));
338
+ refreshMarks(); // the mark follows the CHANGE, not only the redraw — see the note in refreshMarks
158
339
  });
159
- el.querySelectorAll('input[data-avol]').forEach((s) => {
160
- s.addEventListener('input', () => {
161
- const k = s.dataset.avol;
162
- if (!k)
163
- return;
164
- const state = ctx.getAudioCat();
165
- if (!state || !state[k])
166
- return;
167
- state[k].vol = (+s.value) / 100;
168
- state[k].on = true;
169
- ctx.setCatGain(k);
170
- const bb = el.querySelector('button[data-acat="' + k + '"]');
171
- if (bb) {
172
- bb.classList.add('is-on');
173
- bb.setAttribute('aria-pressed', 'true');
174
- }
175
- refreshMarks();
176
- });
340
+ el.addEventListener('input', (ev) => {
341
+ const s = ev.target?.closest('input[data-avol]');
342
+ const k = s?.dataset.avol;
343
+ if (!s || !k)
344
+ return;
345
+ const state = ctx.getAudioCat();
346
+ if (!state || !state[k])
347
+ return;
348
+ state[k].vol = (+s.value) / 100;
349
+ state[k].on = true;
350
+ ctx.setCatGain(k);
351
+ const bb = el.querySelector('button[data-acat="' + k + '"]');
352
+ if (bb) {
353
+ bb.classList.add('is-on');
354
+ bb.setAttribute('aria-pressed', 'true');
355
+ }
356
+ refreshMarks();
177
357
  });
178
358
  }
179
359
  function renderCategoryList(sel, keys) {
@@ -181,10 +361,12 @@ export function initSettingsAudio(ctx) {
181
361
  const state = ctx.getAudioCat();
182
362
  if (!el || !state)
183
363
  return;
184
- el.innerHTML = catsListHTML(keys, ctx.audioCats, state);
364
+ mountCatRows(el, keys);
365
+ reflectCatRows(el, keys);
185
366
  wireCatControls(el);
186
- // A prosa volta para o rodapé depois de as linhas serem reconstruídas (CLAUDE.md §4, #109).
187
- ctx.fillExplain?.(ctx.$('#audio .overlay__card'));
367
+ // The prose goes back to the footer after the rows change (CLAUDE.md §4, #109) — in the card of WHOEVER holds
368
+ // this list: since ADR-0151 there are two panels, and the other one's footer is not this child's.
369
+ ctx.fillExplain?.(el.closest('.overlay__card'));
188
370
  }
189
371
  function renderNavSound() {
190
372
  renderCategoryList('#navsound-list', NAV_CATS);
@@ -193,120 +375,47 @@ export function initSettingsAudio(ctx) {
193
375
  if (m && state)
194
376
  m.value = String(navMasterVolume(state, NAV_CATS));
195
377
  }
196
- function reflectTts() {
197
- const b = ctx.$('#opt-tts');
198
- const state = ctx.getAudioCat();
199
- const on = !!(state && state.tts && state.tts.on);
200
- if (b) {
201
- ctx.toggleBtn(b, on);
202
- b.textContent = toggleLabel(on);
203
- }
204
- const e = ctx.$('#tts-engine');
205
- if (e)
206
- e.value = ctx.tts.getEngineSel();
207
- }
208
- /**
209
- * O alternador do ÍNDICE "6 de 10" (ADR-0044, item 3).
210
- *
211
- * Ele mora ao lado da narração por voz e não num painel de visual, porque é a NARRAÇÃO que ele muda: quem
212
- * desliga o índice está encurtando o que ouve a cada passo, e é aqui que essa pessoa vem quando o que ouve
213
- * incomoda.
214
- */
215
- function reflectMenuIndex() {
216
- const b = ctx.$('#opt-menuindex');
217
- if (b) {
218
- ctx.toggleBtn(b, state.menuIndexOn);
219
- b.textContent = toggleLabel(state.menuIndexOn);
220
- }
221
- }
222
- function populateTtsEngines() {
223
- const sel = ctx.$('#tts-engine');
224
- if (!sel || sel.dataset.filled)
225
- return;
226
- sel.dataset.filled = '1';
227
- opcoesDeMotor(ctx.tts.neuralDisponivel !== false).forEach(([v, l]) => {
228
- const o = document.createElement('option');
229
- o.value = v;
230
- o.textContent = t(l);
231
- sel.appendChild(o);
232
- });
233
- sel.value = ctx.tts.getEngineSel();
234
- }
235
- function populateTtsVoices() {
236
- const sel = ctx.$('#tts-voice');
237
- if (!sel)
238
- return;
239
- let voices = [];
240
- try {
241
- voices = (window.speechSynthesis && window.speechSynthesis.getVoices()) || [];
242
- }
243
- catch (e) { /* noop */ }
244
- const list = pickVoicesFor(voices, bcp47());
245
- sel.innerHTML = '';
246
- if (!list.length) {
247
- const o = document.createElement('option');
248
- o.textContent = t('audio.noSystemVoices');
249
- sel.appendChild(o);
250
- ctx.tts.setVoiceObj(null);
251
- return;
252
- }
253
- list.forEach((v) => {
254
- const o = document.createElement('option');
255
- o.value = v.name;
256
- o.textContent = voiceLabel(v);
257
- sel.appendChild(o);
258
- });
259
- const saved = ctx.store.get('incl_tts_voice', null);
260
- const pick = list.find((v) => v.name === saved) || list[0];
261
- sel.value = pick.name;
262
- ctx.tts.setVoiceObj(pick);
263
- }
264
- function hasEnumerateDevices() {
265
- return !!(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices);
266
- }
267
- function hasAudioContextCtor() {
268
- const w = window;
269
- return typeof (w.AudioContext || w.webkitAudioContext) !== 'undefined';
270
- }
378
+ // 📌 Both questions are the HOST's (ADR-0227), and they stay TWO because they produce different sentences: the browser
379
+ // cannot do it, and there is no device at all, are not the same news to someone looking for their headphones.
380
+ const hasEnumerateDevices = () => ctx.audioOutputs.canList();
381
+ const hasAudioContextCtor = () => ctx.audioOutputs.canRoute();
271
382
  function renderSinks(devices) {
272
383
  const el = ctx.$('#audio-sinks');
273
384
  if (!el)
274
385
  return;
275
- el.innerHTML = '';
386
+ // 📌 The document comes from the list node, never from the global one — same shape as the rest of this file
387
+ // since the conversion to nodes, and it is what removes the reach to `document` without asking the contract
388
+ // for a new field (ADR-0221 step 7d).
389
+ const make = makerFor(el);
390
+ while (el.firstChild)
391
+ el.removeChild(el.firstChild);
276
392
  if (!devices.length) {
277
393
  const supported = sinksSupported(hasEnumerateDevices(), hasAudioContextCtor());
278
- el.innerHTML = '<p class="opt-hint">' +
279
- t(supported ? 'audio.sinksHint' : 'audio.sinksUnsupported') +
280
- '</p>';
394
+ // ⚠️ TWO SENTENCES, not one: «this browser CANNOT do it» and «there is no device yet» are different things to
395
+ // a child who is looking for their own headphones, and the dictionary has always had both.
396
+ const hint = make('p');
397
+ hint.className = 'opt-hint';
398
+ hint.textContent = t(supported ? 'audio.sinksHint' : 'audio.sinksUnsupported');
399
+ el.appendChild(hint);
281
400
  return;
282
401
  }
283
402
  const players = ctx.getPlayers();
284
403
  const n = Math.max(1, ctx.getNumPlayers());
404
+ /*
405
+ * 🔴 A PLAYER'S OUTPUT IS A CHOICE, AND ITS SIZE PICKS THE CONTROL (ADR-0130 rule 3): «shared» and up to four devices are
406
+ * a cycle row «◀ Jogador 1: Fone USB ▶», more a dropdown. The size is the room's today — it is drawn again at every
407
+ * detection, so a fifth headset plugged in turns the rows into dropdowns.
408
+ */
409
+ const outputs = [{ value: '', label: t('audio.sinkShared') }, ...devices.map((d, k) => ({ value: d.deviceId, label: sinkOptionLabel(t, d, k) }))];
285
410
  for (let i = 0; i < n; i++) {
286
411
  const p = players[i];
287
- const row = document.createElement('div');
412
+ const name = t('audio.playerN', { n: i + 1 });
413
+ const row = make('div');
288
414
  row.className = 'ctrl-row';
289
- const lbl = document.createElement('label');
290
- lbl.textContent = t('audio.playerN', { n: i + 1 });
291
- lbl.setAttribute('for', 'sink-p' + i);
292
- const sel = document.createElement('select');
293
- sel.className = 'vol';
294
- sel.id = 'sink-p' + i;
295
- const o0 = document.createElement('option');
296
- o0.value = '';
297
- o0.textContent = t('audio.sinkShared');
298
- sel.appendChild(o0);
299
- devices.forEach((d, k) => {
300
- const o = document.createElement('option');
301
- o.value = d.deviceId;
302
- o.textContent = sinkOptionLabel(d, k);
303
- sel.appendChild(o);
304
- });
305
- sel.value = sinkSelectValue(p);
306
- sel.addEventListener('change', () => {
415
+ const choice = mountChoice(kitCtx(el), { label: name, values: outputs, current: sinkSelectValue(p) }, (value) => {
307
416
  if (!p)
308
417
  return;
309
- p.audioSink = sel.value || null;
418
+ p.audioSink = value || null;
310
419
  ctx.store.set('incl_sink_p' + i, p.audioSink || '');
311
420
  if (p._ac) {
312
421
  try {
@@ -318,84 +427,96 @@ export function initSettingsAudio(ctx) {
318
427
  }
319
428
  ctx.srSay(t(p.audioSink ? 'sr.audio.sinkChanged' : 'sr.audio.sinkDefault', { n: i + 1 }));
320
429
  });
321
- row.appendChild(lbl);
322
- row.appendChild(sel);
430
+ choice.id = 'sink-p' + i;
431
+ if (choice.tagName === 'SELECT') {
432
+ // a dropdown keeps its name beside it; a cycle row already carries it in its own text
433
+ const lbl = make('label');
434
+ lbl.textContent = name;
435
+ lbl.htmlFor = choice.id;
436
+ row.appendChild(lbl);
437
+ }
438
+ else
439
+ row.classList.add('ctrl-row--passos');
440
+ row.appendChild(choice);
323
441
  el.appendChild(row);
324
442
  }
325
443
  }
326
444
  async function enumerateSinks() {
445
+ // without asking permission: only the outputs already known
327
446
  try {
328
- if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
329
- const devs = await navigator.mediaDevices.enumerateDevices();
330
- audioDevices = devs.filter((d) => d.kind === 'audiooutput');
331
- }
447
+ audioDevices = [...await ctx.audioOutputs.list()];
332
448
  }
333
- catch (e) { /* sem pedir permissão: só lista as saídas já conhecidas */ }
449
+ catch (e) { /* the port refused; the list stays as it is */ }
334
450
  renderSinks(audioDevices);
335
451
  }
336
452
  async function detectAudioDevices() {
453
+ // ⚠️ THE LIST EMPTIES when detection fails, and `enumerateSinks` above does NOT empty it: there an error means "I
454
+ // could not ask", here it means "the child asked and the answer is none". The two sentences the panel draws next are
455
+ // different, and so are the two `catch`es.
337
456
  try {
338
- await navigator.mediaDevices.getUserMedia({ audio: true }).then((s) => s.getTracks().forEach((t) => t.stop())).catch(() => { });
339
- const devs = await navigator.mediaDevices.enumerateDevices();
340
- audioDevices = devs.filter((d) => d.kind === 'audiooutput');
457
+ audioDevices = [...await ctx.audioOutputs.detect()];
341
458
  }
342
459
  catch (e) {
343
460
  audioDevices = [];
344
461
  }
345
462
  renderSinks(audioDevices);
346
463
  }
347
- function reflectModoCego() {
464
+ function drawBlindMode() {
348
465
  const b = ctx.$('#opt-modocego');
349
466
  if (b) {
350
- ctx.toggleBtn(b, ctx.getModoCego());
351
- b.textContent = toggleLabel(ctx.getModoCego());
467
+ ctx.toggleBtn(b, ctx.getBlindMode());
468
+ b.textContent = toggleLabel(t, ctx.getBlindMode());
352
469
  }
353
470
  }
354
471
  function renderAudio() {
355
472
  reflectMaster();
356
473
  renderCategoryList('#audio-list', GEN_CATS);
357
474
  renderNavSound();
358
- reflectModoCego();
359
- reflectTts();
360
- populateTtsEngines();
361
- populateTtsVoices();
362
- const cd = ctx.$('#cane-div');
363
- if (cd)
364
- cd.value = String(ctx.getCaneBlockDiv());
365
- void enumerateSinks(); // sem await no original: dispara e segue (lista assíncrona atualiza sozinha)
475
+ drawBlindMode();
476
+ voice.render();
477
+ reflectCane();
478
+ void enumerateSinks(); // not awaited: fire and carry on (the asynchronous list updates by itself)
366
479
  refreshMarks();
367
480
  }
368
481
  /**
369
- * A marca de "saiu do padrão" (ADR-0029).
482
+ * The left-the-default mark (ADR-0029).
370
483
  *
371
- * Chamada de dentro dos HANDLERS de mudança, e não só do `renderAudio()`. Pendurei-a primeiro no render, e
372
- * no jogo a marca não aparecia: mexer numa categoria atualiza aquela linha sozinha, sem redesenhar o painel.
373
- * O ADR-0029 já avisava disso — "pode envelhecer na tela se um painel esquecer de atualizar depois de uma
374
- * mudança" — e eu escrevi o aviso e caí nele na mesma tarde. A regra que sobra: a marca anda com quem
375
- * ESCREVE o valor, nunca com quem desenha.
484
+ * Called from inside the change HANDLERS, not only from `renderAudio()`: changing a category updates that row by
485
+ * itself, without redrawing the panel, so a mark hung only on render goes stale on screen — ADR-0029 warns about it.
486
+ * The rule: the mark travels with whoever WRITES the value, never with whoever draws.
376
487
  *
377
- * O recorte é o MESMO do reset deste menu, e isso não é economia —
378
- * é a regra: só pode ser marcado o que tem padrão em DEFAULTS/`defaultAudioCat`. O motor de voz e a saída
379
- * de áudio por jogador não têm, porque são escolha de DISPOSITIVO e não preferência restaurável; marcá-los
380
- * exigiria inventar uma segunda opinião sobre o que é "padrão" para um fone.
488
+ * The cut is the SAME as this menu's reset, and that is the rule, not thrift: only what has a default in
489
+ * DEFAULTS/`defaultAudioCat` can be marked. The voice engine and the per-player audio output have none, because they
490
+ * are a DEVICE choice and not a restorable preference; marking them would mean inventing a second opinion on what a
491
+ * headset's "default" is.
381
492
  */
382
493
  function refreshMarks() {
383
494
  const state = ctx.getAudioCat();
384
- const mudou = [];
385
- const marcar = (sel, changed) => {
386
- mudou.push(changed);
387
- markChanged(ctx.$(sel)?.closest('.ctrl-row') ?? null, changed);
495
+ const didChange = [];
496
+ const mark = (sel, changed) => {
497
+ didChange.push(changed);
498
+ markChanged(t, ctx.$(sel)?.closest('.ctrl-row') ?? null, changed);
388
499
  };
389
- marcar('#opt-modocego', ctx.getModoCego() !== DEFAULTS.modoCego);
390
- marcar('#cane-div', ctx.getCaneBlockDiv() !== DEFAULTS.caneBlockDiv);
500
+ mark('#opt-modocego', ctx.getBlindMode() !== DEFAULTS.blindMode);
501
+ mark('#cane-div', ctx.getCaneBlockDiv() !== DEFAULTS.caneBlockDiv);
502
+ // ⚠️ TWO MENU MARKS, one per panel: the audio panel's lit by a changed sonar would send the child searching the
503
+ // wrong panel.
504
+ const fromSound = [];
391
505
  for (const c of ctx.audioCats) {
392
506
  const d = defaultAudioCat(c.k);
393
507
  const a = state?.[c.k];
394
- marcar(`[data-acat="${c.k}"]`, !!a && (a.on !== d.on || a.vol !== d.vol));
508
+ const changedHere = !!a && (a.on !== d.on || a.vol !== d.vol);
509
+ if (GEN_CATS.includes(c.k)) {
510
+ fromSound.push(changedHere);
511
+ markChanged(t, ctx.$(`[data-acat="${c.k}"]`)?.closest('.ctrl-row') ?? null, changedHere);
512
+ }
513
+ else
514
+ mark(`[data-acat="${c.k}"]`, changedHere);
395
515
  }
396
- markMenuChanged(ctx.$('[data-act="audio"]'), mudou);
516
+ markMenuChanged(t, ctx.$('[data-act="audio"]'), didChange);
517
+ markMenuChanged(t, ctx.$('[data-act="som"]'), fromSound);
397
518
  }
398
- // ----- widgets estáticos (existem sempre no #audio; fiados UMA vez, nunca recriados por renderAudio) -----
519
+ // ----- static widgets (always present in #audio; wired ONCE, never recreated by renderAudio) -----
399
520
  const audioMasterBtn = ctx.$('#audio-master');
400
521
  if (audioMasterBtn)
401
522
  audioMasterBtn.addEventListener('click', () => {
@@ -424,175 +545,122 @@ export function initSettingsAudio(ctx) {
424
545
  renderNavSound();
425
546
  });
426
547
  /*
427
- * ⚠️ ESTE BOTÃO ERA O ÚNICO DESTE PAINEL QUE NÃO ANUNCIAVA. Medido em 2026-09-08: os cinco irmãos daqui
428
- * anunciam (som, TTS, divisor da bengala, índice de menu, saída de áudio) e o modo cego não — ele parecia
429
- * anunciar porque UM cartucho o fazia a partir do próprio `setModoCego`, e o painel herdava o efeito.
548
+ * ⚠️ THIS BUTTON ANNOUNCES, like its siblings (sound, TTS, cane spacing, menu index, audio output): with the engine's
549
+ * default setter (`setBlindModeValue`, which stores/persists/notifies and does NOT speak), a game that does not inject
550
+ * its own setter would otherwise get a mute button. A toggle that changes state without saying so is invisible to a
551
+ * screen-reader user.
430
552
  *
431
- * ⚠️ E ISSO PASSOU A EXPOR SILÊNCIO no mesmo dia: desde que o campo ganhou padrão da engine
432
- * (`setModoCegoValue`, que grava/persiste/avisa e NÃO fala), um jogo que não injecta o seu próprio setter
433
- * ficava com este botão mudo. Um alternador que muda estado sem o dizer é invisível para quem usa leitor de
434
- * tela — a mesma família de defeito que o `reflectTTS` e o `reflectModoCego` já custaram aqui.
435
- *
436
- * O anúncio pertence a QUEM É ACCIONADO, não ao setter: `core/state` diz que o setter faz três coisas e só
437
- * três. ⚠️ Consequência de lockstep, escrita para não se descobrir depois: quando o `game-platformer` subir
438
- * de versão, tem de TIRAR o `srSay` do `setModoCego` dele, senão a criança ouve o estado duas vezes.
553
+ * The announcement belongs to WHOEVER IS ACTIVATED, not to the setter: `core/state` says a setter does three things
554
+ * and only three. ⚠️ Lockstep consequence, written so it is not discovered later: a cartridge whose own `setBlindMode`
555
+ * also speaks must drop that, or the child hears the state twice.
439
556
  */
440
557
  const mcBtn = ctx.$('#opt-modocego');
441
558
  if (mcBtn) {
442
559
  mcBtn.addEventListener('click', () => {
443
- setModoCego(!ctx.getModoCego());
444
- reflectModoCego();
445
- ctx.srSay(t(ctx.getModoCego() ? 'sr.blind.on' : 'sr.blind.off'));
560
+ writeBlindMode(!ctx.getBlindMode());
561
+ drawBlindMode();
562
+ ctx.srSay(t(ctx.getBlindMode() ? 'sr.blind.on' : 'sr.blind.off'));
446
563
  });
447
564
  }
448
- const caneDivSel = ctx.$('#cane-div');
449
- if (caneDivSel) {
450
- caneDivSel.value = String(ctx.getCaneBlockDiv());
451
- caneDivSel.addEventListener('change', () => {
452
- const div = parseCaneDiv(caneDivSel.value);
565
+ /**
566
+ * The cane's position in use. A host's own `<select>` gets its value; the engine's cycle row (`caneRow`) its words and
567
+ * value, with its name read from the row's hidden `<strong>` — where the game's word for the cane lands (ADR-0153).
568
+ */
569
+ function reflectCane() {
570
+ const cd = ctx.$('#cane-div');
571
+ if (!cd)
572
+ return;
573
+ const current = String(ctx.getCaneBlockDiv());
574
+ if (cd.tagName === 'SELECT') {
575
+ cd.value = current;
576
+ return;
577
+ }
578
+ const label = cd.closest('.ctrl-row')?.querySelector('strong')?.textContent || t('audio.cane');
579
+ updateChoice(cd, { label, values: canePositions(t), current });
580
+ }
581
+ const caneDiv = ctx.$('#cane-div');
582
+ if (caneDiv) {
583
+ reflectCane();
584
+ caneDiv.addEventListener('change', () => {
585
+ // a host's `<select>` holds its value; the engine's cycle row carries it in `data-value` (`mountChoice`)
586
+ const div = parseCaneDiv(caneDiv.tagName === 'SELECT' ? caneDiv.value : (caneDiv.dataset.value ?? ''));
453
587
  ctx.setCaneBlockDiv(div);
454
- ctx.srSay(caneDivMessage(div));
588
+ ctx.srSay(caneDivMessage(t, div));
455
589
  });
456
590
  }
457
- const ttsBtn = ctx.$('#opt-tts');
458
- if (ttsBtn)
459
- ttsBtn.addEventListener('click', () => {
460
- const state = ctx.getAudioCat();
461
- if (!state)
462
- return;
463
- state.tts.on = !state.tts.on;
464
- ctx.setCatGain('tts');
465
- reflectTts();
466
- ctx.srSay(t(state.tts.on ? 'sr.audio.ttsOn' : 'sr.audio.ttsOff'));
467
- if (state.tts.on)
468
- ctx.tts.narrate(t('sr.audio.ttsOnSpoken'));
469
- });
470
- const idxBtn = ctx.$('#opt-menuindex');
471
- if (idxBtn)
472
- idxBtn.addEventListener('click', () => {
473
- state.setMenuIndexOnValue(!state.menuIndexOn);
474
- reflectMenuIndex();
475
- // O anúncio da troca NÃO leva índice: ele não é item de lista nenhuma, e um "1 de 1" aqui seria ruído
476
- // justamente no momento em que a criança está julgando se o ruído incomoda.
477
- ctx.srSay(t(state.menuIndexOn ? 'sr.menu.indexOn' : 'sr.menu.indexOff'));
478
- });
479
- const ttsEngSel = ctx.$('#tts-engine');
480
- if (ttsEngSel)
481
- ttsEngSel.addEventListener('change', () => {
482
- ctx.tts.setEngineSel(ttsEngSel.value);
483
- ctx.store.set('incl_tts_engine', ttsEngSel.value);
484
- if (ttsEngSel.value !== 'webspeech')
485
- ctx.tts.loadTTS();
486
- const opt = ttsEngSel.options[ttsEngSel.selectedIndex];
487
- ctx.tts.narrate(t('sr.audio.engineSet', { motor: opt ? opt.text : '' }));
488
- });
489
- const ttsVoiceSel = ctx.$('#tts-voice');
490
- if (ttsVoiceSel)
491
- ttsVoiceSel.addEventListener('change', () => {
492
- try {
493
- const vs = window.speechSynthesis.getVoices();
494
- ctx.tts.setVoiceObj(vs.find((v) => v.name === ttsVoiceSel.value) || null);
495
- ctx.store.set('incl_tts_voice', ttsVoiceSel.value);
496
- }
497
- catch (e) { /* noop */ }
498
- ctx.tts.narrate(t('sr.audio.voicePicked'));
499
- });
500
- const ttsTestBtn = ctx.$('#opt-tts-test');
501
- if (ttsTestBtn)
502
- ttsTestBtn.addEventListener('click', () => {
503
- const txt = t('audio.voiceSample');
504
- const engine = ctx.tts.getEngine();
505
- if (ctx.tts.getEngineSel() !== 'webspeech' && engine && engine.speak) {
506
- try {
507
- engine.speak(txt);
508
- }
509
- catch (e) { /* noop */ } // motor neural já carregado
510
- }
511
- else {
512
- try {
513
- const ss = window.speechSynthesis;
514
- if (ss) {
515
- ss.cancel();
516
- const u = new SpeechSynthesisUtterance(txt);
517
- u.lang = 'pt-BR';
518
- const vo = ctx.tts.getVoiceObj();
519
- if (vo)
520
- u.voice = vo;
521
- u.rate = 1;
522
- u.volume = 1;
523
- ss.speak(u);
524
- }
525
- }
526
- catch (e) { /* noop */ } // fallback audível (volume 1) + dispara download do neural
527
- if (ctx.tts.getEngineSel() !== 'webspeech')
528
- ctx.tts.loadTTS();
529
- }
530
- ctx.srSay(t('sr.audio.testingVoice'));
531
- });
532
- // ---- restaurar os padrões DESTE menu (ADR-0028) ----
533
591
  //
534
- // A regra dura é o escopo: este botão restaura o que o menu AUDITIVO contém e nada mais. Um reset que
535
- // alcançasse fora de si seria pior que a armadilha que ele existe para desfazer — a criança que desfaz um
536
- // ajuste de som e perde de quebra a configuração motora fica sem conseguir jogar, e sem entender por quê.
592
+ // The hard rule is scope: this button restores what the HEARING menu contains and nothing else. A reset that reached
593
+ // beyond itself would be worse than the trap it exists to undo — a child who undoes a sound setting and loses the
594
+ // mobility setup along with it can no longer play, and does not understand why.
537
595
  //
538
- // O que é deste menu: o modo cego, o espaçamento da bengala e as nove categorias do mixer. O motor de voz
539
- // e a saída de áudio por jogador NÃO entram — são escolha de dispositivo, não preferência restaurável, e
540
- // zerá-las tiraria da criança o fone que é dela numa sala compartilhada.
596
+ // What belongs to this menu: blind mode, the cane spacing and the mixer categories that are not the audio panel's.
597
+ // The voice engine and the per-player audio output do NOT — they are a device choice, not a restorable preference,
598
+ // and clearing them would take away the child's own headset in a shared room.
599
+ /** Resets the categories in `keys` (those that exist in the mixer) to their factory state. */
600
+ function resetCategories(keys) {
601
+ const state = ctx.getAudioCat();
602
+ if (!state)
603
+ return;
604
+ for (const k of keys) {
605
+ if (!state[k])
606
+ continue;
607
+ const d = defaultAudioCat(k);
608
+ state[k].on = d.on;
609
+ state[k].vol = d.vol;
610
+ ctx.setCatGain(k);
611
+ }
612
+ }
541
613
  const resetBtn = ctx.$('#audio-reset');
542
614
  if (resetBtn)
543
615
  resetBtn.addEventListener('click', () => {
544
- setModoCego(DEFAULTS.modoCego);
616
+ writeBlindMode(DEFAULTS.blindMode);
545
617
  ctx.setCaneBlockDiv(DEFAULTS.caneBlockDiv);
546
- const state = ctx.getAudioCat();
547
- if (state)
548
- for (const c of ctx.audioCats) {
549
- const d = defaultAudioCat(c.k);
550
- if (!state[c.k])
551
- continue;
552
- state[c.k].on = d.on;
553
- state[c.k].vol = d.vol;
554
- ctx.setCatGain(c.k);
555
- }
618
+ // 🔴 THIS MENU DOES NOT HOLD THE MUSIC (ADR-0151): resetting it here would reach beyond itself — the scope rule,
619
+ // above. Everything that is not a taste category belongs to this panel.
620
+ resetCategories(ctx.audioCats.map((c) => c.k).filter((k) => !GEN_CATS.includes(k)));
556
621
  renderAudio();
557
- reflectModoCego();
558
- reflectTts();
559
- reflectMenuIndex();
622
+ drawBlindMode();
560
623
  ctx.srSay(t('sr.audio.reset'));
561
624
  });
562
- try {
563
- if (window.speechSynthesis)
564
- window.speechSynthesis.onvoiceschanged = populateTtsVoices;
565
- }
566
- catch (e) { /* noop */ }
567
- const ttsVolEl = ctx.$('#tts-vol');
568
- if (ttsVolEl)
569
- ttsVolEl.addEventListener('input', () => {
570
- const state = ctx.getAudioCat();
571
- if (!state)
572
- return;
573
- state.tts.vol = (+ttsVolEl.value) / 100;
574
- state.tts.on = true;
575
- ctx.setCatGain('tts');
576
- reflectTts();
625
+ // The AUDIO panel's reset: the taste categories and nothing else.
626
+ const soundReset = ctx.$('#som-reset');
627
+ if (soundReset)
628
+ soundReset.addEventListener('click', () => {
629
+ resetCategories(GEN_CATS);
630
+ renderAudio();
631
+ ctx.srSay(t('sr.audio.reset'));
577
632
  });
578
633
  const audioDetectBtn = ctx.$('#audio-detect');
579
634
  if (audioDetectBtn)
580
635
  audioDetectBtn.addEventListener('click', () => { void detectAudioDevices(); });
581
- reflectMaster(); // estado inicial do botão/slider mestre, antes de qualquer abertura do painel
636
+ reflectMaster(); // initial state of the master button/slider, before the panel is ever opened
582
637
  /*
583
- * ⚠️ O PAINEL ASSINA O EVENTO, e isto não é uma ideia nova: é a decisão que o `core/state` já tinha
584
- * escrito ao lado do `setModoCegoValue` — «o setter faz três coisas e só três: grava, persiste, avisa. Os
585
- * efeitos … são reação, e quem reage assina o evento».
638
+ * ⚠️ THE PANEL SUBSCRIBES TO THE EVENT — the decision `core/state` writes next to `setBlindModeValue`: a setter stores,
639
+ * persists and notifies, and effects are reactions, subscribed by whoever reacts.
586
640
  *
587
- * Sem esta assinatura, um jogo que NÃO injecta o seu próprio `setModoCego` liga o modo cego pelo ícone da
588
- * barra e o botão `#opt-modocego` deste painel continua a dizer «Desligado», com `aria-pressed=false` — o
589
- * controlo a mentir o estado para o leitor de tela. É o gémeo exacto do defeito do `reflectTTS` que já está
590
- * registado no `ui/pause-icons`, e não vale a pena descobri-lo uma terceira vez.
641
+ * Without it, a game that does NOT inject its own `setBlindMode` switches blind mode on through the bar's icon and this
642
+ * panel's `#opt-modocego` keeps saying off, with `aria-pressed=false` — the control lying about the state to the screen
643
+ * reader.
591
644
  *
592
- * ⚠️ É seguro para quem JÁ reflecte a partir do seu próprio setter: reflectir é idempotente — relê o estado
593
- * e reescreve o botão. Um anúncio duplicado seria outra história, e por isso a assinatura NÃO anuncia: o
594
- * ícone da barra já diz `sr.icon.blindOn`/`Off` por si.
645
+ * ⚠️ It is safe for whoever ALREADY reflects from their own setter: reflecting is idempotent — it rereads the state and
646
+ * rewrites the button. A duplicated announcement would be another matter, which is why the subscription does NOT
647
+ * announce: the bar's icon already speaks for itself.
595
648
  */
596
- state.on('modoCego', () => { reflectModoCego(); });
597
- return { renderAudio, reflectModoCego, reflectTts };
649
+ ctx.on('blindMode', () => { drawBlindMode(); }); // through the root's door: released by its `dispose()`
650
+ /** One category, in place: the lists hold rows only once the panel has drawn them, and a row not there is not drawn here. */
651
+ function reflectCategory(k) {
652
+ if (k === 'tts')
653
+ voice.reflectTts();
654
+ for (const sel of ['#audio-list', '#navsound-list']) {
655
+ const list = ctx.$(sel);
656
+ if (list)
657
+ reflectCatRows(list, [k]);
658
+ }
659
+ const navMaster = ctx.$('#navsound-master');
660
+ const state = ctx.getAudioCat();
661
+ if (navMaster && state)
662
+ navMaster.value = String(navMasterVolume(state, NAV_CATS));
663
+ refreshMarks();
664
+ }
665
+ return { renderAudio, reflectBlindMode: drawBlindMode, reflectTts: voice.reflectTts, reflectCategory };
598
666
  }