movil: usabilidad tras la prueba en el Pixel

Lo que salio de recorrer la app como alguien de la acampada:

- borrar (tribus y todo lo que usa renderContentActions) pide un segundo
  toque; la X va la ultima de la fila
- Karvan: Llamar empieza solo con voz (si se negaba la camara no arrancaba
  nada); botones con icono y nombre; textos traducidos; ayuda para invitar
- votaciones: tipo Si/No, semaforo o termometro (el servidor ya aceptaba
  options); resultados en barras; aviso a quien la crea de que no puede votar
- workflow activistas con tribus, parlamento, votaciones y eventos
- seccion apagada: /modules dice cual era y la activa de un toque
- Modulos en el movil: agrupados como el lanzador, interruptores y Guardar fijo
- tribus en filas compactas; secciones de una tribu en tira de fichas + Mas
- lanzador y barra con iconos SVG en vez de letras vai
- Ajustes: idioma y aspecto primero; idiomas de un toque
- aviso de bienvenida solo en la portada y en una linea; Ahora no al final
- pie de una linea con el estado de sincronizacion en claro
- mobile_ux.css para los dos skins: un margen de 14px (eran 74 por lado en
  las fichas), Anton solo en titulos y boton principal, buscador en una linea,
  la barra de abajo se esconde con el teclado
This commit is contained in:
SITO 2026-10-19 12:37:58 +02:00
parent 2e3ff1e79f
commit 4dd573662c
14 changed files with 1279 additions and 54 deletions

View file

@ -1,9 +1,74 @@
const { form, button, div, h2, p, section, table, tr, td, input, a, br, ul, li, span, strong } = require("../server/node_modules/hyperaxe");
const { template, i18n } = require('./main_views');
const { form, button, div, h2, h3, p, section, table, tr, td, input, a, br, ul, li, span, strong, label } = require("../server/node_modules/hyperaxe");
const { template, i18n, LAUNCHER_GROUPS } = require('./main_views');
const { iconNode } = require('./mobile_icons');
const { getConfig } = require('../configs/config-manager.js');
const { PRESETS: PRESET_LIST } = require('../models/workflows_model');
const modulesView = () => {
// en el movil: los modulos agrupados como en el lanzador, cada uno una fila que se
// toca entera con un interruptor, y Guardar siempre a la vista. La tabla de 52 filas
// con casillas de 16px eran 11 pantallas
const mobileModulesView = (visibleModules, moduleStates, opts) => {
const groupOf = {};
(LAUNCHER_GROUPS || []).forEach(g => g.mods.forEach(m => { if (m.mod) groupOf[m.mod.replace(/Mod$/, '')] = g; }));
// lo que no sale en el lanzador: Industria va con la economia; lo demas, al final
const economy = (LAUNCHER_GROUPS || []).find(g => g.id === 'economy');
if (economy && !groupOf.industry) groupOf.industry = economy;
const groups = [];
const byId = {};
visibleModules.forEach(mod => {
const g = groupOf[mod.name] || { id: 'other', titleKey: 'menuTools' };
if (!byId[g.id]) { byId[g.id] = { g, mods: [] }; groups.push(byId[g.id]); }
byId[g.id].mods.push(mod);
});
const order = (LAUNCHER_GROUPS || []).map(g => g.id);
const rank = (id) => { const i = order.indexOf(id); return i === -1 ? order.length : i; };
groups.sort((x, y) => rank(x.g.id) - rank(y.g.id));
const on = visibleModules.filter(m => moduleStates[`${m.name}Mod`] === 'on').length;
const blocked = opts.blocked && visibleModules.find(m => `${m.name}Mod` === opts.blocked.mod);
const blockedBox = blocked ? div({ class: 'module-blocked' },
iconNode('module-blocked-ico', blocked.name),
p({ class: 'module-blocked-text' }, String(i18n.moduleBlockedText || '{name} is turned off.').replace('{name}', blocked.label)),
form({ method: 'POST', action: '/modules/enable' },
input({ type: 'hidden', name: 'mod', value: blocked.name }),
input({ type: 'hidden', name: 'returnTo', value: opts.blocked.path || `/${blocked.name}` }),
button({ type: 'submit', class: 'create-button' }, String(i18n.moduleBlockedAction || 'Turn on {name}').replace('{name}', blocked.label))
)
) : null;
return template(
i18n.modules,
section(div({ class: 'tags-header module-header-line' },
h2(i18n.modulesViewTitle),
p(String(i18n.modulesOnCount || '{on} of {total} sections on').replace('{on}', on).replace('{total}', visibleModules.length))
)),
blockedBox,
opts.saved ? p({ class: 'module-saved', role: 'status' }, i18n.modulesSaved || 'Saved.') : null,
section(
form({ action: '/save-modules', method: 'post', class: 'module-switches' },
...groups.map(({ g, mods }) => div({ class: 'module-group' },
h3({ class: 'module-group-title' }, i18n[g.titleKey] || g.id),
...mods.map(mod => label({ class: 'module-switch', for: `${mod.name}Mod` },
iconNode('module-switch-ico', mod.name),
span({ class: 'module-switch-text' },
span({ class: 'module-switch-name' }, mod.label),
span({ class: 'module-switch-desc' }, mod.description)
),
input({
type: 'checkbox', role: 'switch', id: `${mod.name}Mod`, name: `${mod.name}Form`, value: 'on', class: 'switch-input',
checked: moduleStates[`${mod.name}Mod`] === 'on' ? true : undefined
})
))
)),
div({ class: 'module-save-bar' },
button({ type: 'submit', class: 'create-button' }, i18n.saveSettings)
)
)
)
);
};
const modulesView = (_states, opts = {}) => {
const config = getConfig().modules;
const modules = [
{ name: 'agenda', label: i18n.modulesAgendaLabel, description: i18n.modulesAgendaDescription },
@ -76,6 +141,8 @@ const modulesView = () => {
return acc;
}, {});
if (isMobile) return mobileModulesView(visibleModules, moduleStates, opts);
const activeModulesCount = visibleModules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length;
const totalModulesCount = visibleModules.length;
const disabledModulesCount = totalModulesCount - activeModulesCount;