@the-inclusionist/engine 6.36.1

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 (267) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +107 -0
  3. package/app/css/style.css +622 -0
  4. package/app/public/vendor/fonts/andika-400-ext.woff2 +0 -0
  5. package/app/public/vendor/fonts/andika-400.woff2 +0 -0
  6. package/app/public/vendor/fonts/andika-700-ext.woff2 +0 -0
  7. package/app/public/vendor/fonts/andika-700.woff2 +0 -0
  8. package/app/public/vendor/fonts/atkinson-400-ext.woff2 +0 -0
  9. package/app/public/vendor/fonts/atkinson-400.woff2 +0 -0
  10. package/app/public/vendor/fonts/atkinson-700-ext.woff2 +0 -0
  11. package/app/public/vendor/fonts/atkinson-700.woff2 +0 -0
  12. package/app/public/vendor/fonts/atkinson-mono-400-ext.woff2 +0 -0
  13. package/app/public/vendor/fonts/atkinson-mono-400.woff2 +0 -0
  14. package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
  15. package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
  16. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  17. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  18. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  19. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  20. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  21. package/app/public/vendor/fonts/lexend-400-ext.woff2 +0 -0
  22. package/app/public/vendor/fonts/lexend-400.woff2 +0 -0
  23. package/app/public/vendor/fonts/lexend-700-ext.woff2 +0 -0
  24. package/app/public/vendor/fonts/lexend-700.woff2 +0 -0
  25. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  26. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  27. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  28. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  29. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  30. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  31. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  32. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  33. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  34. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  35. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  36. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  37. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  38. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
  39. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  40. package/app/public/vendor/fonts.css +88 -0
  41. package/dist-pkg/boot/create-game.d.ts +159 -0
  42. package/dist-pkg/boot/create-game.js +266 -0
  43. package/dist-pkg/core/a11y-sr.d.ts +4 -0
  44. package/dist-pkg/core/a11y-sr.js +20 -0
  45. package/dist-pkg/core/actions.d.ts +100 -0
  46. package/dist-pkg/core/actions.js +149 -0
  47. package/dist-pkg/core/anel.d.ts +17 -0
  48. package/dist-pkg/core/anel.js +31 -0
  49. package/dist-pkg/core/collision.d.ts +21 -0
  50. package/dist-pkg/core/collision.js +62 -0
  51. package/dist-pkg/core/constants.d.ts +84 -0
  52. package/dist-pkg/core/constants.js +64 -0
  53. package/dist-pkg/core/contract.d.ts +227 -0
  54. package/dist-pkg/core/contract.js +184 -0
  55. package/dist-pkg/core/dom-query.d.ts +9 -0
  56. package/dist-pkg/core/dom-query.js +25 -0
  57. package/dist-pkg/core/entity.d.ts +150 -0
  58. package/dist-pkg/core/entity.js +38 -0
  59. package/dist-pkg/core/escape-html.d.ts +16 -0
  60. package/dist-pkg/core/escape-html.js +32 -0
  61. package/dist-pkg/core/i18n.d.ts +71 -0
  62. package/dist-pkg/core/i18n.js +236 -0
  63. package/dist-pkg/core/layers.d.ts +39 -0
  64. package/dist-pkg/core/layers.js +71 -0
  65. package/dist-pkg/core/letter-grid.d.ts +55 -0
  66. package/dist-pkg/core/letter-grid.js +116 -0
  67. package/dist-pkg/core/loop.d.ts +17 -0
  68. package/dist-pkg/core/loop.js +36 -0
  69. package/dist-pkg/core/password.d.ts +34 -0
  70. package/dist-pkg/core/password.js +205 -0
  71. package/dist-pkg/core/rng.d.ts +20 -0
  72. package/dist-pkg/core/rng.js +61 -0
  73. package/dist-pkg/core/rotulo-acessivel.d.ts +17 -0
  74. package/dist-pkg/core/rotulo-acessivel.js +41 -0
  75. package/dist-pkg/core/run-state.d.ts +115 -0
  76. package/dist-pkg/core/run-state.js +40 -0
  77. package/dist-pkg/core/scenes.d.ts +68 -0
  78. package/dist-pkg/core/scenes.js +64 -0
  79. package/dist-pkg/core/screens.d.ts +16 -0
  80. package/dist-pkg/core/screens.js +28 -0
  81. package/dist-pkg/core/state.d.ts +138 -0
  82. package/dist-pkg/core/state.js +290 -0
  83. package/dist-pkg/core/tiles.d.ts +12 -0
  84. package/dist-pkg/core/tiles.js +61 -0
  85. package/dist-pkg/core/world.d.ts +4 -0
  86. package/dist-pkg/core/world.js +58 -0
  87. package/dist-pkg/educational/activities-registry.d.ts +47 -0
  88. package/dist-pkg/educational/activities-registry.js +95 -0
  89. package/dist-pkg/i18n/en.d.ts +3 -0
  90. package/dist-pkg/i18n/en.js +560 -0
  91. package/dist-pkg/i18n/es.d.ts +3 -0
  92. package/dist-pkg/i18n/es.js +558 -0
  93. package/dist-pkg/i18n/pt.d.ts +3 -0
  94. package/dist-pkg/i18n/pt.js +618 -0
  95. package/dist-pkg/input/default-bindings.d.ts +24 -0
  96. package/dist-pkg/input/default-bindings.js +122 -0
  97. package/dist-pkg/input/devices.d.ts +15 -0
  98. package/dist-pkg/input/devices.js +31 -0
  99. package/dist-pkg/input/edges.d.ts +58 -0
  100. package/dist-pkg/input/edges.js +49 -0
  101. package/dist-pkg/input/gamepad.d.ts +203 -0
  102. package/dist-pkg/input/gamepad.js +531 -0
  103. package/dist-pkg/input/keyboard-runtime.d.ts +61 -0
  104. package/dist-pkg/input/keyboard-runtime.js +83 -0
  105. package/dist-pkg/input/keyboard.d.ts +45 -0
  106. package/dist-pkg/input/keyboard.js +80 -0
  107. package/dist-pkg/input/keydown.d.ts +270 -0
  108. package/dist-pkg/input/keydown.js +381 -0
  109. package/dist-pkg/input/latch.d.ts +30 -0
  110. package/dist-pkg/input/latch.js +63 -0
  111. package/dist-pkg/input/pointer-space.d.ts +40 -0
  112. package/dist-pkg/input/pointer-space.js +51 -0
  113. package/dist-pkg/input/state.d.ts +11 -0
  114. package/dist-pkg/input/state.js +12 -0
  115. package/dist-pkg/input/touch-bindings.d.ts +210 -0
  116. package/dist-pkg/input/touch-bindings.js +365 -0
  117. package/dist-pkg/input/touch.d.ts +173 -0
  118. package/dist-pkg/input/touch.js +314 -0
  119. package/dist-pkg/input/transports.d.ts +90 -0
  120. package/dist-pkg/input/transports.js +96 -0
  121. package/dist-pkg/input/vocabulary-migration.d.ts +58 -0
  122. package/dist-pkg/input/vocabulary-migration.js +125 -0
  123. package/dist-pkg/platform/audio-ambient.d.ts +27 -0
  124. package/dist-pkg/platform/audio-ambient.js +89 -0
  125. package/dist-pkg/platform/audio-earcons.d.ts +24 -0
  126. package/dist-pkg/platform/audio-earcons.js +62 -0
  127. package/dist-pkg/platform/audio-jingles.d.ts +17 -0
  128. package/dist-pkg/platform/audio-jingles.js +54 -0
  129. package/dist-pkg/platform/audio-mixer.d.ts +14 -0
  130. package/dist-pkg/platform/audio-mixer.js +62 -0
  131. package/dist-pkg/platform/audio-nav.d.ts +51 -0
  132. package/dist-pkg/platform/audio-nav.js +99 -0
  133. package/dist-pkg/platform/audio-sonar.d.ts +71 -0
  134. package/dist-pkg/platform/audio-sonar.js +173 -0
  135. package/dist-pkg/platform/audio.d.ts +32 -0
  136. package/dist-pkg/platform/audio.js +191 -0
  137. package/dist-pkg/platform/interruptible-speech.d.ts +19 -0
  138. package/dist-pkg/platform/interruptible-speech.js +79 -0
  139. package/dist-pkg/platform/speech.d.ts +2 -0
  140. package/dist-pkg/platform/speech.js +35 -0
  141. package/dist-pkg/platform/storage.d.ts +87 -0
  142. package/dist-pkg/platform/storage.js +148 -0
  143. package/dist-pkg/platform/tts.d.ts +33 -0
  144. package/dist-pkg/platform/tts.js +142 -0
  145. package/dist-pkg/render/camera.d.ts +58 -0
  146. package/dist-pkg/render/camera.js +105 -0
  147. package/dist-pkg/render/canvas.d.ts +13 -0
  148. package/dist-pkg/render/canvas.js +26 -0
  149. package/dist-pkg/render/cenario-data.d.ts +127 -0
  150. package/dist-pkg/render/cenario-data.js +145 -0
  151. package/dist-pkg/render/city-tex.d.ts +63 -0
  152. package/dist-pkg/render/city-tex.js +228 -0
  153. package/dist-pkg/render/city-tiles.d.ts +11 -0
  154. package/dist-pkg/render/city-tiles.js +140 -0
  155. package/dist-pkg/render/crt.d.ts +19 -0
  156. package/dist-pkg/render/crt.js +92 -0
  157. package/dist-pkg/render/cvd-matrices.d.ts +35 -0
  158. package/dist-pkg/render/cvd-matrices.js +91 -0
  159. package/dist-pkg/render/draw.d.ts +149 -0
  160. package/dist-pkg/render/draw.js +221 -0
  161. package/dist-pkg/render/fx.d.ts +63 -0
  162. package/dist-pkg/render/fx.js +123 -0
  163. package/dist-pkg/render/hc-role-data.d.ts +14 -0
  164. package/dist-pkg/render/hc-role-data.js +24 -0
  165. package/dist-pkg/render/high-contrast.d.ts +109 -0
  166. package/dist-pkg/render/high-contrast.js +254 -0
  167. package/dist-pkg/render/lq-filter.d.ts +37 -0
  168. package/dist-pkg/render/lq-filter.js +94 -0
  169. package/dist-pkg/render/minimap.d.ts +11 -0
  170. package/dist-pkg/render/minimap.js +94 -0
  171. package/dist-pkg/render/parallax.d.ts +85 -0
  172. package/dist-pkg/render/parallax.js +158 -0
  173. package/dist-pkg/render/player-anim.d.ts +71 -0
  174. package/dist-pkg/render/player-anim.js +152 -0
  175. package/dist-pkg/render/port.d.ts +143 -0
  176. package/dist-pkg/render/port.js +29 -0
  177. package/dist-pkg/render/recycling-tex.d.ts +48 -0
  178. package/dist-pkg/render/recycling-tex.js +164 -0
  179. package/dist-pkg/render/scene-city.d.ts +77 -0
  180. package/dist-pkg/render/scene-city.js +181 -0
  181. package/dist-pkg/render/scene-parallax.d.ts +72 -0
  182. package/dist-pkg/render/scene-parallax.js +426 -0
  183. package/dist-pkg/render/scene-sky.d.ts +126 -0
  184. package/dist-pkg/render/scene-sky.js +295 -0
  185. package/dist-pkg/render/screen-pipeline.d.ts +134 -0
  186. package/dist-pkg/render/screen-pipeline.js +177 -0
  187. package/dist-pkg/render/set-cenario.d.ts +54 -0
  188. package/dist-pkg/render/set-cenario.js +103 -0
  189. package/dist-pkg/render/sprite-fx.d.ts +3 -0
  190. package/dist-pkg/render/sprite-fx.js +40 -0
  191. package/dist-pkg/render/textures.d.ts +75 -0
  192. package/dist-pkg/render/textures.js +240 -0
  193. package/dist-pkg/render/title-scene.d.ts +46 -0
  194. package/dist-pkg/render/title-scene.js +75 -0
  195. package/dist-pkg/render/viewports.d.ts +52 -0
  196. package/dist-pkg/render/viewports.js +185 -0
  197. package/dist-pkg/render/viz-axes.d.ts +98 -0
  198. package/dist-pkg/render/viz-axes.js +184 -0
  199. package/dist-pkg/render/viz-modes.d.ts +61 -0
  200. package/dist-pkg/render/viz-modes.js +74 -0
  201. package/dist-pkg/render/viz-setters.d.ts +130 -0
  202. package/dist-pkg/render/viz-setters.js +244 -0
  203. package/dist-pkg/render/weather.d.ts +96 -0
  204. package/dist-pkg/render/weather.js +171 -0
  205. package/dist-pkg/render/wheelchair-sprites.d.ts +24 -0
  206. package/dist-pkg/render/wheelchair-sprites.js +63 -0
  207. package/dist-pkg/render/world-tex.d.ts +18 -0
  208. package/dist-pkg/render/world-tex.js +87 -0
  209. package/dist-pkg/ui/activities-menu.d.ts +258 -0
  210. package/dist-pkg/ui/activities-menu.js +648 -0
  211. package/dist-pkg/ui/caa-sets.d.ts +39 -0
  212. package/dist-pkg/ui/caa-sets.js +65 -0
  213. package/dist-pkg/ui/changed-mark.d.ts +15 -0
  214. package/dist-pkg/ui/changed-mark.js +96 -0
  215. package/dist-pkg/ui/debug-panel.d.ts +67 -0
  216. package/dist-pkg/ui/debug-panel.js +173 -0
  217. package/dist-pkg/ui/dom.d.ts +24 -0
  218. package/dist-pkg/ui/dom.js +47 -0
  219. package/dist-pkg/ui/focus-trap.d.ts +71 -0
  220. package/dist-pkg/ui/focus-trap.js +99 -0
  221. package/dist-pkg/ui/fonts.d.ts +41 -0
  222. package/dist-pkg/ui/fonts.js +59 -0
  223. package/dist-pkg/ui/hud.d.ts +137 -0
  224. package/dist-pkg/ui/hud.js +229 -0
  225. package/dist-pkg/ui/item-announcement.d.ts +19 -0
  226. package/dist-pkg/ui/item-announcement.js +36 -0
  227. package/dist-pkg/ui/layout.d.ts +6 -0
  228. package/dist-pkg/ui/layout.js +54 -0
  229. package/dist-pkg/ui/loop-crash.d.ts +19 -0
  230. package/dist-pkg/ui/loop-crash.js +60 -0
  231. package/dist-pkg/ui/map-hub.d.ts +56 -0
  232. package/dist-pkg/ui/map-hub.js +138 -0
  233. package/dist-pkg/ui/menu-nav.d.ts +141 -0
  234. package/dist-pkg/ui/menu-nav.js +390 -0
  235. package/dist-pkg/ui/pause-icons.d.ts +327 -0
  236. package/dist-pkg/ui/pause-icons.js +620 -0
  237. package/dist-pkg/ui/reach-notice.d.ts +34 -0
  238. package/dist-pkg/ui/reach-notice.js +71 -0
  239. package/dist-pkg/ui/settings-audio.d.ts +139 -0
  240. package/dist-pkg/ui/settings-audio.js +544 -0
  241. package/dist-pkg/ui/settings-caa.d.ts +45 -0
  242. package/dist-pkg/ui/settings-caa.js +137 -0
  243. package/dist-pkg/ui/settings-controls.d.ts +100 -0
  244. package/dist-pkg/ui/settings-controls.js +152 -0
  245. package/dist-pkg/ui/settings-empathy.d.ts +62 -0
  246. package/dist-pkg/ui/settings-empathy.js +137 -0
  247. package/dist-pkg/ui/settings-motion.d.ts +95 -0
  248. package/dist-pkg/ui/settings-motion.js +282 -0
  249. package/dist-pkg/ui/settings-motor.d.ts +77 -0
  250. package/dist-pkg/ui/settings-motor.js +215 -0
  251. package/dist-pkg/ui/settings-panel.d.ts +76 -0
  252. package/dist-pkg/ui/settings-panel.js +187 -0
  253. package/dist-pkg/ui/settings-typo.d.ts +74 -0
  254. package/dist-pkg/ui/settings-typo.js +150 -0
  255. package/dist-pkg/ui/settings-visual.d.ts +111 -0
  256. package/dist-pkg/ui/settings-visual.js +252 -0
  257. package/dist-pkg/ui/shell.d.ts +239 -0
  258. package/dist-pkg/ui/shell.js +430 -0
  259. package/dist-pkg/ui/title.d.ts +36 -0
  260. package/dist-pkg/ui/title.js +60 -0
  261. package/dist-pkg/ui/vlibras.d.ts +11 -0
  262. package/dist-pkg/ui/vlibras.js +87 -0
  263. package/dist-pkg/ui/webcam.d.ts +7 -0
  264. package/dist-pkg/ui/webcam.js +88 -0
  265. package/docs/CREDITS.md +64 -0
  266. package/docs/LICENSES.md +132 -0
  267. package/package.json +115 -0
@@ -0,0 +1,544 @@
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.
15
+ /** Minimal DOM-selector shape (matches ui/dom.ts's `$`). */
16
+ 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';
22
+ import { markChanged, markMenuChanged } from './changed-mark.js';
23
+ 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');
62
+ }
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
+ /**
73
+ * As vozes do sistema no idioma pedido; sem nenhuma, a lista inteira.
74
+ *
75
+ * Chamava-se `pickPtVoices` e filtrava `/^pt/i` fixo, de modo que o jogo em inglês oferecia à pessoa uma
76
+ * lista de vozes PORTUGUESAS para ler texto em inglês. O nome dizia a verdade sobre o que fazia e mentia
77
+ * sobre o que devia fazer.
78
+ *
79
+ * A comparação é pelo PREFIXO de idioma, não pela etiqueta inteira: quem joga em pt-BR também deve poder
80
+ * escolher uma voz pt-PT se for a única instalada, e o navegador de uma escola raramente tem a variante
81
+ * exata. O recuo para a lista inteira fica: uma lista vazia seria pior que uma lista no idioma errado, que
82
+ * ao menos a pessoa pode ouvir e rejeitar.
83
+ */
84
+ export function pickVoicesFor(voices, lang) {
85
+ const pref = lang.slice(0, 2).toLowerCase();
86
+ const iguais = voices.filter((v) => v.lang.slice(0, 2).toLowerCase() === pref);
87
+ return iguais.length ? iguais : voices;
88
+ }
89
+ /** "<name> (<lang>)" option label. */
90
+ export function voiceLabel(v) {
91
+ return v.name + ' (' + v.lang + ')';
92
+ }
93
+ /** Whether the browser can list/switch audio outputs at all (gates the #audio-sinks hint copy). */
94
+ export function sinksSupported(hasEnumerateDevices, hasAudioContextCtor) {
95
+ return hasEnumerateDevices && hasAudioContextCtor;
96
+ }
97
+ /** A device's option label, falling back to a 1-based "Saída N" when the browser withholds the real label
98
+ * (no getUserMedia permission granted yet). */
99
+ export function sinkOptionLabel(d, index) {
100
+ return d.label || t('audio.sinkFallback', { n: index + 1 });
101
+ }
102
+ /** A player's current sink select value ('' = default/shared). */
103
+ export function sinkSelectValue(p) {
104
+ return (p && p.audioSink) || '';
105
+ }
106
+ // ---------------------------------------------------------------------------------------------
107
+ // DOM-facing (thin) — requires `document`/injected ctx
108
+ // ---------------------------------------------------------------------------------------------
109
+ export function initSettingsAudio(ctx) {
110
+ let audioDevices = [];
111
+ function reflectMaster() {
112
+ const b = ctx.$('#audio-master');
113
+ if (b) {
114
+ b.classList.toggle('is-on', ctx.getSoundOn());
115
+ b.setAttribute('aria-pressed', String(ctx.getSoundOn()));
116
+ b.textContent = ctx.getSoundOn() ? '🔊 Ligado' : '🔇 Desligado';
117
+ }
118
+ const v = ctx.$('#audio-master-vol');
119
+ if (v)
120
+ v.value = String(volPercent(ctx.getVolume()));
121
+ const sb = ctx.$('#opt-sound');
122
+ if (sb)
123
+ ctx.toggleBtn(sb, ctx.getSoundOn());
124
+ }
125
+ function wireCatControls(el) {
126
+ el.querySelectorAll('button[data-acat]').forEach((b) => {
127
+ b.addEventListener('click', () => {
128
+ const k = b.dataset.acat;
129
+ if (!k)
130
+ return;
131
+ const state = ctx.getAudioCat();
132
+ if (!state || !state[k])
133
+ return;
134
+ state[k].on = !state[k].on;
135
+ ctx.setCatGain(k);
136
+ b.classList.toggle('is-on', state[k].on);
137
+ b.setAttribute('aria-pressed', String(state[k].on));
138
+ refreshMarks(); // a marca acompanha a MUDANÇA, não só o redesenho — ver a nota em refreshMarks
139
+ });
140
+ });
141
+ el.querySelectorAll('input[data-avol]').forEach((s) => {
142
+ s.addEventListener('input', () => {
143
+ const k = s.dataset.avol;
144
+ if (!k)
145
+ return;
146
+ const state = ctx.getAudioCat();
147
+ if (!state || !state[k])
148
+ return;
149
+ state[k].vol = (+s.value) / 100;
150
+ state[k].on = true;
151
+ ctx.setCatGain(k);
152
+ const bb = el.querySelector('button[data-acat="' + k + '"]');
153
+ if (bb) {
154
+ bb.classList.add('is-on');
155
+ bb.setAttribute('aria-pressed', 'true');
156
+ }
157
+ refreshMarks();
158
+ });
159
+ });
160
+ }
161
+ function renderCategoryList(sel, keys) {
162
+ const el = ctx.$(sel);
163
+ const state = ctx.getAudioCat();
164
+ if (!el || !state)
165
+ return;
166
+ el.innerHTML = catsListHTML(keys, ctx.audioCats, state);
167
+ wireCatControls(el);
168
+ }
169
+ function renderNavSound() {
170
+ renderCategoryList('#navsound-list', NAV_CATS);
171
+ const m = ctx.$('#navsound-master');
172
+ const state = ctx.getAudioCat();
173
+ if (m && state)
174
+ m.value = String(navMasterVolume(state, NAV_CATS));
175
+ }
176
+ function reflectTts() {
177
+ const b = ctx.$('#opt-tts');
178
+ const state = ctx.getAudioCat();
179
+ const on = !!(state && state.tts && state.tts.on);
180
+ if (b) {
181
+ ctx.toggleBtn(b, on);
182
+ b.textContent = toggleLabel(on);
183
+ }
184
+ const e = ctx.$('#tts-engine');
185
+ if (e)
186
+ e.value = ctx.tts.getEngineSel();
187
+ }
188
+ /**
189
+ * O alternador do ÍNDICE "6 de 10" (ADR-0044, item 3).
190
+ *
191
+ * Ele mora ao lado da narração por voz e não num painel de visual, porque é a NARRAÇÃO que ele muda: quem
192
+ * desliga o índice está encurtando o que ouve a cada passo, e é aqui que essa pessoa vem quando o que ouve
193
+ * incomoda.
194
+ */
195
+ function reflectMenuIndex() {
196
+ const b = ctx.$('#opt-menuindex');
197
+ if (b) {
198
+ ctx.toggleBtn(b, state.menuIndexOn);
199
+ b.textContent = toggleLabel(state.menuIndexOn);
200
+ }
201
+ }
202
+ function populateTtsEngines() {
203
+ const sel = ctx.$('#tts-engine');
204
+ if (!sel || sel.dataset.filled)
205
+ return;
206
+ sel.dataset.filled = '1';
207
+ TTS_ENGINE_OPTIONS.forEach(([v, l]) => {
208
+ const o = document.createElement('option');
209
+ o.value = v;
210
+ o.textContent = t(l);
211
+ sel.appendChild(o);
212
+ });
213
+ sel.value = ctx.tts.getEngineSel();
214
+ }
215
+ function populateTtsVoices() {
216
+ const sel = ctx.$('#tts-voice');
217
+ if (!sel)
218
+ return;
219
+ let voices = [];
220
+ try {
221
+ voices = (window.speechSynthesis && window.speechSynthesis.getVoices()) || [];
222
+ }
223
+ catch (e) { /* noop */ }
224
+ const list = pickVoicesFor(voices, bcp47());
225
+ sel.innerHTML = '';
226
+ if (!list.length) {
227
+ const o = document.createElement('option');
228
+ o.textContent = t('audio.noSystemVoices');
229
+ sel.appendChild(o);
230
+ ctx.tts.setVoiceObj(null);
231
+ return;
232
+ }
233
+ list.forEach((v) => {
234
+ const o = document.createElement('option');
235
+ o.value = v.name;
236
+ o.textContent = voiceLabel(v);
237
+ sel.appendChild(o);
238
+ });
239
+ const saved = ctx.store.get('incl_tts_voice', null);
240
+ const pick = list.find((v) => v.name === saved) || list[0];
241
+ sel.value = pick.name;
242
+ ctx.tts.setVoiceObj(pick);
243
+ }
244
+ function hasEnumerateDevices() {
245
+ return !!(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices);
246
+ }
247
+ function hasAudioContextCtor() {
248
+ const w = window;
249
+ return typeof (w.AudioContext || w.webkitAudioContext) !== 'undefined';
250
+ }
251
+ function renderSinks(devices) {
252
+ const el = ctx.$('#audio-sinks');
253
+ if (!el)
254
+ return;
255
+ el.innerHTML = '';
256
+ if (!devices.length) {
257
+ const supported = sinksSupported(hasEnumerateDevices(), hasAudioContextCtor());
258
+ el.innerHTML = '<p class="opt-hint">' +
259
+ t(supported ? 'audio.sinksHint' : 'audio.sinksUnsupported') +
260
+ '</p>';
261
+ return;
262
+ }
263
+ const players = ctx.getPlayers();
264
+ const n = Math.max(1, ctx.getNumPlayers());
265
+ for (let i = 0; i < n; i++) {
266
+ const p = players[i];
267
+ const row = document.createElement('div');
268
+ row.className = 'ctrl-row';
269
+ const lbl = document.createElement('label');
270
+ lbl.textContent = t('audio.playerN', { n: i + 1 });
271
+ lbl.setAttribute('for', 'sink-p' + i);
272
+ const sel = document.createElement('select');
273
+ sel.className = 'vol';
274
+ sel.id = 'sink-p' + i;
275
+ const o0 = document.createElement('option');
276
+ o0.value = '';
277
+ o0.textContent = t('audio.sinkShared');
278
+ sel.appendChild(o0);
279
+ devices.forEach((d, k) => {
280
+ const o = document.createElement('option');
281
+ o.value = d.deviceId;
282
+ o.textContent = sinkOptionLabel(d, k);
283
+ sel.appendChild(o);
284
+ });
285
+ sel.value = sinkSelectValue(p);
286
+ sel.addEventListener('change', () => {
287
+ if (!p)
288
+ return;
289
+ p.audioSink = sel.value || null;
290
+ ctx.store.set('incl_sink_p' + i, p.audioSink || '');
291
+ if (p._ac) {
292
+ try {
293
+ p._ac.close();
294
+ }
295
+ catch (e) { /* noop */ }
296
+ p._ac = null;
297
+ p._acOut = null;
298
+ }
299
+ ctx.srSay(t(p.audioSink ? 'sr.audio.sinkChanged' : 'sr.audio.sinkDefault', { n: i + 1 }));
300
+ });
301
+ row.appendChild(lbl);
302
+ row.appendChild(sel);
303
+ el.appendChild(row);
304
+ }
305
+ }
306
+ async function enumerateSinks() {
307
+ try {
308
+ if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
309
+ const devs = await navigator.mediaDevices.enumerateDevices();
310
+ audioDevices = devs.filter((d) => d.kind === 'audiooutput');
311
+ }
312
+ }
313
+ catch (e) { /* sem pedir permissão: só lista as saídas já conhecidas */ }
314
+ renderSinks(audioDevices);
315
+ }
316
+ async function detectAudioDevices() {
317
+ try {
318
+ await navigator.mediaDevices.getUserMedia({ audio: true }).then((s) => s.getTracks().forEach((t) => t.stop())).catch(() => { });
319
+ const devs = await navigator.mediaDevices.enumerateDevices();
320
+ audioDevices = devs.filter((d) => d.kind === 'audiooutput');
321
+ }
322
+ catch (e) {
323
+ audioDevices = [];
324
+ }
325
+ renderSinks(audioDevices);
326
+ }
327
+ function reflectModoCego() {
328
+ const b = ctx.$('#opt-modocego');
329
+ if (b) {
330
+ ctx.toggleBtn(b, ctx.getModoCego());
331
+ b.textContent = toggleLabel(ctx.getModoCego());
332
+ }
333
+ }
334
+ function renderAudio() {
335
+ reflectMaster();
336
+ renderCategoryList('#audio-list', GEN_CATS);
337
+ renderNavSound();
338
+ reflectModoCego();
339
+ reflectTts();
340
+ populateTtsEngines();
341
+ populateTtsVoices();
342
+ const cd = ctx.$('#cane-div');
343
+ if (cd)
344
+ cd.value = String(ctx.getCaneBlockDiv());
345
+ void enumerateSinks(); // sem await no original: dispara e segue (lista assíncrona atualiza sozinha)
346
+ refreshMarks();
347
+ }
348
+ /**
349
+ * A marca de "saiu do padrão" (ADR-0029).
350
+ *
351
+ * Chamada de dentro dos HANDLERS de mudança, e não só do `renderAudio()`. Pendurei-a primeiro no render, e
352
+ * no jogo a marca não aparecia: mexer numa categoria atualiza aquela linha sozinha, sem redesenhar o painel.
353
+ * O ADR-0029 já avisava disso — "pode envelhecer na tela se um painel esquecer de atualizar depois de uma
354
+ * mudança" — e eu escrevi o aviso e caí nele na mesma tarde. A regra que sobra: a marca anda com quem
355
+ * ESCREVE o valor, nunca com quem desenha.
356
+ *
357
+ * O recorte é o MESMO do reset deste menu, e isso não é economia —
358
+ * é a regra: só pode ser marcado o que tem padrão em DEFAULTS/`defaultAudioCat`. O motor de voz e a saída
359
+ * de áudio por jogador não têm, porque são escolha de DISPOSITIVO e não preferência restaurável; marcá-los
360
+ * exigiria inventar uma segunda opinião sobre o que é "padrão" para um fone.
361
+ */
362
+ function refreshMarks() {
363
+ const state = ctx.getAudioCat();
364
+ const mudou = [];
365
+ const marcar = (sel, changed) => {
366
+ mudou.push(changed);
367
+ markChanged(ctx.$(sel)?.closest('.ctrl-row') ?? null, changed);
368
+ };
369
+ marcar('#opt-modocego', ctx.getModoCego() !== DEFAULTS.modoCego);
370
+ marcar('#cane-div', ctx.getCaneBlockDiv() !== DEFAULTS.caneBlockDiv);
371
+ for (const c of ctx.audioCats) {
372
+ const d = defaultAudioCat(c.k);
373
+ const a = state?.[c.k];
374
+ marcar(`[data-acat="${c.k}"]`, !!a && (a.on !== d.on || a.vol !== d.vol));
375
+ }
376
+ markMenuChanged(ctx.$('[data-act="audio"]'), mudou);
377
+ }
378
+ // ----- widgets estáticos (existem sempre no #audio; fiados UMA vez, nunca recriados por renderAudio) -----
379
+ const audioMasterBtn = ctx.$('#audio-master');
380
+ if (audioMasterBtn)
381
+ audioMasterBtn.addEventListener('click', () => {
382
+ const next = !ctx.getSoundOn();
383
+ ctx.setSoundOn(next);
384
+ reflectMaster();
385
+ ctx.srSay(t(next ? 'sr.audio.soundOn' : 'sr.audio.soundOff'));
386
+ });
387
+ const audioMasterVol = ctx.$('#audio-master-vol');
388
+ if (audioMasterVol)
389
+ audioMasterVol.addEventListener('input', () => {
390
+ const v = (+audioMasterVol.value) / 100;
391
+ ctx.setVolume(v);
392
+ if (v > 0 && !ctx.getSoundOn()) {
393
+ ctx.setSoundOn(true);
394
+ reflectMaster();
395
+ }
396
+ });
397
+ const navMasterEl = ctx.$('#navsound-master');
398
+ if (navMasterEl)
399
+ navMasterEl.addEventListener('input', () => {
400
+ const v = (+navMasterEl.value) / 100;
401
+ const state = ctx.getAudioCat();
402
+ if (state)
403
+ NAV_CATS.forEach((k) => { state[k].vol = v; state[k].on = true; ctx.setCatGain(k); });
404
+ renderNavSound();
405
+ });
406
+ const mcBtn = ctx.$('#opt-modocego');
407
+ if (mcBtn)
408
+ mcBtn.addEventListener('click', () => { ctx.setModoCego(!ctx.getModoCego()); reflectModoCego(); });
409
+ const caneDivSel = ctx.$('#cane-div');
410
+ if (caneDivSel) {
411
+ caneDivSel.value = String(ctx.getCaneBlockDiv());
412
+ caneDivSel.addEventListener('change', () => {
413
+ const div = parseCaneDiv(caneDivSel.value);
414
+ ctx.setCaneBlockDiv(div);
415
+ ctx.srSay(caneDivMessage(div));
416
+ });
417
+ }
418
+ const ttsBtn = ctx.$('#opt-tts');
419
+ if (ttsBtn)
420
+ ttsBtn.addEventListener('click', () => {
421
+ const state = ctx.getAudioCat();
422
+ if (!state)
423
+ return;
424
+ state.tts.on = !state.tts.on;
425
+ ctx.setCatGain('tts');
426
+ reflectTts();
427
+ ctx.srSay(t(state.tts.on ? 'sr.audio.ttsOn' : 'sr.audio.ttsOff'));
428
+ if (state.tts.on)
429
+ ctx.tts.narrate(t('sr.audio.ttsOnSpoken'));
430
+ });
431
+ const idxBtn = ctx.$('#opt-menuindex');
432
+ if (idxBtn)
433
+ idxBtn.addEventListener('click', () => {
434
+ state.setMenuIndexOnValue(!state.menuIndexOn);
435
+ reflectMenuIndex();
436
+ // 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
437
+ // justamente no momento em que a criança está julgando se o ruído incomoda.
438
+ ctx.srSay(t(state.menuIndexOn ? 'sr.menu.indexOn' : 'sr.menu.indexOff'));
439
+ });
440
+ const ttsEngSel = ctx.$('#tts-engine');
441
+ if (ttsEngSel)
442
+ ttsEngSel.addEventListener('change', () => {
443
+ ctx.tts.setEngineSel(ttsEngSel.value);
444
+ ctx.store.set('incl_tts_engine', ttsEngSel.value);
445
+ if (ttsEngSel.value !== 'webspeech')
446
+ ctx.tts.loadTTS();
447
+ const opt = ttsEngSel.options[ttsEngSel.selectedIndex];
448
+ ctx.tts.narrate(t('sr.audio.engineSet', { motor: opt ? opt.text : '' }));
449
+ });
450
+ const ttsVoiceSel = ctx.$('#tts-voice');
451
+ if (ttsVoiceSel)
452
+ ttsVoiceSel.addEventListener('change', () => {
453
+ try {
454
+ const vs = window.speechSynthesis.getVoices();
455
+ ctx.tts.setVoiceObj(vs.find((v) => v.name === ttsVoiceSel.value) || null);
456
+ ctx.store.set('incl_tts_voice', ttsVoiceSel.value);
457
+ }
458
+ catch (e) { /* noop */ }
459
+ ctx.tts.narrate(t('sr.audio.voicePicked'));
460
+ });
461
+ const ttsTestBtn = ctx.$('#opt-tts-test');
462
+ if (ttsTestBtn)
463
+ ttsTestBtn.addEventListener('click', () => {
464
+ const txt = t('audio.voiceSample');
465
+ const engine = ctx.tts.getEngine();
466
+ if (ctx.tts.getEngineSel() !== 'webspeech' && engine && engine.speak) {
467
+ try {
468
+ engine.speak(txt);
469
+ }
470
+ catch (e) { /* noop */ } // motor neural já carregado
471
+ }
472
+ else {
473
+ try {
474
+ const ss = window.speechSynthesis;
475
+ if (ss) {
476
+ ss.cancel();
477
+ const u = new SpeechSynthesisUtterance(txt);
478
+ u.lang = 'pt-BR';
479
+ const vo = ctx.tts.getVoiceObj();
480
+ if (vo)
481
+ u.voice = vo;
482
+ u.rate = 1;
483
+ u.volume = 1;
484
+ ss.speak(u);
485
+ }
486
+ }
487
+ catch (e) { /* noop */ } // fallback audível (volume 1) + dispara download do neural
488
+ if (ctx.tts.getEngineSel() !== 'webspeech')
489
+ ctx.tts.loadTTS();
490
+ }
491
+ ctx.srSay(t('sr.audio.testingVoice'));
492
+ });
493
+ // ---- restaurar os padrões DESTE menu (ADR-0028) ----
494
+ //
495
+ // A regra dura é o escopo: este botão restaura o que o menu AUDITIVO contém e nada mais. Um reset que
496
+ // alcançasse fora de si seria pior que a armadilha que ele existe para desfazer — a criança que desfaz um
497
+ // ajuste de som e perde de quebra a configuração motora fica sem conseguir jogar, e sem entender por quê.
498
+ //
499
+ // O que é deste menu: o modo cego, o espaçamento da bengala e as nove categorias do mixer. O motor de voz
500
+ // e a saída de áudio por jogador NÃO entram — são escolha de dispositivo, não preferência restaurável, e
501
+ // zerá-las tiraria da criança o fone que é dela numa sala compartilhada.
502
+ const resetBtn = ctx.$('#audio-reset');
503
+ if (resetBtn)
504
+ resetBtn.addEventListener('click', () => {
505
+ ctx.setModoCego(DEFAULTS.modoCego);
506
+ ctx.setCaneBlockDiv(DEFAULTS.caneBlockDiv);
507
+ const state = ctx.getAudioCat();
508
+ if (state)
509
+ for (const c of ctx.audioCats) {
510
+ const d = defaultAudioCat(c.k);
511
+ if (!state[c.k])
512
+ continue;
513
+ state[c.k].on = d.on;
514
+ state[c.k].vol = d.vol;
515
+ ctx.setCatGain(c.k);
516
+ }
517
+ renderAudio();
518
+ reflectModoCego();
519
+ reflectTts();
520
+ reflectMenuIndex();
521
+ ctx.srSay(t('sr.audio.reset'));
522
+ });
523
+ try {
524
+ if (window.speechSynthesis)
525
+ window.speechSynthesis.onvoiceschanged = populateTtsVoices;
526
+ }
527
+ catch (e) { /* noop */ }
528
+ const ttsVolEl = ctx.$('#tts-vol');
529
+ if (ttsVolEl)
530
+ ttsVolEl.addEventListener('input', () => {
531
+ const state = ctx.getAudioCat();
532
+ if (!state)
533
+ return;
534
+ state.tts.vol = (+ttsVolEl.value) / 100;
535
+ state.tts.on = true;
536
+ ctx.setCatGain('tts');
537
+ reflectTts();
538
+ });
539
+ const audioDetectBtn = ctx.$('#audio-detect');
540
+ if (audioDetectBtn)
541
+ audioDetectBtn.addEventListener('click', () => { void detectAudioDevices(); });
542
+ reflectMaster(); // estado inicial do botão/slider mestre, antes de qualquer abertura do painel
543
+ return { renderAudio, reflectModoCego, reflectTts };
544
+ }
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { LetterCase } from '../core/state.js';
3
+ import { type CaaSet } from './caa-sets.js';
4
+ /**
5
+ * A linha das LETRAS é um INTERRUPTOR, não duas opções (decisão do Dev). Ligado = só maiúsculas; desligado =
6
+ * maiúsculas e minúsculas. Uma pergunta binária apresentada como duas linhas obriga a criança a comparar as
7
+ * duas para descobrir que são a mesma pergunta.
8
+ */
9
+ export declare function caixaAltaLigada(caso: LetterCase): boolean;
10
+ /** O interruptor das letras. Sempre disponível: é o piso offline, e nunca dependeu de arquivo nenhum. */
11
+ export declare function letrasRowHtml(ligado: boolean): string;
12
+ /**
13
+ * Uma linha do menu: RÓTULO CURTO e nada mais à vista.
14
+ *
15
+ * Tudo o que explica — nota, licença, situação — entra num único `.opt-hint`, que é o que a casca
16
+ * (`fillExplain`) reconhece e MOVE para o rodapé. A primeira versão pendurava três blocos de prosa dentro da
17
+ * linha, e o Dev viu o resultado: o menu virou um manual, mais parecido com um arquivo de configuração do que
18
+ * com um menu de videogame. A explicação já tinha um lugar; eu é que não a pus lá.
19
+ *
20
+ * O `disabled` fica: a situação some do TEXTO, não do controle. E o `aria-label` guarda o motivo, para quem
21
+ * navega por teclado ouvir por que a linha não responde sem precisar caçar o rodapé.
22
+ */
23
+ export declare function caaRowHtml(s: CaaSet, selecionada: boolean): string;
24
+ /** O corpo do menu, em três seções — a seção diz de quem é a vez de agir. */
25
+ export declare function caaListHtml(caso: LetterCase): string;
26
+ export interface SettingsCaaCtx {
27
+ $: <T extends Element = Element>(sel: string) => T | null;
28
+ srSay: (msg: string) => void;
29
+ /** Leitura viva de core/state `letterCase` (o binding é reatribuído pelo setter). */
30
+ getLetterCase: () => LetterCase;
31
+ /** `setLetterCaseValue` de core/state MAIS a reflexão que o jogo precisa (re-render do quiz, rótulos). */
32
+ setLetterCase: (c: LetterCase) => void;
33
+ frontOverlay: (el: HTMLElement | null) => void;
34
+ /** Move a prosa das linhas para o rodapé (ui/settings-panel `fillExplain`). Chamado a CADA render, e não só
35
+ * ao abrir: `render()` reconstrói as linhas, e sem esta chamada a explicação volta para dentro delas — foi
36
+ * exatamente o que aconteceu, e o menu virou manual de novo ao primeiro clique. */
37
+ fillExplain: (card: HTMLElement | null) => void;
38
+ restoreFocus?: (id: string) => boolean;
39
+ }
40
+ export interface SettingsCaaApi {
41
+ render: () => void;
42
+ open: () => void;
43
+ close: () => void;
44
+ }
45
+ export declare function initSettingsCaa(ctx: SettingsCaaCtx): SettingsCaaApi;