@the-inclusionist/engine 7.0.0 → 8.0.0-rc.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.
- package/README.md +5 -3
- package/app/css/style.css +22 -5
- package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
- package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
- package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
- package/app/public/vendor/fonts.css +43 -2
- package/dist-pkg/boot/create-game.d.ts +76 -2
- package/dist-pkg/boot/create-game.js +271 -13
- package/dist-pkg/core/constants.d.ts +0 -28
- package/dist-pkg/core/constants.js +34 -17
- package/dist-pkg/core/contract.d.ts +39 -0
- package/dist-pkg/core/contract.js +36 -0
- package/dist-pkg/core/entity.d.ts +47 -4
- package/dist-pkg/core/layers.d.ts +18 -0
- package/dist-pkg/core/layers.js +18 -0
- package/dist-pkg/core/rng.js +14 -4
- package/dist-pkg/core/route.d.ts +42 -0
- package/dist-pkg/core/route.js +158 -0
- package/dist-pkg/core/state.d.ts +10 -1
- package/dist-pkg/core/state.js +13 -0
- package/dist-pkg/educational/adaptive-engine.d.ts +65 -0
- package/dist-pkg/educational/adaptive-engine.js +117 -0
- package/dist-pkg/educational/segment-bar.d.ts +96 -0
- package/dist-pkg/educational/segment-bar.js +89 -0
- package/dist-pkg/i18n/en.js +33 -0
- package/dist-pkg/i18n/es.js +33 -0
- package/dist-pkg/i18n/pt.js +46 -0
- package/dist-pkg/input/default-bindings.d.ts +40 -0
- package/dist-pkg/input/default-bindings.js +143 -9
- package/dist-pkg/input/gamepad.d.ts +18 -11
- package/dist-pkg/input/gamepad.js +75 -9
- package/dist-pkg/input/keyboard-runtime.d.ts +6 -8
- package/dist-pkg/input/keyboard-runtime.js +24 -5
- package/dist-pkg/input/keyboard.d.ts +10 -0
- package/dist-pkg/input/keyboard.js +41 -11
- package/dist-pkg/input/keydown.d.ts +27 -2
- package/dist-pkg/input/keydown.js +20 -6
- package/dist-pkg/input/latch-scope.d.ts +70 -0
- package/dist-pkg/input/latch-scope.js +110 -0
- package/dist-pkg/input/latch-store.d.ts +45 -0
- package/dist-pkg/input/latch-store.js +74 -0
- package/dist-pkg/input/origem-sintetica.d.ts +44 -0
- package/dist-pkg/input/origem-sintetica.js +60 -0
- package/dist-pkg/input/pointer.d.ts +61 -0
- package/dist-pkg/input/pointer.js +71 -0
- package/dist-pkg/input/state.d.ts +86 -1
- package/dist-pkg/input/state.js +128 -1
- package/dist-pkg/input/touch-bindings.d.ts +11 -2
- package/dist-pkg/input/touch-bindings.js +9 -3
- package/dist-pkg/input/touch.js +9 -1
- package/dist-pkg/input/transporte-em-uso.d.ts +101 -0
- package/dist-pkg/input/transporte-em-uso.js +130 -0
- package/dist-pkg/input/transports.d.ts +88 -2
- package/dist-pkg/input/transports.js +60 -5
- package/dist-pkg/input/vocabulary-migration.d.ts +18 -7
- package/dist-pkg/platform/audio-earcons.d.ts +29 -2
- package/dist-pkg/platform/audio-earcons.js +10 -0
- package/dist-pkg/platform/audio-nav.d.ts +1 -1
- package/dist-pkg/platform/audio-sonar.d.ts +149 -9
- package/dist-pkg/platform/audio-sonar.js +232 -21
- package/dist-pkg/platform/guide-intensity.d.ts +40 -0
- package/dist-pkg/platform/guide-intensity.js +73 -0
- package/dist-pkg/platform/storage.d.ts +30 -0
- package/dist-pkg/platform/storage.js +35 -0
- package/dist-pkg/platform/tts.js +16 -1
- package/dist-pkg/platform/voice-plan.d.ts +90 -0
- package/dist-pkg/platform/voice-plan.js +137 -0
- package/dist-pkg/render/draw.d.ts +1 -1
- package/dist-pkg/render/draw.js +15 -5
- package/dist-pkg/render/viz-axes.d.ts +17 -0
- package/dist-pkg/render/viz-axes.js +19 -0
- package/dist-pkg/render/viz-setters.d.ts +34 -2
- package/dist-pkg/render/viz-setters.js +189 -33
- package/dist-pkg/render/wheelchair-sprites.d.ts +11 -3
- package/dist-pkg/render/wheelchair-sprites.js +10 -4
- package/dist-pkg/ui/dom.js +20 -2
- package/dist-pkg/ui/fonts.d.ts +47 -1
- package/dist-pkg/ui/fonts.js +47 -9
- package/dist-pkg/ui/latch-refusal.d.ts +32 -0
- package/dist-pkg/ui/latch-refusal.js +60 -0
- package/dist-pkg/ui/layout.d.ts +32 -0
- package/dist-pkg/ui/layout.js +64 -1
- package/dist-pkg/ui/motion-scene.d.ts +41 -0
- package/dist-pkg/ui/motion-scene.js +76 -0
- package/dist-pkg/ui/panel-shell.d.ts +53 -0
- package/dist-pkg/ui/panel-shell.js +103 -0
- package/dist-pkg/ui/pause-icons.d.ts +134 -20
- package/dist-pkg/ui/pause-icons.js +307 -45
- package/dist-pkg/ui/reach-notice.js +8 -0
- package/dist-pkg/ui/settings-audio.d.ts +14 -1
- package/dist-pkg/ui/settings-audio.js +43 -3
- package/dist-pkg/ui/settings-controls.d.ts +66 -6
- package/dist-pkg/ui/settings-controls.js +179 -17
- package/dist-pkg/ui/settings-empathy.d.ts +15 -0
- package/dist-pkg/ui/settings-empathy.js +2 -0
- package/dist-pkg/ui/settings-motion.d.ts +25 -19
- package/dist-pkg/ui/settings-motion.js +32 -19
- package/dist-pkg/ui/settings-motor.d.ts +81 -3
- package/dist-pkg/ui/settings-motor.js +118 -9
- package/dist-pkg/ui/settings-typo.d.ts +32 -2
- package/dist-pkg/ui/settings-typo.js +64 -18
- package/dist-pkg/ui/settings-visual.d.ts +21 -1
- package/dist-pkg/ui/settings-visual.js +48 -4
- package/dist-pkg/ui/shell.d.ts +13 -3
- package/dist-pkg/ui/shell.js +3 -4
- package/dist-pkg/ui/simulation-refusal.d.ts +32 -0
- package/dist-pkg/ui/simulation-refusal.js +57 -0
- package/dist-pkg/ui/visual-axes-panel.d.ts +48 -0
- package/dist-pkg/ui/visual-axes-panel.js +95 -0
- package/dist-pkg/ui/webcam.js +6 -1
- package/docs/CREDITS.md +18 -0
- package/docs/LICENSES.md +12 -0
- package/package.json +24 -4
- package/app/public/vendor/fonts/greatvibes-400.woff2 +0 -0
- 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',
|
|
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(
|
|
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(
|
|
149
|
-
const sceneRows = buildSceneRowsHtml(
|
|
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
|
-
|
|
178
|
-
|
|
190
|
+
rm[k] = !rm[k];
|
|
191
|
+
saveRM();
|
|
179
192
|
render();
|
|
180
193
|
updateMotionMaster();
|
|
181
|
-
ctx.srSay(sceneMotionAnnouncement(t(RM_LABEL[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
|
|
216
|
+
for (const c of rmChar)
|
|
204
217
|
marcar(`[data-rmc="${c.prop}"]`, !!(player && player[c.prop]) !== padraoRm);
|
|
205
|
-
for (const k of
|
|
206
|
-
marcar(`[data-rm="${k}"]`, !!
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
for (const k of rmKeys)
|
|
239
|
+
rm[k] = padraoRm;
|
|
240
|
+
saveRM();
|
|
228
241
|
ctx.getPlayers().forEach((p, i) => {
|
|
229
|
-
for (const c of
|
|
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(
|
|
280
|
+
const allFrozen = allMotionFrozen(rmKeys, rm, rmChar, player);
|
|
268
281
|
const next = !allFrozen;
|
|
269
|
-
for (const k of
|
|
270
|
-
|
|
271
|
-
|
|
282
|
+
for (const k of rmKeys)
|
|
283
|
+
rm[k] = next;
|
|
284
|
+
saveRM();
|
|
272
285
|
if (player)
|
|
273
|
-
for (const c of
|
|
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
|
}
|
|
@@ -12,7 +12,7 @@ export interface MotorStore {
|
|
|
12
12
|
}
|
|
13
13
|
/** Minimal per-player shape this module reads/writes (core/state.ts's `players` entries carry much more). */
|
|
14
14
|
/** As duas escolhas motoras por jogador: modo Fácil e teclas de alternância. */
|
|
15
|
-
export type MotorPlayer = PlayerView<'easy' | 'toggleMove' | 'toggleRun'>;
|
|
15
|
+
export type MotorPlayer = PlayerView<'easy' | 'toggleMove' | 'toggleRun' | 'walkDir'>;
|
|
16
16
|
export interface SettingsMotorCtx {
|
|
17
17
|
/** DOM selector (querySelector), injected — never reaches `document` globally. */
|
|
18
18
|
$: DomQuery;
|
|
@@ -25,7 +25,14 @@ export interface SettingsMotorCtx {
|
|
|
25
25
|
/** Live player count (core/state.ts's `numPlayers`); a getter because the value is reassigned over time. */
|
|
26
26
|
getNumPlayers: () => number;
|
|
27
27
|
/** SHARED setter (also used by the pause-menu quick icon `altmove`) — stays in game.js, injected. */
|
|
28
|
-
setToggleMove
|
|
28
|
+
setToggleMove?: (i: number, on: boolean) => void;
|
|
29
|
+
/**
|
|
30
|
+
* QUAL APARELHO ESTE JOGADOR ESTÁ A USAR (ADR-0113) — atravessa daqui para a escrita.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ Opcional pela mesma razão que na `EscritaDaAlternanciaCtx`: sem ele a escrita cai no que já fazia,
|
|
33
|
+
* e exigi-lo quebraria todo consumidor por causa de uma migração a meio.
|
|
34
|
+
*/
|
|
35
|
+
transporteEmUso?: (jogador: number) => string;
|
|
29
36
|
/**
|
|
30
37
|
* A ALTERNÂNCIA DO BOTÃO DE CORRER. Injetada como a irmã acima e pelo mesmo motivo: quem persiste e anuncia
|
|
31
38
|
* é a raiz de composição, que é quem conhece `players` e o armazenamento.
|
|
@@ -33,6 +40,19 @@ export interface SettingsMotorCtx {
|
|
|
33
40
|
setToggleRun: (i: number, on: boolean) => void;
|
|
34
41
|
/** Coin layout depends on any player's Modo Fácil (moedas no chão) — owned by the coin subsystem, injected. */
|
|
35
42
|
rebuildCoins: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* Move a prosa das linhas para o rodapé (`ui/settings-panel` → `fillExplain`). Chamado a CADA render.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ NÃO É OPCIONAL POR ELEGÂNCIA: `fillExplain` roda uma vez quando o overlay é frontalizado e move o
|
|
47
|
+
* `.opt-hint` de dentro de cada linha para o rodapé. Este painel RECONSTRÓI as linhas, e as linhas novas
|
|
48
|
+
* voltam com a prosa lá dentro — então a explicação aparece duas vezes, no rodapé e sob o rótulo, a
|
|
49
|
+
* partir do primeiro clique. O `CLAUDE.md` §4 regista exatamente isto, e a issue #109 já o consertou
|
|
50
|
+
* uma vez noutros painéis.
|
|
51
|
+
*
|
|
52
|
+
* Opcional na assinatura porque um consumidor pode montar o painel sem a casca (um teste, o segundo
|
|
53
|
+
* consumidor): sem casca não há rodapé para duplicar.
|
|
54
|
+
*/
|
|
55
|
+
fillExplain?: (card: HTMLElement | null) => void;
|
|
36
56
|
}
|
|
37
57
|
export interface SettingsMotorApi {
|
|
38
58
|
/** Re-renders #movement-players (kept `hidden`, per E3 — see playerTabsHTML) and (re)wires its buttons. */
|
|
@@ -52,8 +72,66 @@ export interface SettingsMotorApi {
|
|
|
52
72
|
}
|
|
53
73
|
/** localStorage key for a player's Modo Fácil flag (== platform/storage.ts's `easy_p{i}` pattern). */
|
|
54
74
|
export declare function easyKey(i: number): string;
|
|
55
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* localStorage key da alternância do botão de CORRER, na forma LEGADA (sem transporte).
|
|
77
|
+
*
|
|
78
|
+
* ⚠️ ERA UMA CÓPIA DO LITERAL, com um comentário ao lado a dizer «== `toggleRunP` de platform/storage» — o
|
|
79
|
+
* que é a admissão do defeito escrita como se fosse documentação. Duas cópias de um nome mudam uma de cada
|
|
80
|
+
* vez, e o `platform/storage` já tinha escrito a razão de as chaves serem funções: «virar função aqui é o
|
|
81
|
+
* que impede que um deles escreva num nome torto». Agora delega, e há um nome só.
|
|
82
|
+
*
|
|
83
|
+
* ⚠️ E É A CHAVE LEGADA. O ADR-0104 §C pôs o TRANSPORTE no nome, porque a alternância é do aparelho e não da
|
|
84
|
+
* pessoa; esta continua a ser lida para herdar o que a criança já tinha, e não é escrita. A chave nova é
|
|
85
|
+
* `chaveDaAlternancia`, em `input/latch-scope`.
|
|
86
|
+
*/
|
|
56
87
|
export declare function toggleRunKey(i: number): string;
|
|
88
|
+
/** localStorage key da alternância de MARCHA, por jogador. Delega, como os dois irmãos acima. */
|
|
89
|
+
export declare function toggleMoveKey(i: number): string;
|
|
90
|
+
/**
|
|
91
|
+
* A fatia mínima que a escrita da alternância de marcha toca.
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ `walkDir` ENTRA, e não é detalhe: desligar a alternância tem de PARAR quem está a andar por travamento.
|
|
94
|
+
* Sem isso, a criança desliga o modo e a personagem continua a andar sozinha, sem tecla nenhuma premida —
|
|
95
|
+
* e não há erro nenhum a dizê-lo.
|
|
96
|
+
*
|
|
97
|
+
* ⚠️ E É FATIA PRÓPRIA, e não o `MotorPlayer`, porque os DOIS chamadores têm fatias diferentes: o painel
|
|
98
|
+
* motor traz `easy`/`toggleRun` que isto não lê, e o `PausePlayer` traz o visual e as três do movimento
|
|
99
|
+
* reduzido. Uma fatia mínima é o que deixa os dois passarem sem que nenhum tenha de carregar o do outro.
|
|
100
|
+
* `MotorPlayer` e `PausePlayer` ganharam `walkDir` — quebra declarada, porque o campo é do `PlayerBase` e
|
|
101
|
+
* todo jogador da engine já o tem.
|
|
102
|
+
*/
|
|
103
|
+
export type JogadorDaAlternancia = PlayerView<'toggleMove' | 'walkDir'>;
|
|
104
|
+
/** O que a escrita precisa de saber. Tudo o que está aqui já vive no `SettingsMotorCtx` e no `PauseIconsCtx`. */
|
|
105
|
+
export interface EscritaDaAlternanciaCtx {
|
|
106
|
+
readonly players: readonly JogadorDaAlternancia[];
|
|
107
|
+
readonly store: {
|
|
108
|
+
setBool(key: string, on: boolean): void;
|
|
109
|
+
};
|
|
110
|
+
readonly srSay: (msg: string) => void;
|
|
111
|
+
readonly getNumPlayers: () => number;
|
|
112
|
+
/**
|
|
113
|
+
* QUAL APARELHO ESTE JOGADOR ESTÁ A USAR (ADR-0113). `input/state.entradaDe(i).emUso` é quem responde.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ OPCIONAL DE PROPÓSITO, e a razão é o que acontece sem ele: a escrita cai exactamente no que já fazia
|
|
116
|
+
* hoje — só a chave por jogador. Torná-lo obrigatório quebraria todo consumidor que constrói este ctx,
|
|
117
|
+
* por causa de uma migração que ainda não terminou, e o `holdsAtOnce` já mostrou o que isso custa.
|
|
118
|
+
*
|
|
119
|
+
* 📌 E é INJECTADO em vez de importado: `ui/` a ler estado de módulo de `input/` é uma aresta nova entre
|
|
120
|
+
* duas camadas, para poupar um argumento. Este ctx já recebe tudo o resto assim.
|
|
121
|
+
*/
|
|
122
|
+
readonly transporteEmUso?: (jogador: number) => string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* LIGA OU DESLIGA A ALTERNÂNCIA DE MARCHA DE UM JOGADOR — e agora é a engine que o faz (ADR-0106 §4).
|
|
126
|
+
*
|
|
127
|
+
* ⚠️ O COMENTÁRIO QUE JUSTIFICAVA A INJEÇÃO ARGUMENTAVA CONTRA ELA. Ele dizia: «SHARED setter (also used by
|
|
128
|
+
* the pause-menu quick icon `altmove`) — stays in game.js, injected». Ser partilhado por DUAS superfícies da
|
|
129
|
+
* engine é razão para a engine o possuir, não para o cartucho o guardar — e a medição de 2026-09-08 mostra
|
|
130
|
+
* que cada passo já era da engine: `toggleMove` e `walkDir` são campos do `PlayerBase`, a chave é do
|
|
131
|
+
* `platform/storage`, e `sr.motor.toggleMove*` são chaves i18n da engine. Não sobrava efeito de jogo nenhum,
|
|
132
|
+
* o que faz deste o mais limpo dos sete: aqui não há sequer um efeito colateral a injectar.
|
|
133
|
+
*/
|
|
134
|
+
export declare function definirAlternanciaDeMarcha(ctx: EscritaDaAlternanciaCtx, i: number, on: boolean): void;
|
|
57
135
|
/** Clamps the selected player back to 0 once it falls outside 0..numPlayers-1 (e.g. player count dropped). */
|
|
58
136
|
export declare function clampSelPlayer(sel: number, numPlayers: number): number;
|
|
59
137
|
/** Whether ANY player currently uses Modo Fácil or alternância — lights the #opt-movement bar button. */
|
|
@@ -3,25 +3,86 @@
|
|
|
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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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 { recusaDaAlternancia } from './latch-refusal.js';
|
|
15
26
|
// ---------------------------------------------------------------------------------------------
|
|
16
27
|
// Pure logic (no `document`, testable in node)
|
|
17
28
|
// ---------------------------------------------------------------------------------------------
|
|
18
29
|
/** localStorage key for a player's Modo Fácil flag (== platform/storage.ts's `easy_p{i}` pattern). */
|
|
19
30
|
export function easyKey(i) {
|
|
20
|
-
|
|
31
|
+
// ⚠️ ERA A ÚLTIMA CÓPIA DO LITERAL neste ficheiro, e o irmão logo abaixo (`toggleRunKey`) já regista por
|
|
32
|
+
// extenso porque isso é defeito: «duas cópias de um nome mudam uma de cada vez». O `platform/storage` diz o
|
|
33
|
+
// resto — as chaves são funções «para impedir que um deles escreva num nome torto». Passada em 2026-09-08,
|
|
34
|
+
// ao acrescentar o terceiro irmão; um gate afirma agora que os três concordam com o `KEYS`.
|
|
35
|
+
return KEYS.easyP(i);
|
|
21
36
|
}
|
|
22
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* localStorage key da alternância do botão de CORRER, na forma LEGADA (sem transporte).
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ ERA UMA CÓPIA DO LITERAL, com um comentário ao lado a dizer «== `toggleRunP` de platform/storage» — o
|
|
41
|
+
* que é a admissão do defeito escrita como se fosse documentação. Duas cópias de um nome mudam uma de cada
|
|
42
|
+
* vez, e o `platform/storage` já tinha escrito a razão de as chaves serem funções: «virar função aqui é o
|
|
43
|
+
* que impede que um deles escreva num nome torto». Agora delega, e há um nome só.
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ E É A CHAVE LEGADA. O ADR-0104 §C pôs o TRANSPORTE no nome, porque a alternância é do aparelho e não da
|
|
46
|
+
* pessoa; esta continua a ser lida para herdar o que a criança já tinha, e não é escrita. A chave nova é
|
|
47
|
+
* `chaveDaAlternancia`, em `input/latch-scope`.
|
|
48
|
+
*/
|
|
23
49
|
export function toggleRunKey(i) {
|
|
24
|
-
return
|
|
50
|
+
return KEYS.toggleRunP(i);
|
|
51
|
+
}
|
|
52
|
+
/** localStorage key da alternância de MARCHA, por jogador. Delega, como os dois irmãos acima. */
|
|
53
|
+
export function toggleMoveKey(i) {
|
|
54
|
+
return KEYS.toggleMoveP(i);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* LIGA OU DESLIGA A ALTERNÂNCIA DE MARCHA DE UM JOGADOR — e agora é a engine que o faz (ADR-0106 §4).
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ O COMENTÁRIO QUE JUSTIFICAVA A INJEÇÃO ARGUMENTAVA CONTRA ELA. Ele dizia: «SHARED setter (also used by
|
|
60
|
+
* the pause-menu quick icon `altmove`) — stays in game.js, injected». Ser partilhado por DUAS superfícies da
|
|
61
|
+
* engine é razão para a engine o possuir, não para o cartucho o guardar — e a medição de 2026-09-08 mostra
|
|
62
|
+
* que cada passo já era da engine: `toggleMove` e `walkDir` são campos do `PlayerBase`, a chave é do
|
|
63
|
+
* `platform/storage`, e `sr.motor.toggleMove*` são chaves i18n da engine. Não sobrava efeito de jogo nenhum,
|
|
64
|
+
* o que faz deste o mais limpo dos sete: aqui não há sequer um efeito colateral a injectar.
|
|
65
|
+
*/
|
|
66
|
+
export function definirAlternanciaDeMarcha(ctx, i, on) {
|
|
67
|
+
const p = ctx.players[i];
|
|
68
|
+
if (!p)
|
|
69
|
+
return;
|
|
70
|
+
p.toggleMove = on;
|
|
71
|
+
// ⚠️ AS DUAS CHAVES, E A ANTIGA NÃO SAI AINDA — é a forma do `p.visual` ao lado do `p.viz` (#104 etapa 1a),
|
|
72
|
+
// e pela mesma razão: quem LÊ ainda é o cartucho, por `KEYS.toggleMoveP(i)` (`main.ts:540`). Parar de a
|
|
73
|
+
// escrever agora faria a criança perder a escolha no arranque seguinte — o defeito que o ADR-0113 nomeia
|
|
74
|
+
// como a cláusula que decide se a decisão custa um ajuste real no dia em que sai.
|
|
75
|
+
ctx.store.setBool(toggleMoveKey(i), on);
|
|
76
|
+
// 📌 E a chave NOVA, quando se sabe o aparelho. `gravarAlternancia` recusa-se nos quatro assistidos, onde
|
|
77
|
+
// não há escolha a guardar (ADR-0113 cláusula 3) — e devolve `false` para quem chama desabilitar o
|
|
78
|
+
// controle com o motivo dito. Aqui a recusa não muda mais nada: o valor em memória continua a ser o que
|
|
79
|
+
// a regra resolve, e é ela que responde `true` naqueles quatro.
|
|
80
|
+
const transporte = ctx.transporteEmUso ? ctx.transporteEmUso(i) : null;
|
|
81
|
+
if (transporte)
|
|
82
|
+
gravarAlternancia((chave, ligada) => ctx.store.setBool(chave, ligada), 'togglemove', i, transporte, on);
|
|
83
|
+
if (!on)
|
|
84
|
+
p.walkDir = 0;
|
|
85
|
+
ctx.srSay(playerPrefix(i, ctx.getNumPlayers()) + t(on ? 'sr.motor.toggleMoveOn' : 'sr.motor.toggleMoveOff'));
|
|
25
86
|
}
|
|
26
87
|
/** Clamps the selected player back to 0 once it falls outside 0..numPlayers-1 (e.g. player count dropped). */
|
|
27
88
|
export function clampSelPlayer(sel, numPlayers) {
|
|
@@ -59,9 +120,27 @@ export function easyAnnouncement(i, numPlayers, on) {
|
|
|
59
120
|
// DOM-facing (thin) — requires `document`/injected ctx
|
|
60
121
|
// ---------------------------------------------------------------------------------------------
|
|
61
122
|
export function initSettingsMotor(ctx) {
|
|
123
|
+
// ⚠️ RESOLVIDO UMA VEZ: quem injecta manda, quem não injecta passa a ter. A engine sabe fazê-lo sozinha
|
|
124
|
+
// desde 2026-09-08 — ver definirAlternanciaDeMarcha, e o comentário do campo, que argumentava contra si.
|
|
125
|
+
const setToggleMove = ctx.setToggleMove ?? ((i, on) => definirAlternanciaDeMarcha(ctx, i, on));
|
|
62
126
|
let selMovPlayer = 0; // jogador selecionado no painel Acessibilidade motora
|
|
63
127
|
const facilBtn = ctx.$('#opt-facil');
|
|
64
128
|
const altMoveBtn = ctx.$('#opt-altmove');
|
|
129
|
+
/**
|
|
130
|
+
* A DICA ORIGINAL DA LINHA, guardada uma vez.
|
|
131
|
+
*
|
|
132
|
+
* ⚠️ AQUI A RECUSA VAI E VEM, e é essa a diferença para o precedente. O `render/viz-setters` ACRESCENTA
|
|
133
|
+
* o motivo à dica e nunca o retira, o que é correcto lá: aquela lista é reconstruída a cada render. Este
|
|
134
|
+
* botão é persistente e a criança pode largar a webcam e voltar ao teclado — sem guardar o texto de
|
|
135
|
+
* origem, o motivo acumular-se-ia na linha a cada troca de aparelho.
|
|
136
|
+
*/
|
|
137
|
+
const altMoveRow = altMoveBtn?.closest('.ctrl-row') ?? null;
|
|
138
|
+
const altMoveHint = altMoveRow?.querySelector('.opt-hint') ?? null;
|
|
139
|
+
const dicaOriginal = altMoveHint?.textContent ?? '';
|
|
140
|
+
/** A recusa DESTE jogador agora, ou `null`. Recalculada a cada reflexo: o aparelho em uso muda. */
|
|
141
|
+
function recusaAgora(i) {
|
|
142
|
+
return ctx.transporteEmUso ? recusaDaAlternancia(ctx.transporteEmUso(i)) : null;
|
|
143
|
+
}
|
|
65
144
|
const toggleRunBtn = ctx.$('#opt-togglerun');
|
|
66
145
|
// barra acende se QUALQUER jogador usa Fácil/alternância
|
|
67
146
|
function reflectMovementBtn() {
|
|
@@ -122,6 +201,23 @@ export function initSettingsMotor(ctx) {
|
|
|
122
201
|
altMoveBtn.classList.toggle('is-on', on);
|
|
123
202
|
altMoveBtn.setAttribute('aria-pressed', String(on));
|
|
124
203
|
altMoveBtn.textContent = onOffLabel(on);
|
|
204
|
+
/*
|
|
205
|
+
* ⚠️ A CLÁUSULA 3 DO ADR-0113 NA TELA: onde a alternância é exigida, o controle NÃO SOME — fica
|
|
206
|
+
* `aria-disabled` e o motivo entra na dica, que a casca (`ui/settings-panel.fillExplain`) move para o
|
|
207
|
+
* rodapé. Sumir ensinaria que a coisa não existe; deixá-lo activo faria a criança carregar e não
|
|
208
|
+
* perceber por que nada mudou.
|
|
209
|
+
*
|
|
210
|
+
* 📌 E `aria-disabled` e não `disabled`: um botão desabilitado de verdade SAI da ordem de tabulação, e
|
|
211
|
+
* quem navega por teclado deixaria de o alcançar — logo deixaria de poder LER o motivo. É a mesma
|
|
212
|
+
* escolha que a #128 nomeia como defeito quando é feita ao contrário (só classe CSS, sem `aria`).
|
|
213
|
+
*/
|
|
214
|
+
const recusa = recusaAgora(selMovPlayer);
|
|
215
|
+
if (recusa)
|
|
216
|
+
altMoveBtn.setAttribute('aria-disabled', 'true');
|
|
217
|
+
else
|
|
218
|
+
altMoveBtn.removeAttribute('aria-disabled');
|
|
219
|
+
if (altMoveHint)
|
|
220
|
+
altMoveHint.textContent = recusa ? `${dicaOriginal} ${t(recusa.chave)}`.trim() : dicaOriginal;
|
|
125
221
|
}
|
|
126
222
|
reflectMovementBtn();
|
|
127
223
|
}
|
|
@@ -151,13 +247,26 @@ export function initSettingsMotor(ctx) {
|
|
|
151
247
|
reflectAltMove();
|
|
152
248
|
});
|
|
153
249
|
});
|
|
250
|
+
// A prosa volta para o rodapé depois de as linhas serem reconstruídas (CLAUDE.md §4, #109).
|
|
251
|
+
ctx.fillExplain?.(ctx.$('#movement .overlay__card'));
|
|
154
252
|
}
|
|
155
253
|
if (facilBtn) {
|
|
156
254
|
facilBtn.addEventListener('click', () => setEasy(selMovPlayer, !ctx.players[selMovPlayer].easy));
|
|
157
255
|
}
|
|
158
256
|
if (altMoveBtn) {
|
|
159
257
|
altMoveBtn.addEventListener('click', () => {
|
|
160
|
-
|
|
258
|
+
/*
|
|
259
|
+
* ⚠️ RECUSAR DIZENDO, E NÃO EM SILÊNCIO. O precedente (`render/viz-setters`) resolve isto não ligando
|
|
260
|
+
* ouvinte nenhum — pode, porque reconstrói a lista a cada render. Aqui o ouvinte é ligado uma vez, e
|
|
261
|
+
* um `return` mudo seria «aceitar o clique e ignorá-lo», que é a outra metade do que o ADR-0076
|
|
262
|
+
* proíbe. Então a recusa FALA: quem carregou fica a saber por quê, mesmo sem ver a dica.
|
|
263
|
+
*/
|
|
264
|
+
const recusa = recusaAgora(selMovPlayer);
|
|
265
|
+
if (recusa) {
|
|
266
|
+
ctx.srSay(t(recusa.chave));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
setToggleMove(selMovPlayer, !ctx.players[selMovPlayer].toggleMove);
|
|
161
270
|
reflectAltMove();
|
|
162
271
|
});
|
|
163
272
|
}
|
|
@@ -191,7 +300,7 @@ export function initSettingsMotor(ctx) {
|
|
|
191
300
|
if (p.easy)
|
|
192
301
|
setEasy(i, false);
|
|
193
302
|
if (p.toggleMove)
|
|
194
|
-
|
|
303
|
+
setToggleMove(i, false);
|
|
195
304
|
if (p.toggleRun)
|
|
196
305
|
ctx.setToggleRun(i, false);
|
|
197
306
|
});
|
|
@@ -38,7 +38,15 @@ export interface SettingsTypoApi {
|
|
|
38
38
|
/** Currently selected font key. */
|
|
39
39
|
getFontKey: () => string;
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* A key is selectable when it exists in the catalog, is not marked `.off` (licence pending, etc.) — and is
|
|
43
|
+
* `geral`.
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ O TERCEIRO TERMO ENTROU EM 2026-09-07 (issue #87), e sem ele o resto da mudança seria decoração: o menu
|
|
46
|
+
* deixaria de OFERECER as caligráficas e elas continuariam SELECIONÁVEIS por qualquer outro caminho — o
|
|
47
|
+
* `resolveFontKey` de uma chave guardada, um `data-font` num markup de consumidor. «Não está na lista» e «não
|
|
48
|
+
* pode ser escolhida» têm de ser a mesma afirmação, ou a lista é só uma sugestão.
|
|
49
|
+
*/
|
|
42
50
|
export declare function isSelectableFont(k: string): boolean;
|
|
43
51
|
export { resolveFontKey, persistFontKey } from './fonts.js';
|
|
44
52
|
import type { DomQuery } from '../core/dom-query.js';
|
|
@@ -67,8 +75,30 @@ export interface TypoGroupView {
|
|
|
67
75
|
g: string;
|
|
68
76
|
rows: TypoRow[];
|
|
69
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* UMA linha da lista, a partir de uma face do catálogo. Extraída em 2026-09-07 (issue #87) porque o gate do
|
|
80
|
+
* mecanismo `.off` precisava de a exercitar com uma face de MENTIRA: as duas únicas entradas desligadas
|
|
81
|
+
* saíram do roster, e um caso que dependa da composição do catálogo reprova sempre que o roster muda.
|
|
82
|
+
*
|
|
83
|
+
* O mecanismo fica, e é preciso: o item 4 da #87 usa-o para a **Ronde**, que só pode ser oferecida se uma de
|
|
84
|
+
* três faces estiver instalada, porque duas delas são gratuitas apenas para uso pessoal e não podem ser
|
|
85
|
+
* empacotadas.
|
|
86
|
+
*/
|
|
87
|
+
export declare function linhaDaFonte(it: FontItem, fontKey: string): TypoRow;
|
|
70
88
|
/** Pure view-model for the typography list: which row is selected/disabled and its note, per catalog group. */
|
|
71
89
|
export declare function typoGroups(fontKey: string): TypoGroupView[];
|
|
72
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Full innerHTML for #typo-list, given the currently selected key. Pure string building — no DOM.
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ É UM GRUPO DE RÁDIO SÓ, ATRAVESSANDO AS TRÊS SECÇÕES, e isso é a decisão e não um detalhe de
|
|
94
|
+
* marcação: a exclusividade é do MENU inteiro — uma fonte activa no total —, não de cada família. Três
|
|
95
|
+
* grupos diriam a quem escuta que dá para ter uma sans E uma serif ao mesmo tempo.
|
|
96
|
+
*
|
|
97
|
+
* ⚠️ E ISTO DEIXOU DE SER UM INTERRUPTOR EM 2026-09-07. Era `class="mode-btn switch"` + `aria-pressed` +
|
|
98
|
+
* `toggleLabel()`, ou seja dezassete interruptores independentes anunciando «Ligado»/«Desligado» para
|
|
99
|
+
* escolher UMA fonte. A emenda do ADR-0012 diz o contrário em tantas palavras: «THE MENU IS A CHOICE, NOT
|
|
100
|
+
* A TOGGLE […] One font is active; the others are alternatives, not switches.» O `switch` do
|
|
101
|
+
* `style.css:427` desenha uma chave de 52×28 px com bolinha — era o desenho de um estado que não existe.
|
|
102
|
+
*/
|
|
73
103
|
export declare function typoListHTML(fontKey: string): string;
|
|
74
104
|
export declare function initSettingsTypo(ctx: SettingsTypoCtx): SettingsTypoApi;
|
|
@@ -6,17 +6,25 @@
|
|
|
6
6
|
// production). Overlay open/close plumbing (frontOverlay, focus management, the #typo hidden toggle, Escape
|
|
7
7
|
// handling) is the SHARED helper used by every settings panel and stays in game.js. The font catalog itself
|
|
8
8
|
// (FONT_GROUPS/FONT_BY_KEY) stays in ./fonts.js (Phase 2 extraction) — imported here, never duplicated.
|
|
9
|
-
|
|
9
|
+
// (`toggleLabel` saiu daqui em 2026-09-07: ele devolve «Ligado»/«Desligado», e este menu é uma ESCOLHA.)
|
|
10
10
|
import { t } from '../core/i18n.js';
|
|
11
|
-
import { FONT_GROUPS, FONT_BY_KEY, DEFAULT_FONT_KEY } from './fonts.js';
|
|
11
|
+
import { FONT_GROUPS, FONT_BY_KEY, DEFAULT_FONT_KEY, papelDaFonte } from './fonts.js';
|
|
12
12
|
import { markChanged, markMenuChanged, CHANGED_CLASS } from './changed-mark.js';
|
|
13
13
|
// ---------------------------------------------------------------------------------------------
|
|
14
14
|
// Pure logic (no `document`, testable in node)
|
|
15
15
|
// ---------------------------------------------------------------------------------------------
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* A key is selectable when it exists in the catalog, is not marked `.off` (licence pending, etc.) — and is
|
|
18
|
+
* `geral`.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ O TERCEIRO TERMO ENTROU EM 2026-09-07 (issue #87), e sem ele o resto da mudança seria decoração: o menu
|
|
21
|
+
* deixaria de OFERECER as caligráficas e elas continuariam SELECIONÁVEIS por qualquer outro caminho — o
|
|
22
|
+
* `resolveFontKey` de uma chave guardada, um `data-font` num markup de consumidor. «Não está na lista» e «não
|
|
23
|
+
* pode ser escolhida» têm de ser a mesma afirmação, ou a lista é só uma sugestão.
|
|
24
|
+
*/
|
|
17
25
|
export function isSelectableFont(k) {
|
|
18
26
|
const it = FONT_BY_KEY[k];
|
|
19
|
-
return !!it && !it.off;
|
|
27
|
+
return !!it && !it.off && papelDaFonte(it) === 'geral';
|
|
20
28
|
}
|
|
21
29
|
// A semantica da chave (validacao + migracao da chave antiga) mora em ui/fonts.ts, que e o dono do
|
|
22
30
|
// catalogo; re-exportada aqui para quem ja consome este modulo. Uma implementacao, nao duas.
|
|
@@ -38,35 +46,73 @@ export function fontCssTarget(k, it) {
|
|
|
38
46
|
const suffix = it.fb === 'serif' ? ',Georgia,serif' : it.fb === 'cursive' ? ',cursive' : '';
|
|
39
47
|
return { fonte: 'custom', customFamily: `'${it.fam}'${suffix}` };
|
|
40
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* UMA linha da lista, a partir de uma face do catálogo. Extraída em 2026-09-07 (issue #87) porque o gate do
|
|
51
|
+
* mecanismo `.off` precisava de a exercitar com uma face de MENTIRA: as duas únicas entradas desligadas
|
|
52
|
+
* saíram do roster, e um caso que dependa da composição do catálogo reprova sempre que o roster muda.
|
|
53
|
+
*
|
|
54
|
+
* O mecanismo fica, e é preciso: o item 4 da #87 usa-o para a **Ronde**, que só pode ser oferecida se uma de
|
|
55
|
+
* três faces estiver instalada, porque duas delas são gratuitas apenas para uso pessoal e não podem ser
|
|
56
|
+
* empacotadas.
|
|
57
|
+
*/
|
|
58
|
+
export function linhaDaFonte(it, fontKey) {
|
|
59
|
+
const disabled = !!it.off;
|
|
60
|
+
// `d` e `off` também guardam CHAVE. O travessão que junta os dois é pontuação, não frase — as duas
|
|
61
|
+
// metades são independentes e cada uma traduz por si.
|
|
62
|
+
const desc = it.d ? t(it.d) : '', motivo = it.off ? t(it.off) : '';
|
|
63
|
+
const note = desc ? desc + (disabled ? ' — ' + motivo : '') : disabled ? motivo : '';
|
|
64
|
+
return { key: it.k, fam: it.fam, selected: fontKey === it.k, disabled, note };
|
|
65
|
+
}
|
|
41
66
|
/** Pure view-model for the typography list: which row is selected/disabled and its note, per catalog group. */
|
|
42
67
|
export function typoGroups(fontKey) {
|
|
68
|
+
// ⚠️ SÓ AS GERAIS ENTRAM NA LISTA (emenda do ADR-0012, issue #87). As caligráficas existem para a criança
|
|
69
|
+
// APRENDER a ler letra cursiva — isso é matéria, e vive DENTRO das atividades, em botões próprios. Oferecê-
|
|
70
|
+
// las aqui é dar-lhe a matéria como obstáculo em todo lugar onde ela só quer navegar o menu.
|
|
71
|
+
//
|
|
72
|
+
// Um grupo que fique sem nenhuma face geral desaparece da lista, em vez de aparecer como título vazio.
|
|
43
73
|
return FONT_GROUPS.map((g) => ({
|
|
44
74
|
g: t(g.g), // `g` guarda CHAVE i18n desde o item 14 (ver ui/fonts)
|
|
45
|
-
rows: g.items.map((it) =>
|
|
46
|
-
|
|
47
|
-
// `d` e `off` também guardam CHAVE. O travessão que junta os dois é pontuação, não frase — as duas
|
|
48
|
-
// metades são independentes e cada uma traduz por si.
|
|
49
|
-
const desc = it.d ? t(it.d) : '', motivo = it.off ? t(it.off) : '';
|
|
50
|
-
const note = desc ? desc + (disabled ? ' — ' + motivo : '') : disabled ? motivo : '';
|
|
51
|
-
return { key: it.k, fam: it.fam, selected: fontKey === it.k, disabled, note };
|
|
52
|
-
}),
|
|
53
|
-
}));
|
|
75
|
+
rows: g.items.filter((it) => papelDaFonte(it) === 'geral').map((it) => linhaDaFonte(it, fontKey)),
|
|
76
|
+
})).filter((grupo) => grupo.rows.length > 0);
|
|
54
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* ⚠️ A MARCA DE SELEÇÃO, e ela existe porque COR NÃO É ESTADO.
|
|
80
|
+
*
|
|
81
|
+
* O `.mode-btn.is-on` pinta o botão com `var(--accent)` — o fundo amarelo que a emenda do ADR-0012 pede
|
|
82
|
+
* por extenso. Mas quem não distingue a cor não vê estado nenhum, e é a mesma razão pela qual
|
|
83
|
+
* `ui/activities-menu.ts:656` já emite ☑/☐ ao lado do `aria-checked`: «o estado em DUAS formas, e nenhuma
|
|
84
|
+
* delas é cor».
|
|
85
|
+
*/
|
|
86
|
+
const MARCA_ESCOLHIDA = '●';
|
|
87
|
+
const MARCA_ALTERNATIVA = '○';
|
|
55
88
|
function rowHTML(row) {
|
|
56
89
|
const noteHTML = row.note
|
|
57
90
|
? `<br><span class="opt-hint" style="margin:0;font-family:var(--font)">${row.note}</span>`
|
|
58
91
|
: '';
|
|
59
92
|
const ariaLabel = row.fam + (row.note ? ' — ' + row.note : '');
|
|
60
|
-
const stateLabel = toggleLabel(row.selected);
|
|
61
93
|
return (`<div class="ctrl-row"><span style="font-family:'${row.fam}'"><strong>${row.fam}</strong>${noteHTML}</span>` +
|
|
62
|
-
`<button class="mode-btn
|
|
63
|
-
`${row.disabled ? ' disabled' : ''} aria-
|
|
94
|
+
`<button class="mode-btn${row.selected ? ' is-on' : ''}" data-font="${row.key}" type="button" role="radio"` +
|
|
95
|
+
`${row.disabled ? ' disabled' : ''} aria-checked="${row.selected}" aria-label="${ariaLabel}">` +
|
|
96
|
+
`${row.selected ? MARCA_ESCOLHIDA : MARCA_ALTERNATIVA}</button></div>`);
|
|
64
97
|
}
|
|
65
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Full innerHTML for #typo-list, given the currently selected key. Pure string building — no DOM.
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ É UM GRUPO DE RÁDIO SÓ, ATRAVESSANDO AS TRÊS SECÇÕES, e isso é a decisão e não um detalhe de
|
|
102
|
+
* marcação: a exclusividade é do MENU inteiro — uma fonte activa no total —, não de cada família. Três
|
|
103
|
+
* grupos diriam a quem escuta que dá para ter uma sans E uma serif ao mesmo tempo.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ E ISTO DEIXOU DE SER UM INTERRUPTOR EM 2026-09-07. Era `class="mode-btn switch"` + `aria-pressed` +
|
|
106
|
+
* `toggleLabel()`, ou seja dezassete interruptores independentes anunciando «Ligado»/«Desligado» para
|
|
107
|
+
* escolher UMA fonte. A emenda do ADR-0012 diz o contrário em tantas palavras: «THE MENU IS A CHOICE, NOT
|
|
108
|
+
* A TOGGLE […] One font is active; the others are alternatives, not switches.» O `switch` do
|
|
109
|
+
* `style.css:427` desenha uma chave de 52×28 px com bolinha — era o desenho de um estado que não existe.
|
|
110
|
+
*/
|
|
66
111
|
export function typoListHTML(fontKey) {
|
|
67
|
-
|
|
112
|
+
const grupos = typoGroups(fontKey)
|
|
68
113
|
.map((group) => `<h3 class="panel-sub">${group.g}</h3>` + group.rows.map(rowHTML).join(''))
|
|
69
114
|
.join('');
|
|
115
|
+
return `<div role="radiogroup" aria-label="${t('font.grupo.rotulo')}">${grupos}</div>`;
|
|
70
116
|
}
|
|
71
117
|
// ---------------------------------------------------------------------------------------------
|
|
72
118
|
// DOM-facing (thin) — requires `document`/injected ctx
|
|
@@ -50,7 +50,14 @@ export interface SettingsVisualCtx {
|
|
|
50
50
|
/** Desenha UMA lista de rádio de modos visuais (+ abas por jogador). O MESMO helper que o painel de
|
|
51
51
|
* empatia usa — de propósito: as três correções mudaram de menu, e mudar junto a aparência delas faria a
|
|
52
52
|
* criança ter de reaprender um controle que ela já conhecia. */
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Os DOIS eixos deste painel (#104). Substituiu o `renderVizGroup`, que fica com o painel de EMPATIA.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ A lista de sete que este painel oferecia era a forma honesta de contar uma exclusividade REAL, e o
|
|
57
|
+
* `VISUAL_MODES` explica-a em prosa logo acima. Ela deixou de existir: o estado tem dois eixos, e os
|
|
58
|
+
* escritores por eixo mexem num sem tocar no outro.
|
|
59
|
+
*/
|
|
60
|
+
renderEixosVisuais: (listSel: string, tabsSel: string) => void;
|
|
54
61
|
setLq: (t: number) => void;
|
|
55
62
|
setOwnerColors: (on: boolean) => void;
|
|
56
63
|
setCbSafe: (on: boolean) => void;
|
|
@@ -58,6 +65,19 @@ export interface SettingsVisualCtx {
|
|
|
58
65
|
setOutlineBg: (level: number) => void;
|
|
59
66
|
setRoleColor: (key: RoleKey, hex: string) => void;
|
|
60
67
|
resetRoleColors: () => void;
|
|
68
|
+
/**
|
|
69
|
+
* Move a prosa das linhas para o rodapé (`ui/settings-panel` → `fillExplain`). Chamado a CADA render.
|
|
70
|
+
*
|
|
71
|
+
* ⚠️ NÃO É OPCIONAL POR ELEGÂNCIA: `fillExplain` roda uma vez quando o overlay é frontalizado e move o
|
|
72
|
+
* `.opt-hint` de dentro de cada linha para o rodapé. Este painel RECONSTRÓI as linhas, e as linhas novas
|
|
73
|
+
* voltam com a prosa lá dentro — então a explicação aparece duas vezes, no rodapé e sob o rótulo, a
|
|
74
|
+
* partir do primeiro clique. O `CLAUDE.md` §4 regista exatamente isto, e a issue #109 já o consertou
|
|
75
|
+
* uma vez noutros painéis.
|
|
76
|
+
*
|
|
77
|
+
* Opcional na assinatura porque um consumidor pode montar o painel sem a casca (um teste, o segundo
|
|
78
|
+
* consumidor): sem casca não há rodapé para duplicar.
|
|
79
|
+
*/
|
|
80
|
+
fillExplain?: (card: HTMLElement | null) => void;
|
|
61
81
|
}
|
|
62
82
|
/**
|
|
63
83
|
* `viz` quando ele é um modo DESTE menu (contraste ou correção), senão 'normal'.
|