@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,215 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/settings-motor — MOTOR / MOVIMENTO POR JOGADOR panel (Estágio 4): extracted from game.js's
3
+ // renderMovPlayers()/reflectFacil()/reflectAltMove()/setEasy() (~line 2136). Pure logic (per-player tab
4
+ // clamp/view-model, the "any player active" predicate, the Modo Fácil announcement text) is separated from the
5
+ // thin DOM-touching render/reflect functions. DI via initSettingsMotor(ctx): `$` (DOM selector), `srSay`,
6
+ // `store` (platform/storage shape), `players`/`getNumPlayers` (core/state.ts's live state) and two setters that
7
+ // stay OUTSIDE this module because they are shared with other UI surfaces: `setToggleMove` (also fired by the
8
+ // pause-menu quick-icon `iconAct('altmove', …)`, unrelated to this panel) and `rebuildCoins` (coin subsystem,
9
+ // Modo Fácil puts coins on the ground). Overlay open/close plumbing (frontOverlay, #movement hidden toggle,
10
+ // Escape handling, renderMapHub) is the SHARED helper used by every settings panel and stays in game.js.
11
+ import { toggleLabel } from './dom.js';
12
+ import { t } from '../core/i18n.js';
13
+ import { markChanged, markMenuChanged } from './changed-mark.js';
14
+ import { DEFAULTS } from '../core/state.js';
15
+ // ---------------------------------------------------------------------------------------------
16
+ // Pure logic (no `document`, testable in node)
17
+ // ---------------------------------------------------------------------------------------------
18
+ /** localStorage key for a player's Modo Fácil flag (== platform/storage.ts's `easy_p{i}` pattern). */
19
+ export function easyKey(i) {
20
+ return 'incl_easy_p' + i;
21
+ }
22
+ /** localStorage key da alternância do botão de CORRER (== `toggleRunP` de platform/storage). */
23
+ export function toggleRunKey(i) {
24
+ return 'incl_togglerun_p' + i;
25
+ }
26
+ /** Clamps the selected player back to 0 once it falls outside 0..numPlayers-1 (e.g. player count dropped). */
27
+ export function clampSelPlayer(sel, numPlayers) {
28
+ return sel >= numPlayers ? 0 : sel;
29
+ }
30
+ /** Whether ANY player currently uses Modo Fácil or alternância — lights the #opt-movement bar button. */
31
+ export function anyMotorActive(players) {
32
+ return players.some((p) => p.easy || p.toggleMove);
33
+ }
34
+ /** '❚❚ Ligado' / '▶ Desligado' para #opt-facil e #opt-altmove. Reexporta o de ui/dom, que é o único que
35
+ * existe desde o item 14 — o corpo daqui era uma cópia, e o comentário já dizia "shared" sem sê-lo. */
36
+ export const onOffLabel = toggleLabel;
37
+ /**
38
+ * Full innerHTML for #movement-players, given the player count and the active index. Pure string building —
39
+ * no DOM. NOTE (verbatim from game.js, E3): the panel keeps this list `hidden` — a single player edits only
40
+ * their own screen (scope = pauseActor) — but the tabs/buttons are still built and wired for >1 player.
41
+ */
42
+ export function playerTabsHTML(numPlayers, selected) {
43
+ if (numPlayers <= 1)
44
+ return '';
45
+ return Array.from({ length: numPlayers }, (_, p) => `<button class="mode-btn${p === selected ? ' is-on' : ''}" data-mp="${p}" type="button">Jogador ${p + 1}</button>`).join('');
46
+ }
47
+ /**
48
+ * O 'Jogador N: ' que abre um anúncio quando há mais de uma tela. Uma tela só não leva prefixo — dizer
49
+ * "Jogador 1" para quem está sozinho é ruído, e ruído no leitor de tela custa tempo de escuta.
50
+ */
51
+ export function playerPrefix(i, numPlayers) {
52
+ return numPlayers > 1 ? t('sr.player.prefix', { n: i + 1 }) : '';
53
+ }
54
+ /** srSay text for a Modo Fácil change. A frase inteira vem do dicionário — ver `sr.motor.easyOn`. */
55
+ export function easyAnnouncement(i, numPlayers, on) {
56
+ return playerPrefix(i, numPlayers) + t(on ? 'sr.motor.easyOn' : 'sr.motor.easyOff');
57
+ }
58
+ // ---------------------------------------------------------------------------------------------
59
+ // DOM-facing (thin) — requires `document`/injected ctx
60
+ // ---------------------------------------------------------------------------------------------
61
+ export function initSettingsMotor(ctx) {
62
+ let selMovPlayer = 0; // jogador selecionado no painel Acessibilidade motora
63
+ const facilBtn = ctx.$('#opt-facil');
64
+ const altMoveBtn = ctx.$('#opt-altmove');
65
+ const toggleRunBtn = ctx.$('#opt-togglerun');
66
+ // barra acende se QUALQUER jogador usa Fácil/alternância
67
+ function reflectMovementBtn() {
68
+ const b = ctx.$('#opt-movement');
69
+ if (b)
70
+ b.classList.toggle('is-on', anyMotorActive(ctx.players));
71
+ refreshMarks();
72
+ }
73
+ /**
74
+ * A marca de "saiu do padrão" (ADR-0029). Pendurada no reflect que JÁ roda a cada mudança dos dois
75
+ * controles, porque uma marca que precise de uma chamada própria é uma marca que alguém vai esquecer —
76
+ * e uma marca errada manda a criança desfazer o que ela nunca mexeu.
77
+ *
78
+ * O escopo segue o do reset deste menu: as duas PREFERÊNCIAS. Os métodos de entrada (olhos, mapeamento)
79
+ * ficam de fora aqui também — não porque não possam mudar, mas porque o padrão deles não mora em DEFAULTS,
80
+ * e marcar sem uma fonte única de "o que é padrão" seria inventar uma segunda opinião sobre isso.
81
+ */
82
+ function refreshMarks() {
83
+ const easy = ctx.players.some((p) => !!p.easy) !== DEFAULTS.easy;
84
+ const alt = ctx.players.some((p) => !!p.toggleMove) !== DEFAULTS.toggleMove;
85
+ // A ALTERNÂNCIA DO CORRER PERGUNTA DIFERENTE, e a diferença é o que ela tem de próprio: ela LIGA SOZINHA
86
+ // no controle de tela. Marcar pelo ESTADO acenderia a marca para 100% de quem joga em tablet, sem ninguém
87
+ // ter tocado em nada — e uma marca sempre acesa não significa nada. Pior: o comentário do reset deste
88
+ // menu já diz que "uma marca errada manda a criança desfazer o que ela nunca mexeu".
89
+ //
90
+ // Então o que marca é a ESCOLHA GUARDADA. Valor salvo significa que alguém mexeu naquele controle; o
91
+ // ligar automático não salva nada, e por isso não marca.
92
+ const runEscolhido = ctx.players.some((p, i) => ctx.store.get(toggleRunKey(i)) != null && !!p.toggleRun !== DEFAULTS.toggleRun);
93
+ markChanged(facilBtn?.closest('.ctrl-row') ?? null, easy);
94
+ markChanged(altMoveBtn?.closest('.ctrl-row') ?? null, alt);
95
+ markChanged(toggleRunBtn?.closest('.ctrl-row') ?? null, runEscolhido);
96
+ markMenuChanged(ctx.$('[data-act="motora"]'), [easy, alt, runEscolhido]);
97
+ }
98
+ function reflectFacil() {
99
+ const p = ctx.players[selMovPlayer];
100
+ const on = !!(p && p.easy);
101
+ if (facilBtn) {
102
+ facilBtn.classList.toggle('is-on', on);
103
+ facilBtn.setAttribute('aria-pressed', String(on));
104
+ facilBtn.textContent = onOffLabel(on);
105
+ }
106
+ reflectMovementBtn();
107
+ }
108
+ function reflectToggleRun() {
109
+ const p = ctx.players[selMovPlayer];
110
+ const on = !!(p && p.toggleRun);
111
+ if (toggleRunBtn) {
112
+ toggleRunBtn.classList.toggle('is-on', on);
113
+ toggleRunBtn.setAttribute('aria-pressed', String(on));
114
+ toggleRunBtn.textContent = onOffLabel(on);
115
+ }
116
+ reflectMovementBtn();
117
+ }
118
+ function reflectAltMove() {
119
+ const p = ctx.players[selMovPlayer];
120
+ const on = !!(p && p.toggleMove);
121
+ if (altMoveBtn) {
122
+ altMoveBtn.classList.toggle('is-on', on);
123
+ altMoveBtn.setAttribute('aria-pressed', String(on));
124
+ altMoveBtn.textContent = onOffLabel(on);
125
+ }
126
+ reflectMovementBtn();
127
+ }
128
+ function setEasy(i, on) {
129
+ const p = ctx.players[i];
130
+ if (!p)
131
+ return;
132
+ p.easy = on;
133
+ ctx.store.setBool(easyKey(i), on);
134
+ reflectFacil();
135
+ ctx.rebuildCoins();
136
+ ctx.srSay(easyAnnouncement(i, ctx.getNumPlayers(), on));
137
+ }
138
+ function renderMovPlayers() {
139
+ const tabs = ctx.$('#movement-players');
140
+ if (!tabs)
141
+ return;
142
+ const numPlayers = ctx.getNumPlayers();
143
+ selMovPlayer = clampSelPlayer(selMovPlayer, numPlayers);
144
+ tabs.hidden = true; // E3: sem abas — cada jogador edita só o seu (escopo = pauseActor)
145
+ tabs.innerHTML = playerTabsHTML(numPlayers, selMovPlayer);
146
+ tabs.querySelectorAll('button[data-mp]').forEach((b) => {
147
+ b.addEventListener('click', () => {
148
+ selMovPlayer = Number(b.dataset.mp);
149
+ renderMovPlayers();
150
+ reflectFacil();
151
+ reflectAltMove();
152
+ });
153
+ });
154
+ }
155
+ if (facilBtn) {
156
+ facilBtn.addEventListener('click', () => setEasy(selMovPlayer, !ctx.players[selMovPlayer].easy));
157
+ }
158
+ if (altMoveBtn) {
159
+ altMoveBtn.addEventListener('click', () => {
160
+ ctx.setToggleMove(selMovPlayer, !ctx.players[selMovPlayer].toggleMove);
161
+ reflectAltMove();
162
+ });
163
+ }
164
+ if (toggleRunBtn) {
165
+ toggleRunBtn.addEventListener('click', () => {
166
+ ctx.setToggleRun(selMovPlayer, !ctx.players[selMovPlayer].toggleRun);
167
+ reflectToggleRun();
168
+ });
169
+ }
170
+ // ---- restaurar os padrões DESTE menu (ADR-0028) ----
171
+ //
172
+ // O alcance aqui é MENOR que a tela, e de propósito. A Acessibilidade motora hospeda quatro coisas: Modo
173
+ // Fácil, movimento por alternância, o controle pelos olhos (#opt-eyes) e o mapeamento de teclas (#map-hub).
174
+ // O reset devolve as duas PREFERÊNCIAS e não encosta nos dois MÉTODOS DE ENTRADA, por uma razão que vale
175
+ // mais que a simetria:
176
+ //
177
+ // UM RESET SÓ PODE DESFAZER O QUE ELE TAMBÉM CONSEGUE REFAZER.
178
+ //
179
+ // A criança que joga com os olhos aponta com os olhos. Desligar o controle pela webcam tira dela o ponteiro
180
+ // com que ela clicaria o botão de volta — o reset deixaria de remover uma armadilha para virar uma, e a
181
+ // saída passaria a depender de outra pessoa estar por perto. O mesmo vale para quem remapeou as teclas
182
+ // porque só alcança algumas: devolver o mapa de fábrica é devolver teclas que a mão dela não chega. Esse
183
+ // mapeamento, aliás, já tem o reset dele (#ctrl-reset), onde a escolha é explícita e não um efeito colateral.
184
+ //
185
+ // Por isso o anúncio DIZ o que ficou de fora: um botão que restaura menos do que o nome promete precisa
186
+ // dizer isso em voz alta, ou a criança conclui que ele não funcionou.
187
+ const resetBtn = ctx.$('#movement-reset');
188
+ if (resetBtn)
189
+ resetBtn.addEventListener('click', () => {
190
+ ctx.players.forEach((p, i) => {
191
+ if (p.easy)
192
+ setEasy(i, false);
193
+ if (p.toggleMove)
194
+ ctx.setToggleMove(i, false);
195
+ if (p.toggleRun)
196
+ ctx.setToggleRun(i, false);
197
+ });
198
+ reflectFacil();
199
+ reflectAltMove();
200
+ reflectToggleRun();
201
+ ctx.srSay(t('sr.motor.reset'));
202
+ });
203
+ reflectFacil();
204
+ reflectAltMove();
205
+ reflectToggleRun();
206
+ return {
207
+ renderMovPlayers,
208
+ reflectFacil,
209
+ reflectAltMove,
210
+ reflectToggleRun,
211
+ setEasy,
212
+ setSelPlayer: (i) => { selMovPlayer = i; },
213
+ getSelPlayer: () => selMovPlayer,
214
+ };
215
+ }
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Escopo dos overlays de a11y: o inCanvasMenus() do game.js reparenta TODOS para dentro do #game-region
3
+ * ("nenhuma tela fora do canvas"). Verbatim do sharedDialogOpen() original. */
4
+ export declare const OVERLAY_SCOPE_SELECTOR = "#game-region .overlay";
5
+ /** Texto de repouso do rodapé de explicação (verbatim do game.js). */
6
+ export declare const EXPLAIN_IDLE = "Passe o mouse ou navegue pelas op\u00E7\u00F5es para ver a explica\u00E7\u00E3o.";
7
+ /** z-index inicial da pilha: o primeiro overlay trazido à frente recebe 61. Verbatim (`let _ovZ=60`). */
8
+ export declare const OVERLAY_BASE_Z = 60;
9
+ export interface SettingsPanelCtx {
10
+ /** ui/dom.ts `$` — usado só para resolver `#<id>` e checar visibilidade na cadeia de Escape (o `dlgVis`
11
+ * do game.js). Injetado, e não importado direto, para o teste node poder passar um DOM falso. */
12
+ $: <T extends Element = Element>(sel: string) => T | null;
13
+ /** ui/dom.ts `$$` — usado só por topVisibleOverlay(), que varre OVERLAY_SCOPE_SELECTOR. */
14
+ $$: <T extends Element = Element>(sel: string) => T[];
15
+ /** Três coisas do `document`, e só elas: `createElement` (fillExplain CRIA o rodapé `.opt-explain` na
16
+ * primeira vez que vê um card), `activeElement` e `contains` (frontOverlay anota quem abriu o diálogo e
17
+ * restoreFocus devolve o foco para lá). Injetado para não amarrar o módulo ao global. */
18
+ doc: Pick<Document, 'createElement' | 'activeElement' | 'contains'>;
19
+ /** z-index EFETIVO de um elemento. No game.js é `+getComputedStyle(el).zIndex||0` (repare: 'auto' vira NaN
20
+ * e o `||0` o transforma em 0 — comportamento preservado, é responsabilidade de quem injeta). Injetado
21
+ * porque getComputedStyle só existe no navegador, e porque o teste node precisa simular a pilha. */
22
+ computedZ: (el: Element) => number;
23
+ }
24
+ /** Uma entrada do registro de overlays — substitui a tabela OVERLAY_CLOSE do game.js. */
25
+ export interface OverlayEntry {
26
+ /** Fecha o diálogo. É o MESMO valor que estava em OVERLAY_CLOSE[id] no game.js: cada painel devolve o foco
27
+ * do seu jeito lá dentro (uns a um `#opt-*`, outros ao menu de pausa) — a casca não opina. */
28
+ close: () => void;
29
+ /** Este diálogo entra na cadeia de Escape? #touchcfg e #help NÃO entram — nunca tiveram flag `*Open` no
30
+ * monólito, e a exclusão é verbatim (há conserto pendente). É obrigatório e não tem padrão de propósito:
31
+ * no monólito a exclusão era a AUSÊNCIA de um campo, o tipo de erro que ninguém comete de novo por escolha,
32
+ * só por esquecimento. Aqui quem registra tem de dizer em qual lado está.
33
+ *
34
+ * ANTES aqui havia `isOpen?: () => boolean`, espelhando sete flags `let` do game.js (`optionsOpen`,
35
+ * `audioOpen`, …) mais o `export let motionOpen` de ui/settings-motion. Todas eram escritas na linha
36
+ * colada ao `ov.hidden` do próprio open/close, e nenhuma tinha outro leitor. Como `escapeTarget` já
37
+ * descartava diálogo invisível (a guarda `dlgVis` logo abaixo), a flag era redundante COM a guarda: onde
38
+ * concordavam não mudava nada, e onde divergiam quem decidia era o `hidden`. Sete variáveis mutáveis, um
39
+ * binding mutável exportado entre módulos e um callback de ctx, todos apagados sem mudar comportamento —
40
+ * o teste que pina a divergência (flag presa em true num diálogo escondido) continua verde. */
41
+ inEscapeChain: boolean;
42
+ }
43
+ export interface SettingsPanelApi {
44
+ /** Traz o overlay para a frente da pilha (z crescente) e preenche o rodapé de explicação do seu card.
45
+ * Chamado por TODOS os open* (game.js) e pelos módulos que já têm o seu (motion/empathy/touch/gamepad). */
46
+ frontOverlay: (el: HTMLElement | null) => void;
47
+ /** Move as descrições das `.ctrl-row` do card para o rodapé `.opt-explain` (idempotente por linha). */
48
+ fillExplain: (card: HTMLElement | null) => void;
49
+ /** Registra um diálogo. A ORDEM DE REGISTRO é significativa: é ela que a cadeia de Escape percorre. */
50
+ register: (id: string, entry: OverlayEntry) => void;
51
+ /** Devolve o foco a quem abriu `id` (o par de `frontOverlay`). Falso se o abridor sumiu ou não é focável. */
52
+ restoreFocus: (id: string) => boolean;
53
+ /** OVERLAY_CLOSE[id]?.() — devolve true se havia entrada registrada (o `if(c)c()` do dialogBack). */
54
+ closeById: (id: string) => boolean;
55
+ /** Id do diálogo que deve consumir a tecla, ou null. Percorre na ORDEM DE REGISTRO (não por z-index — é o
56
+ * encadeamento if/else do game.js portado verbatim) e devolve o primeiro que esteja com a flag `*Open`
57
+ * ligada E de fato visível (`!el.hidden`). Ver o relatório: isto NÃO é "o de cima". */
58
+ escapeTarget: () => string | null;
59
+ /** O overlay VISÍVEL mais alto na pilha (sharedDialogOpen do game.js). Empate de z: vence o último no DOM. */
60
+ topVisibleOverlay: () => HTMLElement | null;
61
+ /** Ids registrados, na ordem — só para teste/depuração. */
62
+ registeredIds: () => string[];
63
+ }
64
+ /**
65
+ * A descrição de uma linha de opção, do jeito que o game.js a extrai:
66
+ * há `.opt-hint` dentro do <span>? usa o texto dele. Senão, tira do texto do <span> o prefixo do <strong>
67
+ * (o rótulo curto) e o travessão que separa rótulo de descrição.
68
+ * Aceita — (em dash), – (en dash) e - (hífen), como no original.
69
+ */
70
+ export declare function rowExplainText(spanText: string, strongText: string, hintText: string | null): string;
71
+ /**
72
+ * Escolhe o overlay visível de z-index mais alto. Empate: vence o ÚLTIMO da lista (ordem do DOM) — é o efeito
73
+ * do `sort` estável seguido de `ov[ov.length-1]` no game.js. Lista vazia → null.
74
+ */
75
+ export declare function topByZ<T>(overlays: readonly T[], zOf: (el: T) => number): T | null;
76
+ export declare function initSettingsPanel(ctx: SettingsPanelCtx): SettingsPanelApi;
@@ -0,0 +1,187 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/settings-panel.ts — a CASCA COMUM dos diálogos de configuração (#visual, #typo, #audio, #options,
3
+ // #movement, #animation, #empathy, #touchcfg, #help). Os SETE painéis já saíram para ui/settings-*.ts; o que
4
+ // sobrou no game.js em volta deles era: (a) a pilha de z-index dos overlays (`_ovZ` + frontOverlay), (b) o
5
+ // rodapé de explicação (fillExplain), (c) a tabela OVERLAY_CLOSE (id → fechar) e (d) o encadeamento de Escape.
6
+ //
7
+ // DECISÃO DE PROJETO (ver relatório da extração): NÃO existe aqui um open()/close() genérico. Os nove diálogos
8
+ // divergem em quase tudo o que a casca faria — o que renderizar antes de mostrar (de 0 a 5 chamadas distintas),
9
+ // QUAL controle recebe o foco ao abrir (`button` · `button[data-viz]||button` · `button[data-font]:not([disabled])
10
+ // ||button` · `select,button`), qual flag `*Open` refletir (sete têm, dois não), o que fazer a mais ao fechar
11
+ // (só #options cancela a captura de tecla) e PARA ONDE o foco volta (sete vão a um `#opt-*`, dois devolvem ao
12
+ // menu de pausa por menuFocus/sharedDialogOpen). Uma casca com seis parâmetros de exceção, todos eles closures
13
+ // de uma linha, custaria o mesmo código e uma indireção a mais para ler — e três dos painéis (motion, empathy,
14
+ // touch) já implementam o seu próprio open/close dentro do módulo. Então este arquivo entrega SÓ o que é
15
+ // genuinamente comum, e o open/close continua sendo sete (nove) funções curtas, cada uma legível sozinha.
16
+ //
17
+ // ACESSIBILIDADE: quem foca o quê continua nas funções de abrir/fechar de cada painel — esta casca não mexe em
18
+ // foco, em `aria-modal` (é estático no index.html) e não instala armadilha de foco (o original também não tem;
19
+ // ver o relatório). O que ELA cuida é da ORDEM DE LEITURA: fillExplain tira a descrição longa de dentro da
20
+ // linha, deixa só o rótulo em <strong>, e passa a descrição a um rodapé `aria-live="polite"` — foco/hover na
21
+ // linha atualiza esse rodapé. E frontOverlay garante que o último diálogo aberto fique por cima (z crescente),
22
+ // que é o que sharedDialogOpen/topVisibleOverlay lê de volta para saber quem navegar.
23
+ //
24
+ // INJETADO via initSettingsPanel(ctx): $/$$ (ui/dom.ts), doc (só `createElement`, para o rodapé) e computedZ
25
+ // (leitura do z-index efetivo). Nenhuma I/O no import; todo o estado (o contador `_ovZ` e o registro de
26
+ // overlays) vive no closure do init — dois inits em processos de teste distintos não vazam um no outro.
27
+ /** Escopo dos overlays de a11y: o inCanvasMenus() do game.js reparenta TODOS para dentro do #game-region
28
+ * ("nenhuma tela fora do canvas"). Verbatim do sharedDialogOpen() original. */
29
+ export const OVERLAY_SCOPE_SELECTOR = '#game-region .overlay';
30
+ /** Texto de repouso do rodapé de explicação (verbatim do game.js). */
31
+ export const EXPLAIN_IDLE = 'Passe o mouse ou navegue pelas opções para ver a explicação.';
32
+ /** z-index inicial da pilha: o primeiro overlay trazido à frente recebe 61. Verbatim (`let _ovZ=60`). */
33
+ export const OVERLAY_BASE_Z = 60;
34
+ // ---------------------------------------------------------------------------------------------------------
35
+ // Lógica PURA — sem `document`, testável no project node.
36
+ // ---------------------------------------------------------------------------------------------------------
37
+ /**
38
+ * A descrição de uma linha de opção, do jeito que o game.js a extrai:
39
+ * há `.opt-hint` dentro do <span>? usa o texto dele. Senão, tira do texto do <span> o prefixo do <strong>
40
+ * (o rótulo curto) e o travessão que separa rótulo de descrição.
41
+ * Aceita — (em dash), – (en dash) e - (hífen), como no original.
42
+ */
43
+ export function rowExplainText(spanText, strongText, hintText) {
44
+ if (hintText !== null)
45
+ return hintText.trim();
46
+ return spanText.slice(strongText.length).replace(/^\s*[—–-]\s*/, '').trim();
47
+ }
48
+ /**
49
+ * Escolhe o overlay visível de z-index mais alto. Empate: vence o ÚLTIMO da lista (ordem do DOM) — é o efeito
50
+ * do `sort` estável seguido de `ov[ov.length-1]` no game.js. Lista vazia → null.
51
+ */
52
+ export function topByZ(overlays, zOf) {
53
+ if (!overlays.length)
54
+ return null;
55
+ const sorted = [...overlays].sort((a, b) => zOf(a) - zOf(b));
56
+ return sorted[sorted.length - 1];
57
+ }
58
+ // ---------------------------------------------------------------------------------------------------------
59
+ // Casca DOM
60
+ // ---------------------------------------------------------------------------------------------------------
61
+ export function initSettingsPanel(ctx) {
62
+ // `_ovZ` do game.js. Fica no closure (e não no módulo) para que cada init comece limpo — sem isso um teste
63
+ // herdaria a pilha do anterior.
64
+ let ovZ = OVERLAY_BASE_Z;
65
+ // OVERLAY_CLOSE + as flags `*Open`, num registro só. Map preserva a ordem de inserção, que é justamente a
66
+ // ordem do encadeamento de Escape.
67
+ const registry = new Map();
68
+ function fillExplain(card) {
69
+ if (!card)
70
+ return;
71
+ let f = card.querySelector('.opt-explain');
72
+ if (!f) {
73
+ f = ctx.doc.createElement('div');
74
+ f.className = 'opt-explain';
75
+ f.setAttribute('aria-live', 'polite'); // rodapé anunciado ao mudar (foco/hover na linha)
76
+ // O texto de repouso pode ser DO PAINEL, via `data-explain-idle` no card. É onde uma introdução de menu
77
+ // deve morar: um parágrafo de prosa no topo transforma o menu num manual, e o rodapé já é o lugar da
78
+ // explicação — o painel só passa a ter algo a dizer enquanto ninguém aponta para nenhuma linha.
79
+ const idle = card.dataset.explainIdle || EXPLAIN_IDLE;
80
+ f.dataset.idle = idle;
81
+ f.textContent = idle;
82
+ card.appendChild(f);
83
+ }
84
+ const footer = f; // estreita o tipo para os closures abaixo
85
+ card.querySelectorAll('.ctrl-row').forEach((row) => {
86
+ if (row.dataset.explainDone)
87
+ return; // idempotente: render() redesenha o card, mas linha feita não repete
88
+ const span = row.querySelector(':scope > span');
89
+ const strong = span ? span.querySelector('strong') : null;
90
+ if (!span || !strong) {
91
+ row.dataset.explainDone = '1';
92
+ return;
93
+ } // linha sem rótulo curto: nada a mover
94
+ const hint = span.querySelector('.opt-hint');
95
+ const desc = rowExplainText(span.textContent ?? '', strong.textContent ?? '', hint ? (hint.textContent ?? '') : null);
96
+ row.dataset.explainDone = '1';
97
+ if (!desc)
98
+ return; // rótulo sem descrição: a linha fica como está
99
+ row.dataset.explain = desc;
100
+ span.innerHTML = strong.outerHTML; // ORDEM DE LEITURA: a linha passa a ter só o rótulo curto…
101
+ const show = () => { footer.textContent = desc; }; // …e a descrição vai ao rodapé ao focar/passar o mouse
102
+ const clear = () => { footer.textContent = footer.dataset.idle ?? ''; };
103
+ row.addEventListener('mouseenter', show);
104
+ row.addEventListener('focusin', show);
105
+ row.addEventListener('mouseleave', clear);
106
+ // NOTA (bug do original, preservado): não há listener de 'focusout' — navegando por teclado o rodapé
107
+ // nunca volta ao texto de repouso. Relatado, não corrigido.
108
+ });
109
+ }
110
+ /** Quem tinha o foco quando cada diálogo foi trazido para a frente — a chave é o id do diálogo. Usado por
111
+ * `restoreFocus`, o par de `frontOverlay`. Não é um `let` global disfarçado: nasce e morre com o registro,
112
+ * e a única coisa que o lê é a devolução do foco. */
113
+ const openerOf = new Map();
114
+ function frontOverlay(el) {
115
+ if (!el)
116
+ return;
117
+ // ANTES de o diálogo aparecer: quem está com o foco agora é quem o abriu, e é para ele que o foco volta
118
+ // (WCAG 2.4.3). O elemento de verdade, e não um id fixo: o mesmo painel é aberto do menu de pausa, de um
119
+ // atalho de teclado e de um botão da barra, e só um deles é o certo em cada vez.
120
+ // Checagem ESTRUTURAL, e não `instanceof HTMLElement`: este módulo roda no project `node`, onde não existe
121
+ // global de DOM nenhum. O que se pede do abridor é só o que se vai usar dele — saber receber foco.
122
+ const opener = ctx.doc.activeElement;
123
+ const focavel = !!opener && typeof opener.focus === 'function';
124
+ const dentro = !!opener && typeof el.contains === 'function' && el.contains(opener);
125
+ if (focavel && opener !== el && !dentro)
126
+ openerOf.set(el.id, opener);
127
+ el.style.zIndex = String(++ovZ);
128
+ const card = el.querySelector('.overlay__card');
129
+ if (card)
130
+ fillExplain(card);
131
+ }
132
+ /**
133
+ * Devolve o foco a quem abriu o diálogo `id`. Verdadeiro se conseguiu.
134
+ *
135
+ * Antes disto cada `close*` focava um `#opt-*` fixo — e SEIS desses nove ids não existem no documento
136
+ * (`#opt-visual`, `#opt-sound`, `#opt-movement`, `#opt-controls`, `#opt-animation`, `#opt-empathy`: são
137
+ * ganchos para uma barra de botões que ainda não foi feita). O `if (b) b.focus()` engolia isso em silêncio,
138
+ * então fechar o painel deixava o foco no `<body>`: quem navega por teclado voltava para o começo do
139
+ * documento, e quem usa leitor de tela perdia o lugar inteiro.
140
+ *
141
+ * Só devolve para elemento que ainda está no documento e ainda é focável — um painel pode ter sido aberto de
142
+ * dentro de outro que já fechou, e nesse caso quem decide é o chamador (daí o booleano).
143
+ */
144
+ function restoreFocus(id) {
145
+ const opener = openerOf.get(id);
146
+ openerOf.delete(id);
147
+ if (!opener)
148
+ return false;
149
+ if (typeof ctx.doc.contains === 'function' && !ctx.doc.contains(opener))
150
+ return false; // saiu do documento
151
+ if (typeof opener.hasAttribute === 'function' && opener.hasAttribute('disabled'))
152
+ return false;
153
+ if (opener.offsetParent === null)
154
+ return false; // escondido: focar nele não levaria o foco a lugar nenhum
155
+ opener.focus();
156
+ return ctx.doc.activeElement === opener;
157
+ }
158
+ function register(id, entry) {
159
+ registry.set(id, entry);
160
+ }
161
+ function closeById(id) {
162
+ const entry = registry.get(id);
163
+ if (!entry)
164
+ return false;
165
+ entry.close();
166
+ return true;
167
+ }
168
+ function escapeTarget() {
169
+ for (const [id, entry] of registry) {
170
+ if (!entry.inEscapeChain)
171
+ continue; // #touchcfg e #help ficam de fora, verbatim
172
+ const el = ctx.$('#' + id);
173
+ if (!el || el.hidden)
174
+ continue; // aberto = VISÍVEL; era `dlgVis` no monólito, e agora é a única fonte
175
+ return id;
176
+ }
177
+ return null;
178
+ }
179
+ function topVisibleOverlay() {
180
+ const visible = ctx.$$(OVERLAY_SCOPE_SELECTOR).filter((o) => !o.hidden);
181
+ return topByZ(visible, ctx.computedZ);
182
+ }
183
+ return {
184
+ frontOverlay, fillExplain, register, closeById, escapeTarget, topVisibleOverlay, restoreFocus,
185
+ registeredIds: () => [...registry.keys()],
186
+ };
187
+ }
@@ -0,0 +1,74 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { type FontItem } from './fonts.js';
3
+ /** Minimal DOM-selector shape (matches ui/dom.ts's `$`). */
4
+ export type { DomQuery } from '../core/dom-query.js';
5
+ /** Minimal platform/storage.ts shape this module needs (get/set only — no direct localStorage access). */
6
+ export interface TypoStore {
7
+ get(key: string, fallback?: string | null): string | null;
8
+ set(key: string, value: string | number | boolean): boolean;
9
+ }
10
+ export interface SettingsTypoCtx {
11
+ /** DOM selector (querySelector), injected — never reaches `document` globally. */
12
+ $: DomQuery;
13
+ /** Screen-reader announcement (core/a11y-sr's srSay), injected. */
14
+ srSay: (msg: string) => void;
15
+ /** Persistence (platform/storage.ts), injected. */
16
+ store: TypoStore;
17
+ /** Element the chosen font is applied to (dataset.fonte + --font-custom): document.documentElement in prod. */
18
+ root: HTMLElement;
19
+ /**
20
+ * Move a prosa das linhas para o rodapé (`ui/settings-panel` → `fillExplain`). Chamado a CADA `render()`, e
21
+ * não só ao abrir — que é o defeito que este parâmetro existe para fechar.
22
+ *
23
+ * `fillExplain` roda uma vez quando o overlay é frontalizado e reescreve cada `<span>` para conter só o
24
+ * rótulo curto. Mas `render()` reconstrói o `#typo-list` inteiro a cada clique numa fonte, e as linhas novas
25
+ * voltam com o `.opt-hint` dentro. Sem esta chamada a descrição aparece DUAS vezes — no rodapé, vinda da
26
+ * primeira passada, e sob o nome da fonte, vinda do redesenho. O CLAUDE.md §4 avisa exatamente isso.
27
+ *
28
+ * OPCIONAL de propósito: um consumidor que monte este painel sem a casca (o segundo consumidor, um teste)
29
+ * continua desenhando. O que ele não pode é desenhar prosa duplicada, e sem casca não há rodapé para duplicar.
30
+ */
31
+ fillExplain?: (card: HTMLElement | null) => void;
32
+ }
33
+ export interface SettingsTypoApi {
34
+ /** Re-renders #typo-list from current selection and (re)wires its buttons. Idempotent. */
35
+ render: () => void;
36
+ /** Selects+applies+persists a font by key; no-op for unknown/disabled keys. Mirrors old setGameFont(k,announce). */
37
+ setFont: (k: string, announce?: boolean) => void;
38
+ /** Currently selected font key. */
39
+ getFontKey: () => string;
40
+ }
41
+ /** A key is selectable when it exists in the catalog and is not marked `.off` (licence pending, etc.). */
42
+ export declare function isSelectableFont(k: string): boolean;
43
+ export { resolveFontKey, persistFontKey } from './fonts.js';
44
+ import type { DomQuery } from '../core/dom-query.js';
45
+ export interface FontCssTarget {
46
+ /** Value written to root.dataset.fonte. */
47
+ fonte: 'padrao' | 'alfabetizacao' | 'dislexia' | 'custom';
48
+ /** Value for the --font-custom CSS property, or null to remove the property. */
49
+ customFamily: string | null;
50
+ }
51
+ /**
52
+ * Maps a selectable font key to the CSS it drives. The three canonical EdSP fonts (atkinson/andika/lexend) use
53
+ * dedicated data-fonte values (Lexend's keeps the BDA letter/word spacing tied to data-fonte="dislexia"); every
54
+ * other catalog font goes through --font-custom with a generic fallback by family (serif/cursive/none).
55
+ * Verbatim port of the branching inside the old setGameFont().
56
+ */
57
+ export declare function fontCssTarget(k: string, it: FontItem): FontCssTarget;
58
+ export interface TypoRow {
59
+ key: string;
60
+ fam: string;
61
+ selected: boolean;
62
+ disabled: boolean;
63
+ /** Description (+ "— <off reason>" when disabled), or '' when there is none. */
64
+ note: string;
65
+ }
66
+ export interface TypoGroupView {
67
+ g: string;
68
+ rows: TypoRow[];
69
+ }
70
+ /** Pure view-model for the typography list: which row is selected/disabled and its note, per catalog group. */
71
+ export declare function typoGroups(fontKey: string): TypoGroupView[];
72
+ /** Full innerHTML for #typo-list, given the currently selected key. Pure string building — no DOM. */
73
+ export declare function typoListHTML(fontKey: string): string;
74
+ export declare function initSettingsTypo(ctx: SettingsTypoCtx): SettingsTypoApi;