@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,39 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ export type CaaTier =
3
+ /** CC BY-SA ou nosso: pode viajar dentro do jogo, funciona sem rede nenhuma. */
4
+ 'bundled'
5
+ /** A licença permite usar, não redistribuir: quem baixa é a pessoa, uma vez, e fica em cache. */
6
+ | 'fetched'
7
+ /** Falta permissão. Aparece no menu, não é escolhível. */
8
+ | 'negotiating';
9
+ export interface CaaSet {
10
+ readonly key: string;
11
+ readonly nome: string;
12
+ readonly tier: CaaTier;
13
+ /** Licença como ela foi VERIFICADA, ou null quando ainda não há. Texto curto, para caber na tela. */
14
+ readonly licenca: string | null;
15
+ /** Funciona NESTA build? Hoje só as letras — os pictogramas são arquivos que ainda não vieram. */
16
+ readonly disponivel: boolean;
17
+ /** Só para pictogramas: uma linha sobre a origem, quando ela muda o que a escolha significa. */
18
+ readonly nota?: string;
19
+ }
20
+ export declare const CAA_SETS: readonly CaaSet[];
21
+ export declare const CAA_BY_KEY: Readonly<Record<string, CaaSet>>;
22
+ /** Quais CONJUNTOS a criança pode escolher hoje: nenhum. O menu não finge o contrário — os pictogramas são
23
+ * milhares de arquivos que ainda não entraram no repositório, e quatro deles dependem de negociação. */
24
+ export declare function caaDisponiveis(): CaaSet[];
25
+ /**
26
+ * Por que este conjunto não está disponível — chave i18n, nunca texto pronto.
27
+ *
28
+ * As duas respostas dizem coisas diferentes a quem lê, e é por isso que são duas: "em preparação" quer dizer
29
+ * que a licença está resolvida e o trabalho é NOSSO; "aguardando negociação" quer dizer que a permissão é de
30
+ * OUTRA PESSOA e nenhum esforço nosso a antecipa.
31
+ *
32
+ * E é por isso que os indisponíveis aparecem em vez de serem escondidos: são justamente os conjuntos que uma
33
+ * escola brasileira tem mais chance de já usar em outro produto. O educador que procura PCS e não acha conclui
34
+ * que o jogo não faz — quando o obstáculo é uma licença, não uma capacidade. Esconder responderia a pergunta
35
+ * errada, e responderia errado.
36
+ */
37
+ export declare function caaMotivo(s: CaaSet): string | null;
38
+ /** Rótulo pronto para a linha do menu: nome + o motivo, quando há um. */
39
+ export declare function caaRotulo(s: CaaSet): string;
@@ -0,0 +1,65 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/caa-sets.ts — O CATÁLOGO DA COMUNICAÇÃO AUMENTADA E ALTERNATIVA (ADR-0028). Módulo-folha: dados puros e
3
+ // dois predicados, zero DOM, zero I/O. Quem desenha o menu é ui/settings-caa; quem decide o que ele oferece
4
+ // é este arquivo, porque a decisão é de LICENÇA e não de interface.
5
+ //
6
+ // PARA QUEM ISTO EXISTE: há crianças que leem símbolos e não letras. Para elas, o pictograma não é um enfeite
7
+ // nem uma muleta — é a escrita. Um jogo educativo que só sabe exibir letras simplesmente não fala com elas.
8
+ //
9
+ // A REGRA QUE ORGANIZA A LISTA: a camada de cada conjunto é decidida pela LICENÇA DELE e por mais nada — não
10
+ // pela qualidade, não pelo tamanho, não pela facilidade de integrar. No instante em que um conjunto mudar de
11
+ // camada por outro motivo, este arquivo terá deixado de ser verdadeiro.
12
+ //
13
+ // `tier` é o FATO JURÍDICO (podemos redistribuir? é preciso baixar? falta permissão?). `disponivel` é o FATO
14
+ // DE HOJE (funciona nesta build?). São duas perguntas diferentes e o menu precisa das duas: a primeira diz
15
+ // quem tem de agir, a segunda diz o que a criança consegue escolher agora.
16
+ import { t } from '../core/i18n.js';
17
+ // AS LETRAS NÃO ESTÃO NESTA LISTA, e a ausência é decisão do Dev. Elas eram duas entradas — "maiúsculas e
18
+ // minúsculas" e "maiúsculas somente" — e viraram UM interruptor: "Letras maiúsculas", ligado ou desligado,
19
+ // com o desligado significando as duas caixas. Uma escolha binária apresentada como duas opções faz a criança
20
+ // comparar duas linhas para descobrir que são a mesma pergunta; um interruptor pergunta uma vez.
21
+ //
22
+ // Sobra aqui o que é de verdade uma LISTA: os conjuntos de pictogramas, entre os quais se escolhe um.
23
+ export const CAA_SETS = [
24
+ // --- PICTOGRAMAS que PODEM viajar com o jogo (CC BY-SA, verificado pelo Dev em 2026-08-24). ---
25
+ { key: 'mulberry', nome: 'Mulberry Symbols', tier: 'bundled', licenca: 'CC BY-SA', disponivel: false },
26
+ { key: 'blissymbolics', nome: 'Blissymbolics', tier: 'bundled', licenca: 'CC BY-SA 4.0', disponivel: false,
27
+ nota: 'Escrita simbólica própria: os símbolos se combinam para formar sentidos novos.' },
28
+ { key: 'tawasol', nome: 'Tawasol', tier: 'bundled', licenca: 'CC BY-SA 4.0', disponivel: false,
29
+ nota: 'Desenhado na e para a cultura árabe. Um pictograma não é neutro — a criança reconhece mais depressa os objetos, as roupas e os rostos do próprio mundo.' },
30
+ // --- PICTOGRAMA que precisa ser BAIXADO. ---
31
+ { key: 'arasaac', nome: 'ARASAAC', tier: 'fetched', licenca: 'baixado à parte', disponivel: false },
32
+ // --- AGUARDANDO NEGOCIAÇÃO. Ficam visíveis de propósito: ver `caaMotivo`. ---
33
+ { key: 'sclera', nome: 'Sclera', tier: 'negotiating', licenca: null, disponivel: false },
34
+ { key: 'pcs', nome: 'PCS', tier: 'negotiating', licenca: null, disponivel: false },
35
+ { key: 'symbolstix', nome: 'SymbolStix', tier: 'negotiating', licenca: null, disponivel: false },
36
+ { key: 'widgit', nome: 'Widgit Symbols', tier: 'negotiating', licenca: null, disponivel: false },
37
+ ];
38
+ export const CAA_BY_KEY = Object.fromEntries(CAA_SETS.map((s) => [s.key, s]));
39
+ /** Quais CONJUNTOS a criança pode escolher hoje: nenhum. O menu não finge o contrário — os pictogramas são
40
+ * milhares de arquivos que ainda não entraram no repositório, e quatro deles dependem de negociação. */
41
+ export function caaDisponiveis() {
42
+ return CAA_SETS.filter((s) => s.disponivel);
43
+ }
44
+ /**
45
+ * Por que este conjunto não está disponível — chave i18n, nunca texto pronto.
46
+ *
47
+ * As duas respostas dizem coisas diferentes a quem lê, e é por isso que são duas: "em preparação" quer dizer
48
+ * que a licença está resolvida e o trabalho é NOSSO; "aguardando negociação" quer dizer que a permissão é de
49
+ * OUTRA PESSOA e nenhum esforço nosso a antecipa.
50
+ *
51
+ * E é por isso que os indisponíveis aparecem em vez de serem escondidos: são justamente os conjuntos que uma
52
+ * escola brasileira tem mais chance de já usar em outro produto. O educador que procura PCS e não acha conclui
53
+ * que o jogo não faz — quando o obstáculo é uma licença, não uma capacidade. Esconder responderia a pergunta
54
+ * errada, e responderia errado.
55
+ */
56
+ export function caaMotivo(s) {
57
+ if (s.disponivel)
58
+ return null;
59
+ return s.tier === 'negotiating' ? 'caa.aguardandoNegociacao' : 'caa.emPreparo';
60
+ }
61
+ /** Rótulo pronto para a linha do menu: nome + o motivo, quando há um. */
62
+ export function caaRotulo(s) {
63
+ const motivo = caaMotivo(s);
64
+ return motivo ? `${s.nome} — ${t(motivo)}` : s.nome;
65
+ }
@@ -0,0 +1,15 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ export declare const CHANGED_CLASS = "is-changed";
3
+ /**
4
+ * Marca (ou desmarca) UM controle. Idempotente por construção: limpa e só então aplica. Os painéis chamam
5
+ * isto de dentro do mesmo `reflect*` que já redesenha o controle, então ele roda muitas vezes seguidas.
6
+ */
7
+ export declare function markChanged(el: HTMLElement | null, changed: boolean): void;
8
+ /**
9
+ * Sobe a marca um nível: o botão que ABRE o menu fica marcado enquanto qualquer opção dentro dele estiver.
10
+ * Sem isso a trilha começaria dentro do menu, e achar o menu certo continuaria custando abrir os sete.
11
+ *
12
+ * `changes` é uma lista de booleanos e não de elementos porque quem sabe comparar com o padrão é o painel;
13
+ * este módulo não deve ter opinião sobre o que é padrão — há uma fonte só, e é o DEFAULTS do ADR-0028.
14
+ */
15
+ export declare function markMenuChanged(opener: HTMLElement | null, changes: readonly boolean[]): void;
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/changed-mark.ts — MARCA O QUE SAIU DO PADRÃO (ADR-0029). Módulo-folha: só DOM e i18n, nenhuma dep de
3
+ // estado. Quem sabe o que é padrão é cada painel, que já leu DEFAULTS (ADR-0028); aqui mora só o COMO marcar.
4
+ //
5
+ // O problema que ele resolve: o reset por menu do ADR-0028 só serve a quem sabe que há algo para desfazer.
6
+ // Sete menus, ~40 controles, e o que a criança mexeu tem exatamente a mesma aparência do que ela não mexeu.
7
+ // Os menus não estavam mal rotulados — estavam IMPESQUISÁVEIS.
8
+ //
9
+ // TRÊS CANAIS, e nenhum é enfeite. O ADR-0029 explica por quê; em resumo: a moldura branca sozinha não serve
10
+ // a quem não distingue cores, some no alto contraste (onde `--ink-soft` JÁ é branco) e não existe para quem
11
+ // não enxerga. Então:
12
+ //
13
+ // 1. COR — `--changed` na moldura (CSS; troca sozinha no alto contraste).
14
+ // 2. FORMA — anel interno, uma moldura dupla. Sobrevive a escala de cinza e a qualquer paleta, porque é uma
15
+ // CONTAGEM DE ANÉIS e não uma matiz.
16
+ // 3. NOME — o nome acessível do controle ganha um sufixo. É o canal que faz mais trabalho pelo menor preço:
17
+ // a criança cega PERCORRE o menu e OUVE, em ordem, o que saiu do padrão.
18
+ //
19
+ // O canal 3 tem dois caminhos porque os painéis constroem o nome de duas formas, e um `aria-label` vence o
20
+ // conteúdo do botão. Marcar só um deles deixaria metade dos controles mudos — em silêncio, que é o pior jeito
21
+ // de uma marca de acessibilidade falhar, porque nada na tela denuncia a falta.
22
+ //
23
+ // O sufixo NUNCA toca o `textContent`. A primeira versão pendurava um `<span class="sr-only">` dentro do
24
+ // botão, e um teste que aferia o rótulo visível caiu — mostrando o problema real: os painéis reescrevem
25
+ // `textContent` inteiro a cada reflect, então o sufixo vivia à mercê da ordem das chamadas. Agora o caminho
26
+ // sem `aria-label` CRIA um, a partir do conteúdo, e o desmarcar simplesmente o remove.
27
+ //
28
+ // E a idempotência é ESTRUTURAL, não caso a caso: toda chamada primeiro devolve o controle ao estado
29
+ // original — tirando qualquer rastro nosso, dos dois caminhos — e só então aplica. Uma marca que só soubesse
30
+ // acrescentar acabaria em tudo, e uma marca em tudo não é marca.
31
+ import { t } from '../core/i18n.js';
32
+ export const CHANGED_CLASS = 'is-changed';
33
+ /** Guarda o `aria-label` ORIGINAL quando ele é do painel, para devolvê-lo sem adivinhar por texto. */
34
+ const BASE_ATTR = 'markBase';
35
+ /** Marca que o `aria-label` foi criado por NÓS: desmarcar tira o atributo inteiro em vez de restaurar algo. */
36
+ const OWNED_ATTR = 'markOwnsLabel';
37
+ /**
38
+ * O nó cujo NOME acessível recebe o sufixo: o PRIMEIRO controle da linha em ordem de documento, ou a própria
39
+ * linha quando ela não tem controle dentro.
40
+ *
41
+ * "O primeiro" é decisão, não acaso — e ela apareceu ao verificar no jogo. Uma linha do mixer tem DOIS
42
+ * controles: o volume e o liga/desliga. O sufixo foi para o volume, que vem antes no markup, e isso está
43
+ * certo por dois motivos: é o controle que a criança alcança primeiro ao tabular, então ela ouve "alterado"
44
+ * ANTES de decidir se para nesta linha; e repetir o sufixo nos dois faria o leitor de tela dizer a mesma
45
+ * coisa duas vezes ao atravessar uma linha só, que é ruído com cara de informação.
46
+ *
47
+ * O que isto deixa frágil, e por isso está preso por teste: reordenar o markup move o sufixo de controle sem
48
+ * quebrar nada visível.
49
+ */
50
+ function namedNode(el) {
51
+ return el.querySelector('button, select, input, [role="button"]') ?? el;
52
+ }
53
+ /** Devolve o controle ao estado original, venha o rótulo do painel ou de nós. Base de toda chamada. */
54
+ function clearMark(node) {
55
+ if (node.dataset[OWNED_ATTR] !== undefined) {
56
+ node.removeAttribute('aria-label');
57
+ delete node.dataset[OWNED_ATTR];
58
+ }
59
+ else if (node.dataset[BASE_ATTR] !== undefined) {
60
+ node.setAttribute('aria-label', node.dataset[BASE_ATTR]);
61
+ delete node.dataset[BASE_ATTR];
62
+ }
63
+ }
64
+ /**
65
+ * Marca (ou desmarca) UM controle. Idempotente por construção: limpa e só então aplica. Os painéis chamam
66
+ * isto de dentro do mesmo `reflect*` que já redesenha o controle, então ele roda muitas vezes seguidas.
67
+ */
68
+ export function markChanged(el, changed) {
69
+ if (!el)
70
+ return;
71
+ el.classList.toggle(CHANGED_CLASS, changed);
72
+ const node = namedNode(el);
73
+ clearMark(node);
74
+ if (!changed)
75
+ return;
76
+ const suffix = t('a11y.changed');
77
+ const label = node.getAttribute('aria-label');
78
+ if (label !== null) {
79
+ node.dataset[BASE_ATTR] = label; // rótulo do painel: guardamos e devolvemos depois
80
+ node.setAttribute('aria-label', label + ', ' + suffix);
81
+ }
82
+ else {
83
+ node.dataset[OWNED_ATTR] = '1'; // rótulo vinha do conteúdo: criamos um e o assumimos
84
+ node.setAttribute('aria-label', (node.textContent ?? '').trim() + ', ' + suffix);
85
+ }
86
+ }
87
+ /**
88
+ * Sobe a marca um nível: o botão que ABRE o menu fica marcado enquanto qualquer opção dentro dele estiver.
89
+ * Sem isso a trilha começaria dentro do menu, e achar o menu certo continuaria custando abrir os sete.
90
+ *
91
+ * `changes` é uma lista de booleanos e não de elementos porque quem sabe comparar com o padrão é o painel;
92
+ * este módulo não deve ter opinião sobre o que é padrão — há uma fonte só, e é o DEFAULTS do ADR-0028.
93
+ */
94
+ export function markMenuChanged(opener, changes) {
95
+ markChanged(opener, changes.some(Boolean));
96
+ }
@@ -0,0 +1,67 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { JuiceFlags } from '../render/fx.js';
3
+ type Tune = Record<string, number>;
4
+ type Anim = Record<string, number>;
5
+ type Juice = JuiceFlags;
6
+ /**
7
+ * UMA FOTO DO PERSONAGEM num quadro — DADOS, e nenhum objeto do PixiJS.
8
+ *
9
+ * A raiz de composição, que é o único lugar onde o PixiJS já é conhecido, tira a foto e entrega strings. É a
10
+ * mesma escolha de `RenderizarEm` e `CriarSprite` em render/port: pedir o VERBO cabe onde emprestar o objeto
11
+ * não cabe — e aqui ela paga duas vezes, porque mantém `ui/debug-panel` testável no project `node`.
12
+ */
13
+ export interface AmostraDoPersonagem {
14
+ /** Identidade da textura DENTRO desta gravação. Não é geometria: os quatro quadros de idle têm a mesma. */
15
+ texturaId: number;
16
+ /** O recorte, "x,y LxA". */
17
+ recorte: string;
18
+ /** A base da textura, "LxA". */
19
+ base: string;
20
+ /** A posição do sprite, "x,y". */
21
+ posicao: string;
22
+ /** A escala, "x,y" — o squash & stretch mexe nela. */
23
+ escala: string;
24
+ /** Quantos IRMÃOS da câmera estavam desenhando alguma textura do personagem neste quadro. */
25
+ irmaosDesenhando: number;
26
+ /** Onde eles estavam, para o caso de haver algum. */
27
+ posIrmaos: string;
28
+ }
29
+ /** O que a sonda responde. Três perguntas, porque são elas que separam as causas que sobraram. */
30
+ export interface ResumoDaSonda {
31
+ quadros: number;
32
+ texturas: number;
33
+ maxIrmaos: number;
34
+ exemploIrmaos: string;
35
+ /** As texturas cujo recorte é grande demais para um quadro de personagem — sangramento de atlas. */
36
+ sangramento: string[];
37
+ escalas: string[];
38
+ veredito: string;
39
+ }
40
+ /**
41
+ * Reduz a gravação a três perguntas e um veredito.
42
+ *
43
+ * A ORDEM DO VEREDITO É A DECISÃO: irmão desenhando vence sangramento, porque é ele que produz cópias
44
+ * INTEIRAS em posições diferentes — que é exatamente o que foi relatado. E quando as três perguntas vêm
45
+ * limpas o veredito NÃO diz "está tudo bem": ele diz ONDE procurar em seguida. Ausência de prova nas três
46
+ * não é prova de ausência, e encerrar a busca aqui a encerraria no lugar errado.
47
+ */
48
+ export declare function resumirSonda(amostras: readonly AmostraDoPersonagem[]): ResumoDaSonda;
49
+ export interface DebugPanelCtx {
50
+ TUNE: Tune;
51
+ ANIM: Anim;
52
+ JUICE: Juice;
53
+ saveJuice: () => void;
54
+ /**
55
+ * Uma foto do personagem AGORA, ou `null` se ainda não há personagem na cena.
56
+ *
57
+ * OPCIONAL: um hospedeiro sem personagem (o quiz) não a fornece, e a sonda simplesmente não aparece. O
58
+ * painel continua sem conhecer o PixiJS.
59
+ */
60
+ amostrarPersonagem?: () => AmostraDoPersonagem | null;
61
+ /** Chama `fn` a cada quadro e devolve como cancelar. É o relógio do render, injetado como verbo. */
62
+ aoQuadro?: (fn: () => void) => () => void;
63
+ /** Override for tests; defaults to location.search. */
64
+ search?: string;
65
+ }
66
+ export declare function initDebugPanel(ctx: DebugPanelCtx): HTMLElement | null;
67
+ export {};
@@ -0,0 +1,173 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/debug-panel — the ?debug=true live-tuning panel (physics/animation sliders + juice toggles). Leaf UI module.
3
+ // Built via closure DI: receives the LIVE TUNE/ANIM/JUICE objects (mutated in place) + saveJuice from the composition
4
+ // root, so the sliders/checkboxes tune the same state the game reads. Extracted from game.js (modularization Tier 1).
5
+ // Returns the panel element (or null when not in ?debug=true) — testable without booting the game.
6
+ /** Um recorte maior que isto não é um quadro de personagem: é um pedaço do atlas. O maior real tem 31x35. */
7
+ const MAIOR_QUADRO = 64;
8
+ /**
9
+ * Reduz a gravação a três perguntas e um veredito.
10
+ *
11
+ * A ORDEM DO VEREDITO É A DECISÃO: irmão desenhando vence sangramento, porque é ele que produz cópias
12
+ * INTEIRAS em posições diferentes — que é exatamente o que foi relatado. E quando as três perguntas vêm
13
+ * limpas o veredito NÃO diz "está tudo bem": ele diz ONDE procurar em seguida. Ausência de prova nas três
14
+ * não é prova de ausência, e encerrar a busca aqui a encerraria no lugar errado.
15
+ */
16
+ export function resumirSonda(amostras) {
17
+ if (!amostras.length) {
18
+ return { quadros: 0, texturas: 0, maxIrmaos: 0, exemploIrmaos: '', sangramento: [], escalas: [], veredito: 'não gravou nada — o personagem existia?' };
19
+ }
20
+ const texturas = new Set(amostras.map((a) => a.texturaId)).size;
21
+ const comIrmaos = amostras.filter((a) => a.irmaosDesenhando > 0);
22
+ const maxIrmaos = comIrmaos.reduce((m, a) => Math.max(m, a.irmaosDesenhando), 0);
23
+ const grande = (r) => {
24
+ const m = /(\d+)x(\d+)$/.exec(r);
25
+ return !!m && (+m[1] > MAIOR_QUADRO || +m[2] > MAIOR_QUADRO);
26
+ };
27
+ const sangramento = [...new Set(amostras.filter((a) => grande(a.recorte)).map((a) => a.recorte + ' (base ' + a.base + ')'))];
28
+ const escalas = [...new Set(amostras.map((a) => a.escala))].sort();
29
+ const veredito = maxIrmaos > 0
30
+ ? 'ALGUÉM DESENHA DUAS VEZES: até ' + maxIrmaos + ' irmão(s) da câmera com a textura do personagem'
31
+ : sangramento.length
32
+ ? 'RECORTE GRANDE DEMAIS: o quadro está pegando pedaço do atlas'
33
+ : 'sprite limpo (uma textura por quadro, sem irmão, recorte de quadro) — procure em composição: filtro, pós-efeito ou câmera';
34
+ return { quadros: amostras.length, texturas, maxIrmaos, exemploIrmaos: comIrmaos.length ? comIrmaos[0].posIrmaos : '', sangramento, escalas, veredito };
35
+ }
36
+ export function initDebugPanel(ctx) {
37
+ const search = ctx.search ?? location.search;
38
+ if (!/[?&]debug=true/.test(search))
39
+ return null;
40
+ const { TUNE, ANIM, JUICE, saveJuice } = ctx;
41
+ const KNOBS = [
42
+ { h: 'Movimento (valores absolutos)' },
43
+ { label: 'Velocidade de andar', get: () => TUNE.hWalk, set: (v) => (TUNE.hWalk = v), min: 0.5, max: 5, step: 0.1 },
44
+ { label: 'Velocidade de correr', get: () => TUNE.hRun, set: (v) => (TUNE.hRun = v), min: 1, max: 7, step: 0.1 },
45
+ { label: 'Super-corrida (turbo)', get: () => TUNE.hTurbo, set: (v) => (TUNE.hTurbo = v), min: 1, max: 9, step: 0.1 },
46
+ { label: 'Pulo (impulso inicial)', get: () => TUNE.jumpVel, set: (v) => (TUNE.jumpVel = v), min: 1, max: 8, step: 0.1 },
47
+ { label: 'Ultra-pulo', get: () => TUNE.ultraJumpVel, set: (v) => (TUNE.ultraJumpVel = v), min: 3, max: 16, step: 0.1 },
48
+ { label: 'Trampolim (base)', get: () => TUNE.trampBase, set: (v) => (TUNE.trampBase = v), min: 2, max: 10, step: 0.1 },
49
+ { label: 'Trampolim (máximo)', get: () => TUNE.trampMax, set: (v) => (TUNE.trampMax = v), min: 3, max: 14, step: 0.1 },
50
+ { label: 'Nado: impulso', get: () => TUNE.waterJump, set: (v) => (TUNE.waterJump = v), min: 1, max: 7, step: 0.1 },
51
+ { label: 'Nado: impulso correndo', get: () => TUNE.waterJumpRun, set: (v) => (TUNE.waterJumpRun = v), min: 1, max: 8, step: 0.1 },
52
+ { label: 'Nado: quadros/braçada', get: () => TUNE.waterStrokeFrames, set: (v) => (TUNE.waterStrokeFrames = v), min: 10, max: 60, step: 1 },
53
+ { label: 'Escalada (velocidade)', get: () => TUNE.climbSpeed, set: (v) => (TUNE.climbSpeed = v), min: 0.5, max: 4, step: 0.1 },
54
+ { label: 'Gravidade', get: () => TUNE.gravity, set: (v) => (TUNE.gravity = v), min: 0.05, max: 0.4, step: 0.01 },
55
+ { label: 'Queda máxima', get: () => TUNE.maxFall, set: (v) => (TUNE.maxFall = v), min: 3, max: 14, step: 0.5 },
56
+ { label: 'Queda máxima na água', get: () => TUNE.waterMaxFall, set: (v) => (TUNE.waterMaxFall = v), min: 1, max: 8, step: 0.5 },
57
+ { h: 'Animação (cadência: ticks/quadro)' },
58
+ { label: 'Andar', get: () => ANIM.walkHold, set: (v) => (ANIM.walkHold = v), min: 1, max: 20, step: 1, cad: true },
59
+ { label: 'Correr', get: () => ANIM.runHold, set: (v) => (ANIM.runHold = v), min: 1, max: 20, step: 1, cad: true },
60
+ { label: 'Parado (idle)', get: () => ANIM.idleHold, set: (v) => (ANIM.idleHold = v), min: 2, max: 40, step: 1, cad: true },
61
+ { label: 'Nado', get: () => ANIM.swimHold, set: (v) => (ANIM.swimHold = v), min: 2, max: 24, step: 1, cad: true },
62
+ { h: 'Juice (efeitos de resposta) — toggles independentes' },
63
+ { label: '💨 Poeira (pulo/pouso/corrida)', chk: () => JUICE.dust, set: (v) => { JUICE.dust = v; saveJuice(); } },
64
+ { label: '✨ Brilho ao coletar', chk: () => JUICE.sparkle, set: (v) => { JUICE.sparkle = v; saveJuice(); } },
65
+ { label: '🤸 Squash & stretch', chk: () => JUICE.squash, set: (v) => { JUICE.squash = v; saveJuice(); } },
66
+ { label: '⏱️ Hit-stop (impacto)', chk: () => JUICE.hitstop, set: (v) => { JUICE.hitstop = v; saveJuice(); } },
67
+ { label: '📳 Tremor de tela', chk: () => JUICE.shake, set: (v) => { JUICE.shake = v; saveJuice(); } },
68
+ { label: '🌟 Cintilar dos itens', chk: () => JUICE.shimmer, set: (v) => { JUICE.shimmer = v; saveJuice(); } },
69
+ ]; // Estética CRT saiu daqui: mora no menu Sensibilidade visual (pedido do José).
70
+ const p = document.createElement('div');
71
+ p.id = 'debug-panel';
72
+ p.hidden = true; // começa oculto; abre pelo botão 🐞 Debug
73
+ p.setAttribute('role', 'group');
74
+ p.setAttribute('aria-label', 'Painel de depuração');
75
+ p.style.cssText = 'position:fixed;top:8px;right:8px;z-index:200;background:rgba(11,16,32,.97);color:#fff;border:2px solid #ffd23f;border-radius:8px;padding:.6rem .7rem;font:13px/1.4 system-ui,sans-serif;max-width:270px;max-height:86vh;overflow:auto;box-shadow:0 4px 16px rgba(0,0,0,.5)';
76
+ p.innerHTML = '<strong>🔧 ?debug — valores ao vivo</strong>';
77
+ for (const k of KNOBS) {
78
+ if ('h' in k) {
79
+ const h = document.createElement('div');
80
+ h.textContent = k.h;
81
+ h.style.cssText = 'margin:.7rem 0 .1rem;font-weight:700;color:#ffd23f;border-bottom:1px solid rgba(255,210,63,.4)';
82
+ p.appendChild(h);
83
+ continue;
84
+ }
85
+ if ('chk' in k) {
86
+ const row = document.createElement('label');
87
+ row.style.cssText = 'display:flex;gap:.4rem;align-items:center;margin-top:.4rem;font-size:12px;cursor:pointer';
88
+ const inp = document.createElement('input');
89
+ inp.type = 'checkbox';
90
+ inp.checked = k.chk();
91
+ inp.addEventListener('change', () => k.set(inp.checked));
92
+ row.appendChild(inp);
93
+ row.appendChild(document.createTextNode(k.label));
94
+ p.appendChild(row);
95
+ continue;
96
+ }
97
+ const row = document.createElement('div');
98
+ row.style.cssText = 'margin-top:.5rem';
99
+ const lab = document.createElement('label');
100
+ lab.style.cssText = 'display:block;font-size:12px;margin-bottom:2px';
101
+ const val = document.createElement('strong');
102
+ val.style.cssText = 'color:#ffd23f;float:right';
103
+ const upd = () => { val.textContent = k.cad ? `${k.get()} (${Math.round(60 / k.get())}fps)` : String(k.get()); };
104
+ lab.textContent = k.label;
105
+ lab.appendChild(val);
106
+ const inp = document.createElement('input');
107
+ inp.type = 'range';
108
+ inp.min = String(k.min);
109
+ inp.max = String(k.max);
110
+ inp.step = String(k.step);
111
+ inp.value = String(k.get());
112
+ inp.style.cssText = 'width:100%';
113
+ inp.setAttribute('aria-label', k.label);
114
+ inp.addEventListener('input', () => { k.set(parseFloat(inp.value)); upd(); });
115
+ row.appendChild(lab);
116
+ row.appendChild(inp);
117
+ p.appendChild(row);
118
+ upd();
119
+ }
120
+ /* ===================== A SONDA DO PERSONAGEM =====================
121
+ Ela mora AQUI, e não num script para colar no console, porque foi o que o Dev pediu — e ele tem razão
122
+ pelo motivo de sempre: um instrumento que só existe enquanto alguém lembra de colar não é instrumento, é
123
+ lembrança. Aqui ele fica ao lado dos outros valores ao vivo, atrás do mesmo `?debug=true`.
124
+
125
+ Só aparece se o hospedeiro souber tirar a foto: um jogo sem personagem não ganha um botão que não faz
126
+ nada. */
127
+ if (ctx.amostrarPersonagem && ctx.aoQuadro) {
128
+ const amostrar = ctx.amostrarPersonagem, aoQuadro = ctx.aoQuadro;
129
+ const h = document.createElement('div');
130
+ h.textContent = 'Sonda do personagem';
131
+ h.style.cssText = 'margin:.7rem 0 .1rem;font-weight:700;color:#ffd23f;border-bottom:1px solid rgba(255,210,63,.4)';
132
+ p.appendChild(h);
133
+ const saida = document.createElement('pre');
134
+ saida.style.cssText = 'margin:.4rem 0 0;font:11px/1.35 ui-monospace,monospace;white-space:pre-wrap;color:#cfe';
135
+ saida.setAttribute('aria-live', 'polite');
136
+ const btn = document.createElement('button');
137
+ btn.type = 'button';
138
+ btn.textContent = '🥷 Gravar 180 quadros';
139
+ btn.style.cssText = 'margin-top:.4rem;width:100%;min-height:32px;font:inherit;font-weight:700;cursor:pointer;border-radius:6px;border:1px solid #ffd23f;background:#1a2740;color:#fff';
140
+ btn.addEventListener('click', () => {
141
+ if (btn.disabled)
142
+ return;
143
+ btn.disabled = true;
144
+ const amostras = [];
145
+ let n = 0;
146
+ saida.textContent = 'gravando… PULE agora';
147
+ const parar = aoQuadro(() => {
148
+ const a = amostrar();
149
+ if (a)
150
+ amostras.push(a);
151
+ if (++n < 180)
152
+ return;
153
+ parar();
154
+ btn.disabled = false;
155
+ const r = resumirSonda(amostras);
156
+ saida.textContent = [
157
+ 'quadros: ' + r.quadros + ' texturas: ' + r.texturas,
158
+ 'irmãos desenhando: ' + r.maxIrmaos + (r.exemploIrmaos ? ' em ' + r.exemploIrmaos : ''),
159
+ 'recorte grande: ' + (r.sangramento.length ? r.sangramento.join(' ') : 'nenhum'),
160
+ 'escalas: ' + r.escalas.join(' '),
161
+ '',
162
+ '→ ' + r.veredito,
163
+ ].join(String.fromCharCode(10));
164
+ // O BRUTO fica alcançável para quem quiser ir além do resumo — sem poluir o painel com 180 linhas.
165
+ window.__sonda = amostras;
166
+ });
167
+ });
168
+ p.appendChild(btn);
169
+ p.appendChild(saida);
170
+ }
171
+ document.body.appendChild(p);
172
+ return p;
173
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ export declare const $: <T extends Element = HTMLElement>(s: string) => T | null;
3
+ export declare const $$: <T extends Element = HTMLElement>(s: string) => T[];
4
+ /**
5
+ * Reflects an on/off state onto a toggle button: the visual class AND `aria-pressed`.
6
+ * Lives here because the two must never drift apart — a button that looks pressed but does not
7
+ * say so is invisible to a screen reader, which is a pillar violation, not a style slip.
8
+ */
9
+ export declare function toggleBtn(b: Element | null, on: boolean): void;
10
+ /**
11
+ * O TEXTO do botão de alternância: '❚❚ Ligado' / '▶ Desligado'.
12
+ *
13
+ * Mora aqui pelo mesmo motivo do `toggleBtn` logo acima — os dois são as duas metades do mesmo gesto, e a
14
+ * nota daquela função diz que elas "não podem se separar". Elas tinham se separado em TREZE cópias, em nove
15
+ * arquivos, e dois deles declaravam um helper que se chamava compartilhado e servia um chamador só
16
+ * (`settings-empathy.toggleLabel` e `settings-motor.onOffLabel`, ambos apagados agora).
17
+ *
18
+ * O custo dessa dispersão não era estético: com o texto em treze lugares, traduzir o jogo exigiria achar os
19
+ * treze, e esquecer um deixaria um botão em português no meio do inglês — sem erro, sem teste vermelho.
20
+ */
21
+ export declare function toggleLabel(on: boolean): string;
22
+ /** O `aria-label` da mesma alternância: '{alvo}: ligado'. Separado do rótulo visível porque o leitor de tela
23
+ * precisa do NOME do alvo junto, e a tela não (o nome já está na linha ao lado do botão). */
24
+ export declare function toggleAria(alvo: string, on: boolean): string;
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/dom.ts — atalhos de seleção do DOM, usados em toda a UI (menus/HUD/pausa/quiz/opções). PURO no import:
3
+ // só define as funções (não toca no DOM → importável em node). $ = querySelector; $$ = querySelectorAll
4
+ // como Array. Genéricos: $<HTMLInputElement>('#x') já tipa o retorno. (Fase 2.27 / Tier 1)
5
+ //
6
+ // DEIXOU DE SER FOLHA no item 14, e de propósito: passou a importar `core/i18n` para que `toggleLabel` (o
7
+ // texto do botão de alternância) more ao lado do `toggleBtn` (a classe e o `aria-pressed`). A propriedade que
8
+ // importa — importável em node, sem I/O no import — continua valendo; o que se perdeu foi a contagem de zero
9
+ // dependências, e o que se ganhou está escrito no comentário de `toggleLabel`.
10
+ import { t } from '../core/i18n.js';
11
+ // O PADRAO E `HTMLElement`, NAO `Element`, e a diferenca aparece em dezessete pontos de chamada: `.hidden`,
12
+ // `.focus()` e `.value` nao existem em `Element`, e sao exatamente o que uma aplicacao faz com o que
13
+ // seleciona. `Element` e o padrao do lib.dom porque `querySelector` tambem serve para SVG e MathML —
14
+ // nenhum seletor deste projeto pega um desses (conferido). Quem precisar de um tipo mais estreito passa
15
+ // o parametro: `$<HTMLSelectElement>('#pad-design')`.
16
+ export const $ = (s) => document.querySelector(s);
17
+ export const $$ = (s) => [...document.querySelectorAll(s)];
18
+ /**
19
+ * Reflects an on/off state onto a toggle button: the visual class AND `aria-pressed`.
20
+ * Lives here because the two must never drift apart — a button that looks pressed but does not
21
+ * say so is invisible to a screen reader, which is a pillar violation, not a style slip.
22
+ */
23
+ export function toggleBtn(b, on) {
24
+ if (!b)
25
+ return;
26
+ b.classList.toggle('is-on', on);
27
+ b.setAttribute('aria-pressed', String(on));
28
+ }
29
+ /**
30
+ * O TEXTO do botão de alternância: '❚❚ Ligado' / '▶ Desligado'.
31
+ *
32
+ * Mora aqui pelo mesmo motivo do `toggleBtn` logo acima — os dois são as duas metades do mesmo gesto, e a
33
+ * nota daquela função diz que elas "não podem se separar". Elas tinham se separado em TREZE cópias, em nove
34
+ * arquivos, e dois deles declaravam um helper que se chamava compartilhado e servia um chamador só
35
+ * (`settings-empathy.toggleLabel` e `settings-motor.onOffLabel`, ambos apagados agora).
36
+ *
37
+ * O custo dessa dispersão não era estético: com o texto em treze lugares, traduzir o jogo exigiria achar os
38
+ * treze, e esquecer um deixaria um botão em português no meio do inglês — sem erro, sem teste vermelho.
39
+ */
40
+ export function toggleLabel(on) {
41
+ return t(on ? 'ui.toggle.on' : 'ui.toggle.off');
42
+ }
43
+ /** O `aria-label` da mesma alternância: '{alvo}: ligado'. Separado do rótulo visível porque o leitor de tela
44
+ * precisa do NOME do alvo junto, e a tela não (o nome já está na linha ao lado do botão). */
45
+ export function toggleAria(alvo, on) {
46
+ return t(on ? 'ui.toggle.ariaOn' : 'ui.toggle.ariaOff', { alvo });
47
+ }
@@ -0,0 +1,71 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * O que conta como focável. Deliberadamente SEM `[contenteditable]` e sem `tabindex` positivo: os dois
4
+ * existem, mas não aparecem em nenhum diálogo desta engine (conferido), e um seletor que promete mais do que
5
+ * foi verificado é um seletor que mente na próxima revisão.
6
+ */
7
+ export declare const SELETOR_FOCAVEL: string;
8
+ /**
9
+ * Para onde o foco tem de ir quando o Tab está prestes a sair do diálogo — ou `null` quando não há nada a
10
+ * fazer e o navegador deve seguir sozinho.
11
+ *
12
+ * Os quatro casos, e o terceiro é a razão de isto existir:
13
+ *
14
+ * 1. lista VAZIA → `null`. Um diálogo sem nada focável é um defeito do diálogo, e prender o foco nele
15
+ * deixaria a criança sem saída nenhuma. Melhor deixar sair do que trancar num lugar mudo.
16
+ * 2. o foco está no ÚLTIMO e vai para a frente → volta ao primeiro; no PRIMEIRO e vai para trás → vai ao
17
+ * último. É o ciclo, que é o que «armadilha» quer dizer.
18
+ * 3. ⚠️ o foco está FORA da lista → traz de volta. É o caso que os outros três não cobrem: o foco pode já
19
+ * ter escapado antes de esta armadilha existir, ou por um clique no tabuleiro, ou porque o diálogo
20
+ * abriu sem focar nada. Sem ele a armadilha só funciona para quem já estava dentro.
21
+ * 4. o foco está no MEIO → `null`, e o navegador caminha. Ver o comentário do cabeçalho.
22
+ */
23
+ export declare function proximoNaArmadilha<T>(focaveis: readonly T[], atual: T | null, paraTras: boolean): T | null;
24
+ interface AlvoDeEvento {
25
+ addEventListener: (tipo: 'keydown', fn: (e: KeyboardEvent) => void, captura?: boolean) => void;
26
+ removeEventListener?: (tipo: 'keydown', fn: (e: KeyboardEvent) => void, captura?: boolean) => void;
27
+ }
28
+ export interface FocusTrapCtx {
29
+ /** O diálogo VISÍVEL mais alto da pilha, ou `null`. É o `topVisibleOverlay` de `ui/settings-panel`. */
30
+ overlayDeCima: () => HTMLElement | null;
31
+ /** Quem tem o foco agora — `() => document.activeElement`. Injetado: o project node não tem documento. */
32
+ focoAtual: () => Element | null;
33
+ /** Os focáveis DENTRO do diálogo, na ordem do documento e já filtrados por visibilidade. */
34
+ focaveisDe: (dentro: HTMLElement) => HTMLElement[];
35
+ win: AlvoDeEvento;
36
+ }
37
+ export interface FocusTrapApi {
38
+ /** O manipulador, exposto para o teste o chamar sem instalar nada. */
39
+ aoTeclar: (e: KeyboardEvent) => void;
40
+ /** Instala na fase de CAPTURA, pelo mesmo motivo do `menu-nav`: ver a tecla antes de quem está por baixo. */
41
+ attach: () => void;
42
+ /**
43
+ * Desinstala.
44
+ *
45
+ * ⚠️ ELE NASCEU DE UM TESTE QUE REPROVOU, e não de simetria: dois casos no mesmo ficheiro instalaram duas
46
+ * armadilhas na mesma janela, e a primeira continuou a prender o foco durante o segundo — o caso «sem
47
+ * diálogo aberto o Tab é do jogo» ficou vermelho por causa de uma armadilha que já devia ter saído.
48
+ *
49
+ * O que isso mostra vale fora do teste: uma armadilha que não se desinstala é um vazamento para qualquer
50
+ * página que monte a engine duas vezes — o `demos`, que troca de jogo sem recarregar, é exatamente isso.
51
+ */
52
+ detach: () => void;
53
+ }
54
+ export declare function initFocusTrap(ctx: FocusTrapCtx): FocusTrapApi;
55
+ /**
56
+ * Os focáveis de um contêiner, no navegador. Vive aqui e não na raiz para as duas raízes não escreverem duas
57
+ * versões que divergiriam em silêncio — uma prenderia o foco e a outra deixaria escapar, no mesmo produto.
58
+ *
59
+ * ⚠️ A VISIBILIDADE É POR `getClientRects()`, e não por `offsetParent`.
60
+ *
61
+ * ⚠️ E A PRIMEIRA VERSÃO DESTE COMENTÁRIO ESTAVA ERRADA, o que vale mais registrar do que apagar: ele dizia
62
+ * que `offsetParent` é `null` para «qualquer elemento em `position: fixed`». Não é — é `null` para o elemento
63
+ * FIXO EM SI, e os descendentes dele devolvem o próprio contêiner. O teste de navegador reprovou a afirmação,
64
+ * que é exatamente o que um teste tem de fazer com uma justificativa inventada.
65
+ *
66
+ * A razão verdadeira é mais simples e mais forte: `getClientRects()` responde *«isto desenha alguma caixa?»*,
67
+ * o que cobre `display:none` em QUALQUER ancestral, elementos de tamanho zero e o caso do contêiner fixo —
68
+ * sem que quem lê precise de saber em que casos `offsetParent` tem buracos.
69
+ */
70
+ export declare function focaveisNoDom(dentro: HTMLElement): HTMLElement[];
71
+ export {};