@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
@@ -16,6 +16,7 @@ import { CRT, CRT_DEFAULT, applyCrt } from '../render/crt.js';
16
16
  import { defaultReducedMotion } from '../core/state.js';
17
17
  import { markChanged, markMenuChanged } from './changed-mark.js';
18
18
  import { t } from '../core/i18n.js';
19
+ import { CHAVES_DE_CENA, ANIMACOES_DO_PERSONAGEM, lerCenaGuardada, guardarCena } from './motion-scene.js';
19
20
  /**
20
21
  * Alvo → CHAVE i18n do rótulo. CHAVES, e não texto, pelo motivo de sempre: uma tabela de `const` com texto
21
22
  * resolve UMA vez, no import, e fica congelada no idioma do boot.
@@ -112,10 +113,20 @@ export function getSelectedPlayer() { return selectedPlayer; }
112
113
  /** Chamado de fora (ex.: o atalho "anim" do menu de pausa) antes de open(). */
113
114
  export function setSelectedPlayer(i) { selectedPlayer = i; }
114
115
  export function initSettingsMotion(ctx) {
116
+ /*
117
+ * ⚠️ RESOLVIDAS UMA VEZ, NO ARRANQUE, e não a cada uso. O `rm` é mutado in-place e partilhado por
118
+ * REFERÊNCIA com quem desenha a cena; resolvê-lo a cada leitura criaria um objecto novo por chamada, o
119
+ * interruptor deixaria de alcançar o desenho, e não haveria erro nenhum — o menu diria «reduzido» e a cena
120
+ * continuaria a mexer-se.
121
+ */
122
+ const rm = ctx.rm ?? lerCenaGuardada();
123
+ const rmKeys = ctx.rmKeys ?? CHAVES_DE_CENA;
124
+ const rmChar = ctx.rmChar ?? ANIMACOES_DO_PERSONAGEM;
125
+ const saveRM = ctx.saveRM ?? (() => guardarCena(rm));
115
126
  function reflectMotionBtn() {
116
127
  const b = ctx.$('#opt-animation');
117
128
  if (b)
118
- b.classList.toggle('is-on', ctx.rmKeys.some((k) => ctx.rm[k]));
129
+ b.classList.toggle('is-on', rmKeys.some((k) => rm[k]));
119
130
  }
120
131
  function updateMotionMaster() {
121
132
  reflectMotionBtn();
@@ -123,7 +134,7 @@ export function initSettingsMotion(ctx) {
123
134
  if (!m)
124
135
  return;
125
136
  const player = ctx.getPlayers()[selectedPlayer];
126
- const allFrozen = allMotionFrozen(ctx.rmKeys, ctx.rm, ctx.rmChar, player);
137
+ const allFrozen = allMotionFrozen(rmKeys, rm, rmChar, player);
127
138
  m.textContent = motionMasterLabel(allFrozen);
128
139
  ctx.toggleBtn(m, allFrozen);
129
140
  }
@@ -142,11 +153,13 @@ export function initSettingsMotion(ctx) {
142
153
  tabs.querySelectorAll('button[data-ap]').forEach((b) => b.addEventListener('click', () => {
143
154
  selectedPlayer = Number(b.dataset.ap);
144
155
  render();
156
+ // A prosa volta para o rodapé depois de as linhas serem reconstruídas (CLAUDE.md §4, #109).
157
+ ctx.fillExplain?.(ctx.$('#animation .overlay__card'));
145
158
  }));
146
159
  }
147
160
  const player = ctx.getPlayers()[selectedPlayer];
148
- const charRows = buildCharRowsHtml(ctx.rmChar, player);
149
- const sceneRows = buildSceneRowsHtml(ctx.rmKeys, ctx.rm, RM_LABEL, RM_SOON);
161
+ const charRows = buildCharRowsHtml(rmChar, player);
162
+ const sceneRows = buildSceneRowsHtml(rmKeys, rm, RM_LABEL, RM_SOON);
150
163
  const crtRows = crtToggleRowHtml(t(CRT_LBL.scan), 'scan', !!CRT.scan) + crtToggleRowHtml(t(CRT_LBL.vig), 'vig', !!CRT.vig) + crtRoundRowHtml(t(CRT_LBL.round), CRT.round);
151
164
  el.innerHTML =
152
165
  `<h3 class="panel-sub">Personagem${ctx.getNumPlayers() > 1 ? ' · Jogador ' + (selectedPlayer + 1) : ''} <span class="panel-sub__tag">por jogador</span></h3>${charRows}` +
@@ -174,11 +187,11 @@ export function initSettingsMotion(ctx) {
174
187
  }));
175
188
  el.querySelectorAll('button[data-rm]').forEach((b) => b.addEventListener('click', () => {
176
189
  const k = b.dataset.rm;
177
- ctx.rm[k] = !ctx.rm[k];
178
- ctx.saveRM();
190
+ rm[k] = !rm[k];
191
+ saveRM();
179
192
  render();
180
193
  updateMotionMaster();
181
- ctx.srSay(sceneMotionAnnouncement(t(RM_LABEL[k]), ctx.rm[k]));
194
+ ctx.srSay(sceneMotionAnnouncement(t(RM_LABEL[k]), rm[k]));
182
195
  }));
183
196
  updateMotionMaster();
184
197
  refreshMarks();
@@ -200,10 +213,10 @@ export function initSettingsMotion(ctx) {
200
213
  mudou.push(changed);
201
214
  markChanged(el?.querySelector(sel)?.closest('.ctrl-row') ?? null, changed);
202
215
  };
203
- for (const c of ctx.rmChar)
216
+ for (const c of rmChar)
204
217
  marcar(`[data-rmc="${c.prop}"]`, !!(player && player[c.prop]) !== padraoRm);
205
- for (const k of ctx.rmKeys)
206
- marcar(`[data-rm="${k}"]`, !!ctx.rm[k] !== padraoRm);
218
+ for (const k of rmKeys)
219
+ marcar(`[data-rm="${k}"]`, !!rm[k] !== padraoRm);
207
220
  marcar('[data-crt-tgl="scan"]', !!CRT.scan !== !!CRT_DEFAULT.scan);
208
221
  marcar('[data-crt-tgl="vig"]', !!CRT.vig !== !!CRT_DEFAULT.vig);
209
222
  marcar('[data-crt="round"]', CRT.round !== CRT_DEFAULT.round);
@@ -222,11 +235,11 @@ export function initSettingsMotion(ctx) {
222
235
  if (resetBtn)
223
236
  resetBtn.addEventListener('click', () => {
224
237
  const padraoRm = defaultReducedMotion();
225
- for (const k of ctx.rmKeys)
226
- ctx.rm[k] = padraoRm;
227
- ctx.saveRM();
238
+ for (const k of rmKeys)
239
+ rm[k] = padraoRm;
240
+ saveRM();
228
241
  ctx.getPlayers().forEach((p, i) => {
229
- for (const c of ctx.rmChar) {
242
+ for (const c of rmChar) {
230
243
  p[c.prop] = padraoRm;
231
244
  ctx.store.setBool('incl_' + c.prop + '_p' + i, padraoRm);
232
245
  }
@@ -264,13 +277,13 @@ export function initSettingsMotion(ctx) {
264
277
  if (master)
265
278
  master.addEventListener('click', () => {
266
279
  const player = ctx.getPlayers()[selectedPlayer];
267
- const allFrozen = allMotionFrozen(ctx.rmKeys, ctx.rm, ctx.rmChar, player);
280
+ const allFrozen = allMotionFrozen(rmKeys, rm, rmChar, player);
268
281
  const next = !allFrozen;
269
- for (const k of ctx.rmKeys)
270
- ctx.rm[k] = next;
271
- ctx.saveRM();
282
+ for (const k of rmKeys)
283
+ rm[k] = next;
284
+ saveRM();
272
285
  if (player)
273
- for (const c of ctx.rmChar) {
286
+ for (const c of rmChar) {
274
287
  player[c.prop] = next;
275
288
  ctx.store.setBool('incl_' + c.prop + '_p' + selectedPlayer, next);
276
289
  }
@@ -2,6 +2,7 @@
2
2
  import { toggleLabel } from './dom.js';
3
3
  import type { PlayerView } from '../core/entity.js';
4
4
  import type { DomQuery } from '../core/dom-query.js';
5
+ import { type JogadorDaAlternancia as JogadorDaAlternanciaDaAresta } from '../input/latch-sync.js';
5
6
  /** Minimal DOM-selector shape (matches ui/dom.ts's `$`). */
6
7
  export type { DomQuery } from '../core/dom-query.js';
7
8
  /** Minimal platform/storage.ts shape this module needs. */
@@ -12,7 +13,7 @@ export interface MotorStore {
12
13
  }
13
14
  /** Minimal per-player shape this module reads/writes (core/state.ts's `players` entries carry much more). */
14
15
  /** As duas escolhas motoras por jogador: modo Fácil e teclas de alternância. */
15
- export type MotorPlayer = PlayerView<'easy' | 'toggleMove' | 'toggleRun'>;
16
+ export type MotorPlayer = PlayerView<'easy' | 'toggleMove' | 'toggleRun' | 'walkDir'>;
16
17
  export interface SettingsMotorCtx {
17
18
  /** DOM selector (querySelector), injected — never reaches `document` globally. */
18
19
  $: DomQuery;
@@ -25,7 +26,27 @@ export interface SettingsMotorCtx {
25
26
  /** Live player count (core/state.ts's `numPlayers`); a getter because the value is reassigned over time. */
26
27
  getNumPlayers: () => number;
27
28
  /** SHARED setter (also used by the pause-menu quick icon `altmove`) — stays in game.js, injected. */
28
- setToggleMove: (i: number, on: boolean) => void;
29
+ setToggleMove?: (i: number, on: boolean) => void;
30
+ /**
31
+ * ESTE JOGO SEGURA ALGUMA TECLA? — `GameDeclaration.seguraTeclas` (ADR-0115). Sem ele a linha da
32
+ * alternância fica AUSENTE deste painel.
33
+ *
34
+ * ⚠️ E A ENGINE NÃO DESENHA ESTA LINHA — o markup do `#opt-altmove` é do CARTUCHO, e a engine só o
35
+ * encontra pelo `$`. Logo «não oferecer» aqui não é deixar de renderizar: é tornar a linha ausente para
36
+ * toda a gente, com `hidden`, que a tira da tela E da árvore de acessibilidade. Um `aria-disabled` seria a
37
+ * resposta errada — essa é a da cláusula 3 do ADR-0113, onde o controle EXISTE e está travado com motivo.
38
+ *
39
+ * ⚠️ OBRIGATÓRIO, pela mesma razão que no `PauseIconsCtx`: não há padrão seguro. `true` deixa a linha num
40
+ * jogo onde ela não faz nada; `false` esconde-a de uma criança que depende dela.
41
+ */
42
+ seguraTeclas: boolean;
43
+ /**
44
+ * QUAL APARELHO ESTE JOGADOR ESTÁ A USAR (ADR-0113) — atravessa daqui para a escrita.
45
+ *
46
+ * ⚠️ Opcional pela mesma razão que na `EscritaDaAlternanciaCtx`: sem ele a escrita cai no que já fazia,
47
+ * e exigi-lo quebraria todo consumidor por causa de uma migração a meio.
48
+ */
49
+ transporteEmUso?: (jogador: number) => string;
29
50
  /**
30
51
  * A ALTERNÂNCIA DO BOTÃO DE CORRER. Injetada como a irmã acima e pelo mesmo motivo: quem persiste e anuncia
31
52
  * é a raiz de composição, que é quem conhece `players` e o armazenamento.
@@ -33,6 +54,19 @@ export interface SettingsMotorCtx {
33
54
  setToggleRun: (i: number, on: boolean) => void;
34
55
  /** Coin layout depends on any player's Modo Fácil (moedas no chão) — owned by the coin subsystem, injected. */
35
56
  rebuildCoins: () => void;
57
+ /**
58
+ * Move a prosa das linhas para o rodapé (`ui/settings-panel` → `fillExplain`). Chamado a CADA render.
59
+ *
60
+ * ⚠️ NÃO É OPCIONAL POR ELEGÂNCIA: `fillExplain` roda uma vez quando o overlay é frontalizado e move o
61
+ * `.opt-hint` de dentro de cada linha para o rodapé. Este painel RECONSTRÓI as linhas, e as linhas novas
62
+ * voltam com a prosa lá dentro — então a explicação aparece duas vezes, no rodapé e sob o rótulo, a
63
+ * partir do primeiro clique. O `CLAUDE.md` §4 regista exatamente isto, e a issue #109 já o consertou
64
+ * uma vez noutros painéis.
65
+ *
66
+ * Opcional na assinatura porque um consumidor pode montar o painel sem a casca (um teste, o segundo
67
+ * consumidor): sem casca não há rodapé para duplicar.
68
+ */
69
+ fillExplain?: (card: HTMLElement | null) => void;
36
70
  }
37
71
  export interface SettingsMotorApi {
38
72
  /** Re-renders #movement-players (kept `hidden`, per E3 — see playerTabsHTML) and (re)wires its buttons. */
@@ -52,8 +86,72 @@ export interface SettingsMotorApi {
52
86
  }
53
87
  /** localStorage key for a player's Modo Fácil flag (== platform/storage.ts's `easy_p{i}` pattern). */
54
88
  export declare function easyKey(i: number): string;
55
- /** localStorage key da alternância do botão de CORRER (== `toggleRunP` de platform/storage). */
89
+ /**
90
+ * localStorage key da alternância do botão de CORRER, na forma LEGADA (sem transporte).
91
+ *
92
+ * ⚠️ ERA UMA CÓPIA DO LITERAL, com um comentário ao lado a dizer «== `toggleRunP` de platform/storage» — o
93
+ * que é a admissão do defeito escrita como se fosse documentação. Duas cópias de um nome mudam uma de cada
94
+ * vez, e o `platform/storage` já tinha escrito a razão de as chaves serem funções: «virar função aqui é o
95
+ * que impede que um deles escreva num nome torto». Agora delega, e há um nome só.
96
+ *
97
+ * ⚠️ E É A CHAVE LEGADA. O ADR-0104 §C pôs o TRANSPORTE no nome, porque a alternância é do aparelho e não da
98
+ * pessoa; esta continua a ser lida para herdar o que a criança já tinha, e não é escrita. A chave nova é
99
+ * `chaveDaAlternancia`, em `input/latch-scope`.
100
+ */
56
101
  export declare function toggleRunKey(i: number): string;
102
+ /** localStorage key da alternância de MARCHA, por jogador. Delega, como os dois irmãos acima. */
103
+ export declare function toggleMoveKey(i: number): string;
104
+ /**
105
+ * A fatia mínima que a escrita da alternância de marcha toca.
106
+ *
107
+ * ⚠️ `walkDir` ENTRA, e não é detalhe: desligar a alternância tem de PARAR quem está a andar por travamento.
108
+ * Sem isso, a criança desliga o modo e a personagem continua a andar sozinha, sem tecla nenhuma premida —
109
+ * e não há erro nenhum a dizê-lo.
110
+ *
111
+ * ⚠️ E É FATIA PRÓPRIA, e não o `MotorPlayer`, porque os DOIS chamadores têm fatias diferentes: o painel
112
+ * motor traz `easy`/`toggleRun` que isto não lê, e o `PausePlayer` traz o visual e as três do movimento
113
+ * reduzido. Uma fatia mínima é o que deixa os dois passarem sem que nenhum tenha de carregar o do outro.
114
+ * `MotorPlayer` e `PausePlayer` ganharam `walkDir` — quebra declarada, porque o campo é do `PlayerBase` e
115
+ * todo jogador da engine já o tem.
116
+ *
117
+ * 📌 A DEFINIÇÃO MUDOU DE CASA (issue #127) e o NOME fica publicado aqui. Ela vive em `input/latch-sync`, ao
118
+ * lado da regra que a usa, porque a sincronização da aresta toca exactamente estes dois campos — duas cópias
119
+ * do mesmo tipo divergiriam no dia em que a regra ganhasse um terceiro. ⚠️ **Alias e não `export ... from`**:
120
+ * o retrato de nomes deixa re-exports de fora e leria a mudança de casa como remoção, que é a lição da etapa
121
+ * 1a do ADR-0106.
122
+ */
123
+ export type JogadorDaAlternancia = JogadorDaAlternanciaDaAresta;
124
+ /** O que a escrita precisa de saber. Tudo o que está aqui já vive no `SettingsMotorCtx` e no `PauseIconsCtx`. */
125
+ export interface EscritaDaAlternanciaCtx {
126
+ readonly players: readonly JogadorDaAlternancia[];
127
+ readonly store: {
128
+ setBool(key: string, on: boolean): void;
129
+ };
130
+ readonly srSay: (msg: string) => void;
131
+ readonly getNumPlayers: () => number;
132
+ /**
133
+ * QUAL APARELHO ESTE JOGADOR ESTÁ A USAR (ADR-0113). `input/state.entradaDe(i).emUso` é quem responde.
134
+ *
135
+ * ⚠️ OPCIONAL DE PROPÓSITO, e a razão é o que acontece sem ele: a escrita cai exactamente no que já fazia
136
+ * hoje — só a chave por jogador. Torná-lo obrigatório quebraria todo consumidor que constrói este ctx,
137
+ * por causa de uma migração que ainda não terminou, e o `holdsAtOnce` já mostrou o que isso custa.
138
+ *
139
+ * 📌 E é INJECTADO em vez de importado: `ui/` a ler estado de módulo de `input/` é uma aresta nova entre
140
+ * duas camadas, para poupar um argumento. Este ctx já recebe tudo o resto assim.
141
+ */
142
+ readonly transporteEmUso?: (jogador: number) => string;
143
+ }
144
+ /**
145
+ * LIGA OU DESLIGA A ALTERNÂNCIA DE MARCHA DE UM JOGADOR — e agora é a engine que o faz (ADR-0106 §4).
146
+ *
147
+ * ⚠️ O COMENTÁRIO QUE JUSTIFICAVA A INJEÇÃO ARGUMENTAVA CONTRA ELA. Ele dizia: «SHARED setter (also used by
148
+ * the pause-menu quick icon `altmove`) — stays in game.js, injected». Ser partilhado por DUAS superfícies da
149
+ * engine é razão para a engine o possuir, não para o cartucho o guardar — e a medição de 2026-09-08 mostra
150
+ * que cada passo já era da engine: `toggleMove` e `walkDir` são campos do `PlayerBase`, a chave é do
151
+ * `platform/storage`, e `sr.motor.toggleMove*` são chaves i18n da engine. Não sobrava efeito de jogo nenhum,
152
+ * o que faz deste o mais limpo dos sete: aqui não há sequer um efeito colateral a injectar.
153
+ */
154
+ export declare function definirAlternanciaDeMarcha(ctx: EscritaDaAlternanciaCtx, i: number, on: boolean): void;
57
155
  /** Clamps the selected player back to 0 once it falls outside 0..numPlayers-1 (e.g. player count dropped). */
58
156
  export declare function clampSelPlayer(sel: number, numPlayers: number): number;
59
157
  /** Whether ANY player currently uses Modo Fácil or alternância — lights the #opt-movement bar button. */
@@ -3,25 +3,91 @@
3
3
  // renderMovPlayers()/reflectFacil()/reflectAltMove()/setEasy() (~line 2136). Pure logic (per-player tab
4
4
  // clamp/view-model, the "any player active" predicate, the Modo Fácil announcement text) is separated from the
5
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,
6
+ // `store` (platform/storage shape), `players`/`getNumPlayers` (core/state.ts's live state) and `rebuildCoins`
7
+ // (coin subsystem, Modo Fácil puts coins on the ground).
8
+ //
9
+ // ⚠️ `setToggleMove` DEIXOU DE ESTAR NESSA LISTA em 2026-09-08 (ADR-0106 §4, etapa 1b), e o que ela dava como
10
+ // razão era o argumento contrário: dizia que ele «fica fora deste módulo porque é PARTILHADO com outra
11
+ // superfície da interface» — o ícone `altmove` da pausa. Ser partilhado por duas superfícies da ENGINE é razão
12
+ // para a engine o possuir. `definirAlternanciaDeMarcha` mora aqui; o campo do `ctx` ficou OPCIONAL, então
13
+ // quem injecta continua a mandar e quem não injecta deixa de ficar sem ele.
14
+ // Overlay open/close plumbing (frontOverlay, #movement hidden toggle,
10
15
  // Escape handling, renderMapHub) is the SHARED helper used by every settings panel and stays in game.js.
11
16
  import { toggleLabel } from './dom.js';
12
17
  import { t } from '../core/i18n.js';
13
18
  import { markChanged, markMenuChanged } from './changed-mark.js';
14
19
  import { DEFAULTS } from '../core/state.js';
20
+ // ⚠️ IMPORT DIRETO, e não uma peça a mais no `ctx`, pela mesma razão que o `ui/pause-icons` importa
21
+ // `platform/storage`: um nome de chave injetado é um campo que um consumidor pode omitir, e omiti-lo aqui
22
+ // faria o painel escrever num nome torto — que é o defeito que este import acaba de fechar.
23
+ import { KEYS } from '../platform/storage.js';
24
+ import { gravarAlternancia } from '../input/latch-store.js';
25
+ import { aplicarAlternancia, BASE_DA_MARCHA, } from '../input/latch-sync.js';
26
+ import { recusaDaAlternancia } from './latch-refusal.js';
15
27
  // ---------------------------------------------------------------------------------------------
16
28
  // Pure logic (no `document`, testable in node)
17
29
  // ---------------------------------------------------------------------------------------------
18
30
  /** localStorage key for a player's Modo Fácil flag (== platform/storage.ts's `easy_p{i}` pattern). */
19
31
  export function easyKey(i) {
20
- return 'incl_easy_p' + i;
32
+ // ⚠️ ERA A ÚLTIMA CÓPIA DO LITERAL neste ficheiro, e o irmão logo abaixo (`toggleRunKey`) já regista por
33
+ // extenso porque isso é defeito: «duas cópias de um nome mudam uma de cada vez». O `platform/storage` diz o
34
+ // resto — as chaves são funções «para impedir que um deles escreva num nome torto». Passada em 2026-09-08,
35
+ // ao acrescentar o terceiro irmão; um gate afirma agora que os três concordam com o `KEYS`.
36
+ return KEYS.easyP(i);
21
37
  }
22
- /** localStorage key da alternância do botão de CORRER (== `toggleRunP` de platform/storage). */
38
+ /**
39
+ * localStorage key da alternância do botão de CORRER, na forma LEGADA (sem transporte).
40
+ *
41
+ * ⚠️ ERA UMA CÓPIA DO LITERAL, com um comentário ao lado a dizer «== `toggleRunP` de platform/storage» — o
42
+ * que é a admissão do defeito escrita como se fosse documentação. Duas cópias de um nome mudam uma de cada
43
+ * vez, e o `platform/storage` já tinha escrito a razão de as chaves serem funções: «virar função aqui é o
44
+ * que impede que um deles escreva num nome torto». Agora delega, e há um nome só.
45
+ *
46
+ * ⚠️ E É A CHAVE LEGADA. O ADR-0104 §C pôs o TRANSPORTE no nome, porque a alternância é do aparelho e não da
47
+ * pessoa; esta continua a ser lida para herdar o que a criança já tinha, e não é escrita. A chave nova é
48
+ * `chaveDaAlternancia`, em `input/latch-scope`.
49
+ */
23
50
  export function toggleRunKey(i) {
24
- return 'incl_togglerun_p' + i;
51
+ return KEYS.toggleRunP(i);
52
+ }
53
+ /** localStorage key da alternância de MARCHA, por jogador. Delega, como os dois irmãos acima. */
54
+ export function toggleMoveKey(i) {
55
+ return KEYS.toggleMoveP(i);
56
+ }
57
+ /**
58
+ * LIGA OU DESLIGA A ALTERNÂNCIA DE MARCHA DE UM JOGADOR — e agora é a engine que o faz (ADR-0106 §4).
59
+ *
60
+ * ⚠️ O COMENTÁRIO QUE JUSTIFICAVA A INJEÇÃO ARGUMENTAVA CONTRA ELA. Ele dizia: «SHARED setter (also used by
61
+ * the pause-menu quick icon `altmove`) — stays in game.js, injected». Ser partilhado por DUAS superfícies da
62
+ * engine é razão para a engine o possuir, não para o cartucho o guardar — e a medição de 2026-09-08 mostra
63
+ * que cada passo já era da engine: `toggleMove` e `walkDir` são campos do `PlayerBase`, a chave é do
64
+ * `platform/storage`, e `sr.motor.toggleMove*` são chaves i18n da engine. Não sobrava efeito de jogo nenhum,
65
+ * o que faz deste o mais limpo dos sete: aqui não há sequer um efeito colateral a injectar.
66
+ */
67
+ export function definirAlternanciaDeMarcha(ctx, i, on) {
68
+ const p = ctx.players[i];
69
+ if (!p)
70
+ return;
71
+ // 📌 A REGRA DE DESLIGAR MORA NUM SÍTIO SÓ desde a issue #127: `aplicarAlternancia` põe o valor E pára quem
72
+ // anda por travamento. Ela era duas linhas aqui, e passou a ser partilhada com a sincronização da aresta
73
+ // (`input/latch-sync`) — que resolve a MESMA pergunta ao trocar de aparelho. Duas cópias do «senão a
74
+ // personagem anda sozinha» divergiriam no dia em que uma delas mudasse.
75
+ aplicarAlternancia(p, on);
76
+ // ⚠️ AS DUAS CHAVES, E A ANTIGA NÃO SAI AINDA — é a forma do `p.visual` ao lado do `p.viz` (#104 etapa 1a),
77
+ // e pela mesma razão: quem LÊ ainda é o cartucho, por `KEYS.toggleMoveP(i)` (`main.ts:540`). Parar de a
78
+ // escrever agora faria a criança perder a escolha no arranque seguinte — o defeito que o ADR-0113 nomeia
79
+ // como a cláusula que decide se a decisão custa um ajuste real no dia em que sai.
80
+ ctx.store.setBool(toggleMoveKey(i), on);
81
+ // 📌 E a chave NOVA, quando se sabe o aparelho. `gravarAlternancia` recusa-se nos quatro assistidos, onde
82
+ // não há escolha a guardar (ADR-0113 cláusula 3) — e devolve `false` para quem chama desabilitar o
83
+ // controle com o motivo dito. Aqui a recusa não muda mais nada: o valor em memória continua a ser o que
84
+ // a regra resolve, e é ela que responde `true` naqueles quatro.
85
+ const transporte = ctx.transporteEmUso ? ctx.transporteEmUso(i) : null;
86
+ if (transporte)
87
+ gravarAlternancia((chave, ligada) => ctx.store.setBool(chave, ligada), BASE_DA_MARCHA, i, transporte, on);
88
+ // 📌 O ANÚNCIO É INCONDICIONAL, ao contrário do `aplicarAlternancia`, que devolve «mudou». A criança
89
+ // carregou no ícone: calar-se porque o valor já era esse deixaria o botão sem resposta para quem ouve.
90
+ ctx.srSay(playerPrefix(i, ctx.getNumPlayers()) + t(on ? 'sr.motor.toggleMoveOn' : 'sr.motor.toggleMoveOff'));
25
91
  }
26
92
  /** Clamps the selected player back to 0 once it falls outside 0..numPlayers-1 (e.g. player count dropped). */
27
93
  export function clampSelPlayer(sel, numPlayers) {
@@ -59,9 +125,40 @@ export function easyAnnouncement(i, numPlayers, on) {
59
125
  // DOM-facing (thin) — requires `document`/injected ctx
60
126
  // ---------------------------------------------------------------------------------------------
61
127
  export function initSettingsMotor(ctx) {
128
+ // ⚠️ RESOLVIDO UMA VEZ: quem injecta manda, quem não injecta passa a ter. A engine sabe fazê-lo sozinha
129
+ // desde 2026-09-08 — ver definirAlternanciaDeMarcha, e o comentário do campo, que argumentava contra si.
130
+ const setToggleMove = ctx.setToggleMove ?? ((i, on) => definirAlternanciaDeMarcha(ctx, i, on));
62
131
  let selMovPlayer = 0; // jogador selecionado no painel Acessibilidade motora
63
132
  const facilBtn = ctx.$('#opt-facil');
64
133
  const altMoveBtn = ctx.$('#opt-altmove');
134
+ /**
135
+ * A DICA ORIGINAL DA LINHA, guardada uma vez.
136
+ *
137
+ * ⚠️ AQUI A RECUSA VAI E VEM, e é essa a diferença para o precedente. O `render/viz-setters` ACRESCENTA
138
+ * o motivo à dica e nunca o retira, o que é correcto lá: aquela lista é reconstruída a cada render. Este
139
+ * botão é persistente e a criança pode largar a webcam e voltar ao teclado — sem guardar o texto de
140
+ * origem, o motivo acumular-se-ia na linha a cada troca de aparelho.
141
+ */
142
+ const altMoveRow = altMoveBtn?.closest('.ctrl-row') ?? null;
143
+ const altMoveHint = altMoveRow?.querySelector('.opt-hint') ?? null;
144
+ const dicaOriginal = altMoveHint?.textContent ?? '';
145
+ /*
146
+ * ADR-0115 · A LINHA SOME NUM JOGO QUE NÃO SEGURA NADA — e some para TODA A GENTE.
147
+ *
148
+ * 🔴 `hidden` e não `aria-disabled`: a criança que depende da alternância abre este painel para a ligar, e
149
+ * num quiz não há nada para ela ligar. Um controle desabilitado com um motivo continua a ser um controle
150
+ * que não faz nada — e ainda ocupa um lugar na navegação por teclado, entre dois que funcionam.
151
+ * 📌 A cláusula 3 do ADR-0113 é o caso oposto e continua intacta: lá o aparelho EXIGE a alternância, o
152
+ * controle existe, e fica `aria-disabled` COM o motivo, alcançável para que ela possa lê-lo.
153
+ * ⚠️ E a linha é do CARTUCHO: a engine não a criou e por isso não a destrói. `hidden` é reversível e
154
+ * idempotente; remover markup alheio não é nenhuma das duas coisas.
155
+ */
156
+ if (!ctx.seguraTeclas && altMoveRow)
157
+ altMoveRow.hidden = true;
158
+ /** A recusa DESTE jogador agora, ou `null`. Recalculada a cada reflexo: o aparelho em uso muda. */
159
+ function recusaAgora(i) {
160
+ return ctx.transporteEmUso ? recusaDaAlternancia(ctx.transporteEmUso(i)) : null;
161
+ }
65
162
  const toggleRunBtn = ctx.$('#opt-togglerun');
66
163
  // barra acende se QUALQUER jogador usa Fácil/alternância
67
164
  function reflectMovementBtn() {
@@ -122,6 +219,23 @@ export function initSettingsMotor(ctx) {
122
219
  altMoveBtn.classList.toggle('is-on', on);
123
220
  altMoveBtn.setAttribute('aria-pressed', String(on));
124
221
  altMoveBtn.textContent = onOffLabel(on);
222
+ /*
223
+ * ⚠️ A CLÁUSULA 3 DO ADR-0113 NA TELA: onde a alternância é exigida, o controle NÃO SOME — fica
224
+ * `aria-disabled` e o motivo entra na dica, que a casca (`ui/settings-panel.fillExplain`) move para o
225
+ * rodapé. Sumir ensinaria que a coisa não existe; deixá-lo activo faria a criança carregar e não
226
+ * perceber por que nada mudou.
227
+ *
228
+ * 📌 E `aria-disabled` e não `disabled`: um botão desabilitado de verdade SAI da ordem de tabulação, e
229
+ * quem navega por teclado deixaria de o alcançar — logo deixaria de poder LER o motivo. É a mesma
230
+ * escolha que a #128 nomeia como defeito quando é feita ao contrário (só classe CSS, sem `aria`).
231
+ */
232
+ const recusa = recusaAgora(selMovPlayer);
233
+ if (recusa)
234
+ altMoveBtn.setAttribute('aria-disabled', 'true');
235
+ else
236
+ altMoveBtn.removeAttribute('aria-disabled');
237
+ if (altMoveHint)
238
+ altMoveHint.textContent = recusa ? `${dicaOriginal} ${t(recusa.chave)}`.trim() : dicaOriginal;
125
239
  }
126
240
  reflectMovementBtn();
127
241
  }
@@ -151,13 +265,26 @@ export function initSettingsMotor(ctx) {
151
265
  reflectAltMove();
152
266
  });
153
267
  });
268
+ // A prosa volta para o rodapé depois de as linhas serem reconstruídas (CLAUDE.md §4, #109).
269
+ ctx.fillExplain?.(ctx.$('#movement .overlay__card'));
154
270
  }
155
271
  if (facilBtn) {
156
272
  facilBtn.addEventListener('click', () => setEasy(selMovPlayer, !ctx.players[selMovPlayer].easy));
157
273
  }
158
274
  if (altMoveBtn) {
159
275
  altMoveBtn.addEventListener('click', () => {
160
- ctx.setToggleMove(selMovPlayer, !ctx.players[selMovPlayer].toggleMove);
276
+ /*
277
+ * ⚠️ RECUSAR DIZENDO, E NÃO EM SILÊNCIO. O precedente (`render/viz-setters`) resolve isto não ligando
278
+ * ouvinte nenhum — pode, porque reconstrói a lista a cada render. Aqui o ouvinte é ligado uma vez, e
279
+ * um `return` mudo seria «aceitar o clique e ignorá-lo», que é a outra metade do que o ADR-0076
280
+ * proíbe. Então a recusa FALA: quem carregou fica a saber por quê, mesmo sem ver a dica.
281
+ */
282
+ const recusa = recusaAgora(selMovPlayer);
283
+ if (recusa) {
284
+ ctx.srSay(t(recusa.chave));
285
+ return;
286
+ }
287
+ setToggleMove(selMovPlayer, !ctx.players[selMovPlayer].toggleMove);
161
288
  reflectAltMove();
162
289
  });
163
290
  }
@@ -191,7 +318,7 @@ export function initSettingsMotor(ctx) {
191
318
  if (p.easy)
192
319
  setEasy(i, false);
193
320
  if (p.toggleMove)
194
- ctx.setToggleMove(i, false);
321
+ setToggleMove(i, false);
195
322
  if (p.toggleRun)
196
323
  ctx.setToggleRun(i, false);
197
324
  });
@@ -29,6 +29,20 @@ export interface SettingsTypoCtx {
29
29
  * continua desenhando. O que ele não pode é desenhar prosa duplicada, e sem casca não há rodapé para duplicar.
30
30
  */
31
31
  fillExplain?: (card: HTMLElement | null) => void;
32
+ /**
33
+ * ESTA FAMÍLIA ESTÁ INSTALADA NO APARELHO? — em produção, `(f) => doc.fonts.check(\`16px "${f}"\`)`.
34
+ *
35
+ * É o que transforma o «enquanto» do ADR-0012 em código: a opção da Ronde fica desabilitada ENQUANTO
36
+ * nenhuma das três faces estiver presente, e volta a ficar disponível quando o adulto instalar uma.
37
+ *
38
+ * ⚠️ INJECTADO E NUNCA `document.fonts` LIDO AQUI, pela regra que este ficheiro já segue para o `$`: um
39
+ * global do navegador num módulo que corre em node é o ACHADO 15, e este projecto já pagou por ele com um
40
+ * boot rebentado.
41
+ * 📌 OPCIONAL, e aqui o padrão é mesmo seguro — ao contrário do `seguraTeclas` do `PauseIconsCtx`, onde os
42
+ * dois lados erravam. Sem detector a linha fica desabilitada COM a mensagem, e a mensagem diz ao adulto as
43
+ * três fontes que resolvem. O estado por omissão é o de hoje, e é accionável.
44
+ */
45
+ fonteInstalada?: (familia: string) => boolean;
32
46
  }
33
47
  export interface SettingsTypoApi {
34
48
  /** Re-renders #typo-list from current selection and (re)wires its buttons. Idempotent. */
@@ -38,8 +52,16 @@ export interface SettingsTypoApi {
38
52
  /** Currently selected font key. */
39
53
  getFontKey: () => string;
40
54
  }
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;
55
+ /**
56
+ * A key is selectable when it exists in the catalog, is not marked `.off` (licence pending, etc.) — and is
57
+ * `geral`.
58
+ *
59
+ * ⚠️ O TERCEIRO TERMO ENTROU EM 2026-09-07 (issue #87), e sem ele o resto da mudança seria decoração: o menu
60
+ * deixaria de OFERECER as caligráficas e elas continuariam SELECIONÁVEIS por qualquer outro caminho — o
61
+ * `resolveFontKey` de uma chave guardada, um `data-font` num markup de consumidor. «Não está na lista» e «não
62
+ * pode ser escolhida» têm de ser a mesma afirmação, ou a lista é só uma sugestão.
63
+ */
64
+ export declare function isSelectableFont(k: string, instalada?: (familia: string) => boolean): boolean;
43
65
  export { resolveFontKey, persistFontKey } from './fonts.js';
44
66
  import type { DomQuery } from '../core/dom-query.js';
45
67
  export interface FontCssTarget {
@@ -67,8 +89,30 @@ export interface TypoGroupView {
67
89
  g: string;
68
90
  rows: TypoRow[];
69
91
  }
92
+ /**
93
+ * UMA linha da lista, a partir de uma face do catálogo. Extraída em 2026-09-07 (issue #87) porque o gate do
94
+ * mecanismo `.off` precisava de a exercitar com uma face de MENTIRA: as duas únicas entradas desligadas
95
+ * saíram do roster, e um caso que dependa da composição do catálogo reprova sempre que o roster muda.
96
+ *
97
+ * O mecanismo fica, e é preciso: o item 4 da #87 usa-o para a **Ronde**, que só pode ser oferecida se uma de
98
+ * três faces estiver instalada, porque duas delas são gratuitas apenas para uso pessoal e não podem ser
99
+ * empacotadas.
100
+ */
101
+ export declare function linhaDaFonte(it: FontItem, fontKey: string, instalada?: (familia: string) => boolean): TypoRow;
70
102
  /** 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;
103
+ export declare function typoGroups(fontKey: string, instalada?: (familia: string) => boolean): TypoGroupView[];
104
+ /**
105
+ * Full innerHTML for #typo-list, given the currently selected key. Pure string building — no DOM.
106
+ *
107
+ * ⚠️ É UM GRUPO DE RÁDIO SÓ, ATRAVESSANDO AS TRÊS SECÇÕES, e isso é a decisão e não um detalhe de
108
+ * marcação: a exclusividade é do MENU inteiro — uma fonte activa no total —, não de cada família. Três
109
+ * grupos diriam a quem escuta que dá para ter uma sans E uma serif ao mesmo tempo.
110
+ *
111
+ * ⚠️ E ISTO DEIXOU DE SER UM INTERRUPTOR EM 2026-09-07. Era `class="mode-btn switch"` + `aria-pressed` +
112
+ * `toggleLabel()`, ou seja dezassete interruptores independentes anunciando «Ligado»/«Desligado» para
113
+ * escolher UMA fonte. A emenda do ADR-0012 diz o contrário em tantas palavras: «THE MENU IS A CHOICE, NOT
114
+ * A TOGGLE […] One font is active; the others are alternatives, not switches.» O `switch` do
115
+ * `style.css:427` desenha uma chave de 52×28 px com bolinha — era o desenho de um estado que não existe.
116
+ */
117
+ export declare function typoListHTML(fontKey: string, instalada?: (familia: string) => boolean): string;
74
118
  export declare function initSettingsTypo(ctx: SettingsTypoCtx): SettingsTypoApi;