@the-inclusionist/engine 7.0.1 → 8.0.0-rc.2

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 (136) hide show
  1. package/README.md +5 -3
  2. package/app/css/style.css +22 -5
  3. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  4. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  5. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  6. package/app/public/vendor/fonts/playwrite-ar.woff2 +0 -0
  7. package/app/public/vendor/fonts/playwrite-br.woff2 +0 -0
  8. package/app/public/vendor/fonts/playwrite-ca.woff2 +0 -0
  9. package/app/public/vendor/fonts/playwrite-cl.woff2 +0 -0
  10. package/app/public/vendor/fonts/playwrite-co.woff2 +0 -0
  11. package/app/public/vendor/fonts/playwrite-mx.woff2 +0 -0
  12. package/app/public/vendor/fonts/playwrite-us-modern.woff2 +0 -0
  13. package/app/public/vendor/fonts/playwrite-us-trad.woff2 +0 -0
  14. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  15. package/app/public/vendor/fonts.css +64 -2
  16. package/dist-pkg/boot/create-game.d.ts +107 -4
  17. package/dist-pkg/boot/create-game.js +337 -18
  18. package/dist-pkg/core/constants.d.ts +0 -28
  19. package/dist-pkg/core/constants.js +34 -17
  20. package/dist-pkg/core/contract.d.ts +99 -0
  21. package/dist-pkg/core/contract.js +78 -0
  22. package/dist-pkg/core/entity.d.ts +47 -4
  23. package/dist-pkg/core/layers.d.ts +18 -0
  24. package/dist-pkg/core/layers.js +18 -0
  25. package/dist-pkg/core/rng.js +14 -4
  26. package/dist-pkg/core/route.d.ts +42 -0
  27. package/dist-pkg/core/route.js +158 -0
  28. package/dist-pkg/core/state.d.ts +10 -1
  29. package/dist-pkg/core/state.js +13 -0
  30. package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
  31. package/dist-pkg/educational/adaptive-engine.js +117 -0
  32. package/dist-pkg/educational/segment-bar.d.ts +96 -0
  33. package/dist-pkg/educational/segment-bar.js +89 -0
  34. package/dist-pkg/i18n/en.js +44 -0
  35. package/dist-pkg/i18n/es.js +44 -0
  36. package/dist-pkg/i18n/pt.js +60 -0
  37. package/dist-pkg/input/default-bindings.d.ts +40 -0
  38. package/dist-pkg/input/default-bindings.js +151 -9
  39. package/dist-pkg/input/gamepad.d.ts +33 -12
  40. package/dist-pkg/input/gamepad.js +103 -16
  41. package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
  42. package/dist-pkg/input/keyboard-runtime.js +24 -5
  43. package/dist-pkg/input/keyboard.d.ts +42 -0
  44. package/dist-pkg/input/keyboard.js +87 -17
  45. package/dist-pkg/input/keydown.d.ts +40 -2
  46. package/dist-pkg/input/keydown.js +29 -6
  47. package/dist-pkg/input/latch-edge.d.ts +28 -0
  48. package/dist-pkg/input/latch-edge.js +45 -0
  49. package/dist-pkg/input/latch-scope.d.ts +70 -0
  50. package/dist-pkg/input/latch-scope.js +110 -0
  51. package/dist-pkg/input/latch-store.d.ts +45 -0
  52. package/dist-pkg/input/latch-store.js +74 -0
  53. package/dist-pkg/input/latch-sync.d.ts +47 -0
  54. package/dist-pkg/input/latch-sync.js +43 -0
  55. package/dist-pkg/input/origem-sintetica.d.ts +44 -0
  56. package/dist-pkg/input/origem-sintetica.js +60 -0
  57. package/dist-pkg/input/pad-defaults.d.ts +22 -0
  58. package/dist-pkg/input/pad-defaults.js +34 -0
  59. package/dist-pkg/input/pointer.d.ts +61 -0
  60. package/dist-pkg/input/pointer.js +71 -0
  61. package/dist-pkg/input/state.d.ts +86 -1
  62. package/dist-pkg/input/state.js +128 -1
  63. package/dist-pkg/input/touch-bindings.d.ts +21 -2
  64. package/dist-pkg/input/touch-bindings.js +15 -3
  65. package/dist-pkg/input/touch.js +9 -1
  66. package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
  67. package/dist-pkg/input/transporte-em-uso.js +130 -0
  68. package/dist-pkg/input/transports.d.ts +88 -2
  69. package/dist-pkg/input/transports.js +60 -5
  70. package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
  71. package/dist-pkg/platform/audio-earcons.d.ts +29 -2
  72. package/dist-pkg/platform/audio-earcons.js +10 -0
  73. package/dist-pkg/platform/audio-nav.d.ts +1 -1
  74. package/dist-pkg/platform/audio-sonar.d.ts +149 -9
  75. package/dist-pkg/platform/audio-sonar.js +232 -21
  76. package/dist-pkg/platform/guide-intensity.d.ts +40 -0
  77. package/dist-pkg/platform/guide-intensity.js +73 -0
  78. package/dist-pkg/platform/pesados-catalogo.d.ts +14 -0
  79. package/dist-pkg/platform/pesados-catalogo.js +177 -0
  80. package/dist-pkg/platform/pesados.d.ts +31 -0
  81. package/dist-pkg/platform/pesados.js +75 -0
  82. package/dist-pkg/platform/storage.d.ts +30 -0
  83. package/dist-pkg/platform/storage.js +35 -0
  84. package/dist-pkg/platform/tts.js +16 -1
  85. package/dist-pkg/platform/voice-plan.d.ts +109 -0
  86. package/dist-pkg/platform/voice-plan.js +156 -0
  87. package/dist-pkg/platform/vozes-prontas.d.ts +28 -0
  88. package/dist-pkg/platform/vozes-prontas.js +61 -0
  89. package/dist-pkg/render/draw.d.ts +1 -1
  90. package/dist-pkg/render/draw.js +15 -5
  91. package/dist-pkg/render/viz-axes.d.ts +17 -0
  92. package/dist-pkg/render/viz-axes.js +19 -0
  93. package/dist-pkg/render/viz-setters.d.ts +34 -2
  94. package/dist-pkg/render/viz-setters.js +189 -33
  95. package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
  96. package/dist-pkg/render/wheelchair-sprites.js +10 -4
  97. package/dist-pkg/ui/dom.js +20 -2
  98. package/dist-pkg/ui/fonts.d.ts +71 -1
  99. package/dist-pkg/ui/fonts.js +111 -8
  100. package/dist-pkg/ui/latch-refusal.d.ts +32 -0
  101. package/dist-pkg/ui/latch-refusal.js +60 -0
  102. package/dist-pkg/ui/layout.d.ts +32 -0
  103. package/dist-pkg/ui/layout.js +64 -1
  104. package/dist-pkg/ui/motion-scene.d.ts +41 -0
  105. package/dist-pkg/ui/motion-scene.js +76 -0
  106. package/dist-pkg/ui/panel-shell.d.ts +53 -0
  107. package/dist-pkg/ui/panel-shell.js +103 -0
  108. package/dist-pkg/ui/pause-icons.d.ts +168 -20
  109. package/dist-pkg/ui/pause-icons.js +315 -45
  110. package/dist-pkg/ui/reach-notice.js +8 -0
  111. package/dist-pkg/ui/settings-audio.d.ts +14 -1
  112. package/dist-pkg/ui/settings-audio.js +43 -3
  113. package/dist-pkg/ui/settings-controls.d.ts +66 -6
  114. package/dist-pkg/ui/settings-controls.js +179 -17
  115. package/dist-pkg/ui/settings-empathy.d.ts +15 -0
  116. package/dist-pkg/ui/settings-empathy.js +2 -0
  117. package/dist-pkg/ui/settings-motion.d.ts +25 -19
  118. package/dist-pkg/ui/settings-motion.js +32 -19
  119. package/dist-pkg/ui/settings-motor.d.ts +101 -3
  120. package/dist-pkg/ui/settings-motor.js +136 -9
  121. package/dist-pkg/ui/settings-typo.d.ts +49 -5
  122. package/dist-pkg/ui/settings-typo.js +74 -23
  123. package/dist-pkg/ui/settings-visual.d.ts +21 -1
  124. package/dist-pkg/ui/settings-visual.js +48 -4
  125. package/dist-pkg/ui/shell.d.ts +13 -3
  126. package/dist-pkg/ui/shell.js +3 -4
  127. package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
  128. package/dist-pkg/ui/simulation-refusal.js +57 -0
  129. package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
  130. package/dist-pkg/ui/visual-axes-panel.js +95 -0
  131. package/dist-pkg/ui/webcam.js +6 -1
  132. package/docs/CREDITS.md +22 -1
  133. package/docs/LICENSES.md +170 -132
  134. package/package.json +25 -4
  135. package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
  136. package/app/public/vendor/fonts/ufcook-700.woff2 +0 -0
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** Uma linha pronta a traduzir. `null` = há escolha, e não há nada a dizer. */
3
+ export interface RecusaDaAlternancia {
4
+ readonly chave: string;
5
+ /** O transporte que a exige — fica disponível para quem quiser compor a frase de outro modo. */
6
+ readonly transporte: string;
7
+ }
8
+ /**
9
+ * A chave i18n do motivo, POR TRANSPORTE.
10
+ *
11
+ * ⚠️ QUATRO CHAVES E NÃO UMA COM `{aparelho}`, e a razão é de tradução e não de estilo: «os gestos» é plural
12
+ * e «o olhar» não, logo uma frase única obrigaria cada idioma a montar concordância a partir de um
13
+ * substantivo solto. É exactamente o defeito que o `sr.nav.clockOne` registou para «às 1 horas», e que o
14
+ * `ui/simulation-refusal` já recusou pela mesma razão com os três eixos dele.
15
+ */
16
+ export declare const CHAVE_DA_RECUSA: Readonly<Record<string, string>>;
17
+ /**
18
+ * A alternância pode ser desligada neste aparelho? E se não, o que dizer.
19
+ *
20
+ * `null` = pode; a interface não mostra nada, porque um aviso que aparece sempre deixa de ser lido.
21
+ */
22
+ export declare function recusaDaAlternancia(transporte: string): RecusaDaAlternancia | null;
23
+ /**
24
+ * O controle continua NA TELA quando a alternância é exigida?
25
+ *
26
+ * ⚠️ SEMPRE, e é a metade «não some» da cláusula 3. Existe como função com nome próprio, e não como um
27
+ * `!recusa` no ponto de uso, porque responde a outra pergunta: `recusaDaAlternancia` diz *por que* não dá;
28
+ * esta diz *que a linha continua na tela*. Juntá-las faria «não há motivo» parecer «não desenhe a linha».
29
+ */
30
+ export declare function mostraMesmoExigida(): boolean;
31
+ /** Os transportes que exigem alternância, para quem precisa de os enumerar. Vem da REGRA, não de uma cópia. */
32
+ export declare const EXIGEM_ALTERNANCIA: ReadonlySet<string>;
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/latch-refusal.ts — QUANDO A ALTERNÂNCIA NÃO É UMA ESCOLHA, e o que se diz a quem carregou no botão.
3
+ //
4
+ // ========================= A CLÁUSULA 3 DO ADR-0113 =========================
5
+ // «É impossível desligá-la em modos que não tem como funcionar sem ela (voz e câmera)» — a frase do Dev. Em
6
+ // olhos, rosto, gestos e fala a alternância é o que faz a entrada funcionar: sem ela, um comando de cada vez
7
+ // significa que a criança não consegue andar e agir.
8
+ //
9
+ // ⚠️ E O CONTROLE NÃO SOME — FICA DESABILITADO, COM O MOTIVO DITO. Sumir ensina que a coisa não existe: um
10
+ // adulto conclui que ela foi retirada, não que a webcam a exige. É a mesma metade «never silently removed»
11
+ // que o ADR-0076 escreveu para a simulação, e este ficheiro é o irmão do `ui/simulation-refusal` de propósito
12
+ // — mesma forma, mesmo lugar na camada, e portanto conferível no project `node`, sem documento.
13
+ //
14
+ // 🔴 E O MOTIVO É UM FACTO SOBRE O APARELHO, NUNCA UMA REPREENSÃO. «O controle por olhar precisa das teclas de
15
+ // alternância para funcionar» diz por que o botão não responde; «não desligue isto» repreende uma criança por
16
+ // mexer num ajuste de que ela depende. O ADR-0076 já pagou esta distinção uma vez, e a mutação que a apanhou
17
+ // trocava a frase por uma mais curta, mais clara e mais útil — que reprovava na mesma.
18
+ //
19
+ // Módulo-folha: importa só a regra pura da alternância.
20
+ import { alternanciaEhEscolha, UM_COMANDO_DE_CADA_VEZ } from '../input/latch-scope.js';
21
+ /**
22
+ * A chave i18n do motivo, POR TRANSPORTE.
23
+ *
24
+ * ⚠️ QUATRO CHAVES E NÃO UMA COM `{aparelho}`, e a razão é de tradução e não de estilo: «os gestos» é plural
25
+ * e «o olhar» não, logo uma frase única obrigaria cada idioma a montar concordância a partir de um
26
+ * substantivo solto. É exactamente o defeito que o `sr.nav.clockOne` registou para «às 1 horas», e que o
27
+ * `ui/simulation-refusal` já recusou pela mesma razão com os três eixos dele.
28
+ */
29
+ export const CHAVE_DA_RECUSA = Object.freeze({
30
+ olhos: 'alt.exigida.olhos',
31
+ rosto: 'alt.exigida.rosto',
32
+ gestos: 'alt.exigida.gestos',
33
+ fala: 'alt.exigida.fala',
34
+ });
35
+ /**
36
+ * A alternância pode ser desligada neste aparelho? E se não, o que dizer.
37
+ *
38
+ * `null` = pode; a interface não mostra nada, porque um aviso que aparece sempre deixa de ser lido.
39
+ */
40
+ export function recusaDaAlternancia(transporte) {
41
+ if (alternanciaEhEscolha(transporte))
42
+ return null;
43
+ const chave = CHAVE_DA_RECUSA[transporte];
44
+ // 📌 Um transporte que exija alternância e não tenha frase seria um botão desabilitado SEM motivo — pior do
45
+ // que o defeito que isto conserta, porque a criança deixa de saber sequer que há uma razão. O gate afirma
46
+ // que os dois conjuntos coincidem; aqui a ausência degrada para «não recuso», que mantém o controle vivo.
47
+ return chave ? { chave, transporte } : null;
48
+ }
49
+ /**
50
+ * O controle continua NA TELA quando a alternância é exigida?
51
+ *
52
+ * ⚠️ SEMPRE, e é a metade «não some» da cláusula 3. Existe como função com nome próprio, e não como um
53
+ * `!recusa` no ponto de uso, porque responde a outra pergunta: `recusaDaAlternancia` diz *por que* não dá;
54
+ * esta diz *que a linha continua na tela*. Juntá-las faria «não há motivo» parecer «não desenhe a linha».
55
+ */
56
+ export function mostraMesmoExigida() {
57
+ return true;
58
+ }
59
+ /** Os transportes que exigem alternância, para quem precisa de os enumerar. Vem da REGRA, não de uma cópia. */
60
+ export const EXIGEM_ALTERNANCIA = UM_COMANDO_DE_CADA_VEZ;
@@ -1,4 +1,36 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * A RÉGUA DO ALVO DE TOQUE, INDEXADA PELA ALTURA DO VIEWPORT (ADR-0095, decisão do Dev).
4
+ *
5
+ * ⚠️ O ALVO DEIXOU DE SER UM NÚMERO E PASSOU A SER UMA FUNÇÃO DA TELA, e o motivo é um custo que o gate de
6
+ * `pausa-44px` já tinha MEDIDO e deixado por resolver: a 640×360 o cartão de pausa não cabe e a lista ROLA.
7
+ * Remedido em 06/09, porque o cartão mudou desde então: 391 px de conteúdo para 349 visíveis, ou seja 42 px
8
+ * de excesso (o comentário antigo dizia 413/353). Um alvo de 44 px que exige rolagem para ser alcançado
9
+ * pode custar mais dedo do que um de 24 px que está à vista.
10
+ *
11
+ * Os três degraus são os do Dev, e os dois extremos são as duas normas — não números de gosto:
12
+ *
13
+ * altura ≥ 720 44 px WCAG 2.2 · 2.5.5 Target Size (Enhanced) — AAA
14
+ * altura ≥ 540 34 px o degrau do meio
15
+ * altura < 540 24 px WCAG 2.2 · 2.5.8 Target Size (Minimum) — AA
16
+ *
17
+ * ⚠️ ISTO É «MARCAR HONESTAMENTE ONDE SÓ DÁ AA», que é regra escrita do projeto — e não uma renúncia
18
+ * silenciosa. O que se perde em 360 está registrado com número no ADR-0095: a 96 px/pol, 24 CSS px são
19
+ * 6,4 mm, abaixo do alvo de polegar de 9,6 mm que o painel de toque deste jogo cita. É por isso que o
20
+ * ESPAÇAMENTO entre alvos passa a ser o que protege o dedo onde o tamanho não pode — a mesma saída que a
21
+ * própria 2.5.8 dá na sua exceção de spacing.
22
+ */
23
+ export declare const REGUA_DE_ALVO: readonly {
24
+ readonly altura: number;
25
+ readonly alvo: number;
26
+ }[];
27
+ /**
28
+ * O menor alvo de toque aceitável num viewport desta altura, em CSS px.
29
+ *
30
+ * ⚠️ NUNCA DEVOLVE MENOS DE 24: abaixo disso não é «AA num aparelho pequeno», é furar o piso da WCAG. Uma
31
+ * tela mais baixa que 360 não compra o direito de encolher mais — compra o direito de mostrar menos itens.
32
+ */
33
+ export declare function alvoMinimoDeToque(alturaCss: number): number;
2
34
  /** Liga a contagem de jogadores. Chamado uma vez pela raiz, antes do primeiro `layout()`. */
3
35
  export declare function initLayout(deps: {
4
36
  numJogadores: () => number;
@@ -11,6 +11,45 @@
11
11
  import { $ } from './dom.js';
12
12
  import { screenBaseSize } from '../core/screens.js';
13
13
  import { crtScanVars } from '../render/crt.js';
14
+ /**
15
+ * A RÉGUA DO ALVO DE TOQUE, INDEXADA PELA ALTURA DO VIEWPORT (ADR-0095, decisão do Dev).
16
+ *
17
+ * ⚠️ O ALVO DEIXOU DE SER UM NÚMERO E PASSOU A SER UMA FUNÇÃO DA TELA, e o motivo é um custo que o gate de
18
+ * `pausa-44px` já tinha MEDIDO e deixado por resolver: a 640×360 o cartão de pausa não cabe e a lista ROLA.
19
+ * Remedido em 06/09, porque o cartão mudou desde então: 391 px de conteúdo para 349 visíveis, ou seja 42 px
20
+ * de excesso (o comentário antigo dizia 413/353). Um alvo de 44 px que exige rolagem para ser alcançado
21
+ * pode custar mais dedo do que um de 24 px que está à vista.
22
+ *
23
+ * Os três degraus são os do Dev, e os dois extremos são as duas normas — não números de gosto:
24
+ *
25
+ * altura ≥ 720 44 px WCAG 2.2 · 2.5.5 Target Size (Enhanced) — AAA
26
+ * altura ≥ 540 34 px o degrau do meio
27
+ * altura < 540 24 px WCAG 2.2 · 2.5.8 Target Size (Minimum) — AA
28
+ *
29
+ * ⚠️ ISTO É «MARCAR HONESTAMENTE ONDE SÓ DÁ AA», que é regra escrita do projeto — e não uma renúncia
30
+ * silenciosa. O que se perde em 360 está registrado com número no ADR-0095: a 96 px/pol, 24 CSS px são
31
+ * 6,4 mm, abaixo do alvo de polegar de 9,6 mm que o painel de toque deste jogo cita. É por isso que o
32
+ * ESPAÇAMENTO entre alvos passa a ser o que protege o dedo onde o tamanho não pode — a mesma saída que a
33
+ * própria 2.5.8 dá na sua exceção de spacing.
34
+ */
35
+ export const REGUA_DE_ALVO = Object.freeze([
36
+ { altura: 720, alvo: 44 },
37
+ { altura: 540, alvo: 34 },
38
+ { altura: 0, alvo: 24 },
39
+ ]);
40
+ /**
41
+ * O menor alvo de toque aceitável num viewport desta altura, em CSS px.
42
+ *
43
+ * ⚠️ NUNCA DEVOLVE MENOS DE 24: abaixo disso não é «AA num aparelho pequeno», é furar o piso da WCAG. Uma
44
+ * tela mais baixa que 360 não compra o direito de encolher mais — compra o direito de mostrar menos itens.
45
+ */
46
+ export function alvoMinimoDeToque(alturaCss) {
47
+ const h = Number.isFinite(alturaCss) ? alturaCss : 0;
48
+ for (const degrau of REGUA_DE_ALVO)
49
+ if (h >= degrau.altura)
50
+ return degrau.alvo;
51
+ return 24;
52
+ }
14
53
  // A CONTAGEM DE JOGADORES entra por injeção desde 2026-08-26. Era `numPlayers`, um `let` de `core/state`
15
54
  // importado como binding vivo — e um `let` de módulo é compartilhado por qualquer segundo jogo que a
16
55
  // mesma página carregue (D13 do `demos`, ADR-0038). O que entra aqui é o GETTER da rodada que a raiz
@@ -18,8 +57,23 @@ import { crtScanVars } from '../render/crt.js';
18
57
  let _numJogadores = () => 1;
19
58
  /** Liga a contagem de jogadores. Chamado uma vez pela raiz, antes do primeiro `layout()`. */
20
59
  export function initLayout(deps) { _numJogadores = deps.numJogadores; }
60
+ /**
61
+ * A CASCA QUE DÁ O ESPAÇO DISPONÍVEL — por id OU por classe, e as duas formas valem o mesmo.
62
+ *
63
+ * ⚠️ ERA SÓ `#stage-wrap`, E ISSO DEIXOU A ENGINE SEM ESCALA NO PRÓPRIO HOST. O `app/index.html` do jogo
64
+ * trazia `<div id="stage-wrap">`; quando o cartucho saiu (issue #111) sobrou o `app/quiz.html`, que tem
65
+ * `<div class="stage-wrap">`. A procura por id falhava, `layout()` fazia early-return, e **nada reportava
66
+ * nada**: um `return` silencioso é indistinguível de «não havia o que fazer». O comentário do próprio
67
+ * `quiz.html` dizia «mesmo id que ui/layout escala» — quem o escreveu acreditava que corria.
68
+ *
69
+ * O consumidor não erra ao usar a classe: um documento pode ter várias telas, e um id é único. Aceitar as
70
+ * duas é o que torna a engine consumível por quem não copiou o markup dela.
71
+ */
72
+ function cascaDoPalco() {
73
+ return $('#stage-wrap') ?? $('.stage-wrap');
74
+ }
21
75
  export function layout() {
22
- const wrap = $('#stage-wrap');
76
+ const wrap = cascaDoPalco();
23
77
  if (!wrap)
24
78
  return;
25
79
  wrap.style.paddingRight = '0px';
@@ -47,6 +101,15 @@ export function layout() {
47
101
  gr.style.setProperty('--hud-fs', Math.max(9, Math.round(180 * k * 0.052)) + 'px');
48
102
  gr.style.setProperty('--ui-fs', (8 * k) + 'px'); // base LÓGICA 8px × k (16px em k=2)
49
103
  gr.style.setProperty('--tap', (22 * k) + 'px'); // toque 22px × k (44px em k=2, piso WCAG)
104
+ // ⚠️ O PISO DA RÉGUA (ADR-0095), e ele é OUTRA COISA que o `--tap`. O `--tap` é o tamanho PREFERIDO e
105
+ // cresce com a escala do canvas; `--alvo-min` é o CHÃO por altura de tela — 24 px abaixo de 540, 34 a
106
+ // partir de 540, 44 a partir de 720. Um botão isolado usa o preferido; um item de LISTA, que tem de
107
+ // caber inteiro na tela, usa o chão.
108
+ //
109
+ // ⚠️ A ALTURA É A DO ESPAÇO DISPONÍVEL, e não a da sub-tela de um jogador: o dedo toca o aparelho, não
110
+ // o viewport lógico. Em quatro telas divididas cada uma tem 180 px de alto, e encolher o alvo por causa
111
+ // disso seria ler o número errado — o aparelho continua o mesmo.
112
+ gr.style.setProperty('--alvo-min', alvoMinimoDeToque(availH) + 'px');
50
113
  }
51
114
  crtScanVars(); // scanlines re-alinham quando a escala k muda
52
115
  if (/[?&]debug=true/.test(location.search))
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { PlayerView } from '../core/entity.js';
3
+ /** As quatro animações de CENA, como vocabulário fechado. */
4
+ export type MotionSceneKey = 'parallax' | 'decor' | 'items' | 'particles';
5
+ /** As três do PERSONAGEM, que são campos do jogador. */
6
+ export type MotionCharProp = 'rmWalk' | 'rmBreath' | 'rmFlavor';
7
+ /** Uma animação do personagem e a chave i18n do seu rótulo. */
8
+ export interface MotionCharDef {
9
+ readonly prop: MotionCharProp;
10
+ readonly lbl: string;
11
+ }
12
+ /** ⚠️ `PlayerView` e não `Record`: um `Record` aceita qualquer objecto com essas chaves, jogador ou não. */
13
+ export type MotionPlayer = PlayerView<'rmWalk' | 'rmBreath' | 'rmFlavor'>;
14
+ /** Os quatro interruptores de cena. Objecto VIVO — ver a nota sobre partilha por referência no topo. */
15
+ export type MotionSceneFlags = Record<MotionSceneKey, boolean>;
16
+ /** As quatro animações de CENA. São a união inteira, e o compilador prova-o logo abaixo. */
17
+ export declare const CHAVES_DE_CENA: readonly ["parallax", "decor", "items", "particles"];
18
+ /** As três animações do PERSONAGEM, com as chaves que o `RM_LABEL` desta mesma camada já traduz. */
19
+ export declare const ANIMACOES_DO_PERSONAGEM: readonly [{
20
+ readonly prop: "rmWalk";
21
+ readonly lbl: "rm.walk";
22
+ }, {
23
+ readonly prop: "rmBreath";
24
+ readonly lbl: "rm.breath";
25
+ }, {
26
+ readonly prop: "rmFlavor";
27
+ readonly lbl: "rm.flavor";
28
+ }];
29
+ /** Os quatro interruptores no padrão do sistema — `prefers-reduced-motion`, por `defaultReducedMotion()`. */
30
+ export declare function padraoDeCena(): MotionSceneFlags;
31
+ /**
32
+ * O estado guardado, ou o padrão do sistema quando não há nada guardado.
33
+ *
34
+ * ⚠️ CADA CHAVE É LIDA UMA A UMA, e não `{...guardado}`. O que está no armazenamento veio do navegador de uma
35
+ * criança e pode estar truncado ou de uma versão anterior: espalhar o objecto traria chaves a mais e deixaria
36
+ * chaves a menos por preencher, e uma chave em falta lê-se como `undefined` — que é «não reduzido» para quem
37
+ * pediu redução. O laço garante exactamente as quatro.
38
+ */
39
+ export declare function lerCenaGuardada(): MotionSceneFlags;
40
+ /** Guarda os quatro interruptores. Chamada depois de cada mudança, como o `saveRM` do cartucho fazia. */
41
+ export declare function guardarCena(rm: MotionSceneFlags): void;
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/motion-scene — O MOVIMENTO REDUZIDO DE CENA VOLTA PARA A ENGINE (ADR-0106 §4, etapa 1).
3
+ //
4
+ // ========================= O QUE ESTAVA ERRADO, MEDIDO E NÃO SUPOSTO =========================
5
+ // O `PauseIconsCtx` e o `SettingsMotionCtx` pediam ao JOGO quatro coisas — `rm`, `rmKeys`, `rmChar`,
6
+ // `saveRM` — e o ADR-0106 chamou-lhes «do jogo POR ACIDENTE». A medição de 2026-09-08 no `game-platformer`
7
+ // mostra que a palavra é exacta, porque nenhuma das quatro contém uma escolha do jogo:
8
+ //
9
+ // · `RM_KEYS` era `['parallax','decor','items','particles']` escrito à mão — que é a união `MotionSceneKey`
10
+ // INTEIRA, declarada na engine. Não é «quais destes este jogo tem»: são os quatro, sempre;
11
+ // · `RM_CHAR` era as três propriedades de `MotionCharProp` com as chaves i18n `rm.walk`/`rm.breath`/
12
+ // `rm.flavor` — e o `RM_LABEL` da engine já traduz essas mesmas chaves;
13
+ // · `rm` era lido de `store.KEYS.reducedMotion` (chave da engine) com `defaultReducedMotion()` (padrão da
14
+ // engine);
15
+ // · `saveRM` era `store.setJSON` para a mesma chave da engine.
16
+ //
17
+ // ⚠️ ERA UMA CÓPIA, NÃO UMA DECISÃO. E o custo não é elegância: **cinco jogos não têm nada disto**, porque
18
+ // cada cartucho tinha de se lembrar de escrever as quatro linhas. Uma criança que precisa de parar o
19
+ // movimento da cena abre esses cinco e não tem por onde.
20
+ //
21
+ // ⚠️ O QUE CONTINUA A SER DO JOGO, e por natureza: o EFEITO. Quem lê `rm.decor` para congelar as nuvens é o
22
+ // jogo — a engine possui o interruptor, não o que ele apaga. É a mesma divisão do ADR-0106: o valor é da
23
+ // engine, o efeito colateral é do cartucho.
24
+ //
25
+ // ⚠️ E O OBJECTO É PARTILHADO POR REFERÊNCIA, de propósito. No cartucho ele entra em oito módulos
26
+ // (`weather`, `life`, `fx`, …) que leem `rm.decor`/`rm.particles` a cada quadro. Devolver uma cópia faria
27
+ // cada leitor ver um valor congelado no arranque, e o interruptor deixaria de fazer nada — em silêncio.
28
+ // ⚠️ O VOCABULÁRIO DESCEU PARA CÁ, e foi um gate que o mandou. Escrito ao contrário — os tipos no
29
+ // `settings-motion` e este módulo a importá-los —, o `tests/lotes-passo5` reprovou por CICLO: o painel importa
30
+ // os valores daqui e este importava os tipos de lá. O analisador conta o `import type` como aresta, e tem
31
+ // razão para o que mede (ordem de extração). A saída certa não era calar o gate: o vocabulário pertence a quem
32
+ // possui os VALORES, e o painel é consumidor dele. O `settings-motion` mantém os nomes publicados por alias,
33
+ // então nenhuma linha de importação de nenhum consumidor muda.
34
+ import * as store from '../platform/storage.js';
35
+ import { defaultReducedMotion } from '../core/state.js';
36
+ /** As quatro animações de CENA. São a união inteira, e o compilador prova-o logo abaixo. */
37
+ export const CHAVES_DE_CENA = ['parallax', 'decor', 'items', 'particles'];
38
+ const _COBRE_A_UNIAO = true;
39
+ void _COBRE_A_UNIAO;
40
+ /** As três animações do PERSONAGEM, com as chaves que o `RM_LABEL` desta mesma camada já traduz. */
41
+ export const ANIMACOES_DO_PERSONAGEM = Object.freeze([
42
+ { prop: 'rmWalk', lbl: 'rm.walk' },
43
+ { prop: 'rmBreath', lbl: 'rm.breath' },
44
+ { prop: 'rmFlavor', lbl: 'rm.flavor' },
45
+ ]);
46
+ const _COBRE_O_PERSONAGEM = true;
47
+ void _COBRE_O_PERSONAGEM;
48
+ /** Os quatro interruptores no padrão do sistema — `prefers-reduced-motion`, por `defaultReducedMotion()`. */
49
+ export function padraoDeCena() {
50
+ const o = {};
51
+ const padrao = defaultReducedMotion();
52
+ for (const k of CHAVES_DE_CENA)
53
+ o[k] = padrao;
54
+ return o;
55
+ }
56
+ /**
57
+ * O estado guardado, ou o padrão do sistema quando não há nada guardado.
58
+ *
59
+ * ⚠️ CADA CHAVE É LIDA UMA A UMA, e não `{...guardado}`. O que está no armazenamento veio do navegador de uma
60
+ * criança e pode estar truncado ou de uma versão anterior: espalhar o objecto traria chaves a mais e deixaria
61
+ * chaves a menos por preencher, e uma chave em falta lê-se como `undefined` — que é «não reduzido» para quem
62
+ * pediu redução. O laço garante exactamente as quatro.
63
+ */
64
+ export function lerCenaGuardada() {
65
+ const guardado = store.getJSON(store.KEYS.reducedMotion, null);
66
+ if (!guardado || typeof guardado !== 'object')
67
+ return padraoDeCena();
68
+ const o = {};
69
+ for (const k of CHAVES_DE_CENA)
70
+ o[k] = !!guardado[k];
71
+ return o;
72
+ }
73
+ /** Guarda os quatro interruptores. Chamada depois de cada mudança, como o `saveRM` do cartucho fazia. */
74
+ export function guardarCena(rm) {
75
+ store.setJSON(store.KEYS.reducedMotion, rm);
76
+ }
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** As três coisas do `document` de que a casca precisa. Mesma forma de `ui/loop-crash`. */
3
+ export interface PanelShellCtx {
4
+ /** `document.querySelector`, injetado — a casca nunca alcança o `document` global. */
5
+ procurar: (sel: string) => HTMLElement | null;
6
+ /** `document.createElement`, injetado. */
7
+ criar: (tag: string) => HTMLElement;
8
+ }
9
+ export interface PanelShellSpec {
10
+ /** O id do painel: `typo`, `audio`, `visual`… Gera `#X`, `#X-title`, `#X-list`, `#X-reset`, `#X-close`. */
11
+ id: string;
12
+ /** O título, JÁ TRADUZIDO. Vai por `textContent`. */
13
+ titulo: string;
14
+ /** O `aria-label` da lista, já traduzido — o nome do grupo que a criança ouve ao entrar nele. */
15
+ rotuloDaLista: string;
16
+ /** Os rótulos dos dois botões, já traduzidos. */
17
+ rotuloReset: string;
18
+ rotuloFechar: string;
19
+ /**
20
+ * A introdução do painel, já traduzida. Vira o texto de REPOUSO do rodapé, via `data-explain-idle`.
21
+ *
22
+ * ⚠️ É O ÚNICO CAMINHO QUE ESTA CASCA OFERECE PARA UMA INTRODUÇÃO, e é o ponto da issue #62: não há por
23
+ * onde passar um parágrafo de prosa para o topo do cartão. Ausente = o painel não tem introdução, que é
24
+ * uma resposta legítima e não uma omissão.
25
+ */
26
+ introducao?: string;
27
+ }
28
+ /** O que a casca devolve: o nó e os ids que ela criou, para o painel não os adivinhar. */
29
+ export interface PanelShell {
30
+ overlay: HTMLElement;
31
+ card: HTMLElement;
32
+ lista: HTMLElement;
33
+ reset: HTMLElement;
34
+ fechar: HTMLElement;
35
+ /** Os cinco selectores que este painel passa a garantir. É o contrato, agora dito em vez de descoberto. */
36
+ ids: {
37
+ overlay: string;
38
+ title: string;
39
+ lista: string;
40
+ reset: string;
41
+ fechar: string;
42
+ };
43
+ }
44
+ /** Os ids que um painel de `id` ocupa. Exportado porque um gate e um consumidor precisam de os nomear. */
45
+ export declare function idsDaCasca(id: string): PanelShell['ids'];
46
+ /**
47
+ * Monta (ou reaproveita) a casca do painel `spec.id` e devolve as suas partes.
48
+ *
49
+ * IDEMPOTENTE: se já existir um `#id`, ele é reutilizado e o conteúdo do cartão é reconstruído. Um painel que
50
+ * a raiz monte duas vezes não pode acabar com dois véus — e a raiz monta mais do que uma vez, porque a
51
+ * contagem de jogadores muda a grade de telas.
52
+ */
53
+ export declare function montarCasca(ctx: PanelShellCtx, spec: PanelShellSpec): PanelShell;
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/panel-shell — A CASCA DE UM PAINEL DE AJUSTES, construída em vez de exigida.
3
+ //
4
+ // ========================= O ACHADO QUE ISTO CONSERTA =========================
5
+ // O segundo consumidor (a etapa C da issue #63) mediu-o e escreveu-o no achado 6:
6
+ //
7
+ // «O CONTRATO DE MARKUP É INVISÍVEL. O ctx do painel pede `$` e `store`; o que ele REALMENTE exige é que
8
+ // o documento do consumidor contenha `#typo`, `#typo-list`, `#typo-preview`, `#typo-close` e
9
+ // `#typo-reset`. Nada no tipo diz isso — descobre-se por tentativa, e o modo de falhar é o pior
10
+ // possível: o painel abre VAZIO, sem erro.»
11
+ //
12
+ // Cada `ui/settings-*.ts` preenche o INTERIOR do seu painel; o EXTERIOR — o véu, o cartão, o título, o
13
+ // rodapé de ações e o botão de restaurar — vinha do `app/index.html`, que saiu com o cartucho (#111). Desde
14
+ // então a engine EXIGE cinco ids por painel e não os declara em lado nenhum.
15
+ //
16
+ // ⚠️ E A REGRA DE MENU DO `CLAUDE.md` §4 DEPENDIA DE ALGUÉM SE LEMBRAR DELA. A introdução de um painel vai no
17
+ // `data-explain-idle` do cartão — nunca num `<p>` de prosa no topo —, porque um menu que explica item a item
18
+ // obriga a criança a LER TUDO para achar o que procura. A issue #62 mandava editar seis blocos de markup para
19
+ // isso; o markup saiu, e a regra ficou sem alvo. Aqui ela deixa de ser lembrete e passa a ser construção: a
20
+ // casca não tem por onde receber um `<p>` no topo.
21
+ //
22
+ // ========================= A FORMA, QUE FOI MEDIDA E NÃO INVENTADA =========================
23
+ // É a do painel de tipografia do `app/quiz.html`, que é o único que sobrou e o que o segundo consumidor
24
+ // exercitou de facto:
25
+ //
26
+ // <div id="X" class="overlay" hidden>
27
+ // <div class="overlay__card" role="dialog" aria-modal="true" aria-labelledby="X-title" [data-explain-idle]>
28
+ // <h2 id="X-title">…</h2>
29
+ // <div id="X-list" class="ctrl-list" role="group" aria-label="…"></div> ← o interior, do settings-*
30
+ // <div class="overlay__actions">
31
+ // <button id="X-reset">…</button> <button id="X-close">…</button>
32
+ // </div>
33
+ // </div>
34
+ // </div>
35
+ //
36
+ // ⚠️ O RODAPÉ `.opt-explain` NÃO É CRIADO AQUI, e isso é deliberado: `ui/settings-panel.fillExplain` cria-o
37
+ // quando move a primeira dica para lá. Criá-lo vazio aqui daria uma região `aria-live` que anuncia nada, e
38
+ // duas mãos a criar o mesmo nó é como ele acabaria duplicado.
39
+ //
40
+ // Sem `innerHTML`: tudo por `criar` + `textContent`, no molde de `ui/loop-crash` e `ui/focus-trap`. O título
41
+ // e o rótulo da lista vêm do CHAMADOR já resolvidos — este módulo não traduz, para poder ser exercitado sem
42
+ // dicionário.
43
+ //
44
+ // ⚠️ E NÃO IMPORTA NADA. Uma casca que não usa `innerHTML` também não precisa de escapar texto: `textContent`
45
+ // escapa por construção. Chegou a haver aqui um `escaparHtml` importado «por conveniência» — que é como um
46
+ // módulo-folha deixa de o ser, e como um leitor futuro passa a procurar a interpolação que não existe.
47
+ /** Os ids que um painel de `id` ocupa. Exportado porque um gate e um consumidor precisam de os nomear. */
48
+ export function idsDaCasca(id) {
49
+ return { overlay: id, title: `${id}-title`, lista: `${id}-list`, reset: `${id}-reset`, fechar: `${id}-close` };
50
+ }
51
+ /**
52
+ * Monta (ou reaproveita) a casca do painel `spec.id` e devolve as suas partes.
53
+ *
54
+ * IDEMPOTENTE: se já existir um `#id`, ele é reutilizado e o conteúdo do cartão é reconstruído. Um painel que
55
+ * a raiz monte duas vezes não pode acabar com dois véus — e a raiz monta mais do que uma vez, porque a
56
+ * contagem de jogadores muda a grade de telas.
57
+ */
58
+ export function montarCasca(ctx, spec) {
59
+ const ids = idsDaCasca(spec.id);
60
+ const overlay = ctx.procurar('#' + ids.overlay) ?? ctx.criar('div');
61
+ overlay.id = ids.overlay;
62
+ overlay.className = 'overlay';
63
+ overlay.hidden = true;
64
+ while (overlay.firstChild)
65
+ overlay.removeChild(overlay.firstChild);
66
+ const card = ctx.criar('div');
67
+ card.className = 'overlay__card';
68
+ card.setAttribute('role', 'dialog');
69
+ card.setAttribute('aria-modal', 'true');
70
+ card.setAttribute('aria-labelledby', ids.title);
71
+ // A introdução do painel é o texto de REPOUSO do rodapé (CLAUDE.md §4), nunca um `<p>` no topo.
72
+ if (spec.introducao)
73
+ card.setAttribute('data-explain-idle', spec.introducao);
74
+ const h2 = ctx.criar('h2');
75
+ h2.id = ids.title;
76
+ h2.textContent = spec.titulo;
77
+ card.appendChild(h2);
78
+ const lista = ctx.criar('div');
79
+ lista.id = ids.lista;
80
+ lista.className = 'ctrl-list';
81
+ lista.setAttribute('role', 'group');
82
+ lista.setAttribute('aria-label', spec.rotuloDaLista);
83
+ card.appendChild(lista);
84
+ const acoes = ctx.criar('div');
85
+ acoes.className = 'overlay__actions';
86
+ const reset = botao(ctx, ids.reset, spec.rotuloReset, 'mode-btn');
87
+ const fechar = botao(ctx, ids.fechar, spec.rotuloFechar, 'mode-btn is-on');
88
+ acoes.appendChild(reset);
89
+ acoes.appendChild(fechar);
90
+ card.appendChild(acoes);
91
+ overlay.appendChild(card);
92
+ return { overlay, card, lista, reset, fechar, ids };
93
+ }
94
+ function botao(ctx, id, rotulo, classe) {
95
+ const b = ctx.criar('button');
96
+ b.id = id;
97
+ b.className = classe;
98
+ b.setAttribute('type', 'button');
99
+ // `textContent` e não `innerHTML`: um rótulo traduzido é dado de fora como qualquer outro, e um dicionário
100
+ // de consumidor pode trazer o que quiser dentro dele.
101
+ b.textContent = rotulo;
102
+ return b;
103
+ }