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

@ -0,0 +1,413 @@
/* Piezas de usabilidad de la rama movil, para los dos skins (Acampada y OasisMobile).
*
* Va la ultima, detras de karvan.css. Los colores salen de las variables de
* Acampada (--a26-*) si el tema las define y, si no, de la paleta oscura con oro
* de OasisMobile; asi cada pieza nueva se ve bien en los dos sin duplicarla.
*
* Letra: Anton (--ux-title) solo para titulos y para el boton principal de cada
* pantalla; todo lo demas (fichas, botones secundarios, etiquetas) en la letra del
* sistema en negrita. Antes habia Anton en botones, fichas, cabeceras de tabla y
* pestañas, y la pantalla parecia un cartel.
*
* Espacios: rejilla de 4/8. Zonas tactiles de 44px como minimo. */
:root {
--ux-bg: var(--a26-bg, #121212);
--ux-fg: var(--a26-fg, #f2f2f2);
--ux-accent: var(--a26-neon, #ffd700);
--ux-on-accent: #000;
--ux-soft: var(--a26-soft, #1e1e1e);
--ux-muted: var(--a26-muted, #a8a8a8);
--ux-line: var(--a26-fg, #3a3a3a);
--ux-radius: var(--a26-radius, 12px);
--ux-title: var(--a26-tit, inherit);
--ux-text: var(--a26-txt, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
--ux-danger: #c4003b;
--ux-ok: #0a8f3c;
--ux-warn: #c98a00;
}
/* ---------- letra unificada ---------- */
/* fichas de filtro, pestañas y botones secundarios: letra del sistema */
.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group, .filters) .filter-btn:not(.create-button),
.main-column .filter-box__button, .main-column .doc-export-actions .filter-btn,
.main-column .update-btn, .main-column .tribe-action-btn, .lang-chip, .kc-btn,
.main-column table th, .card-chips-row > span, .pm-exposition-text {
font-family: var(--ux-text) !important;
font-weight: 700 !important;
letter-spacing: .02em !important;
}
.main-column :is(.tribe-mode-buttons, .mode-buttons, .mode-buttons-row, .filter-group, .filters) .filter-btn:not(.create-button) {
font-size: 15px !important;
text-transform: uppercase;
}
/* los formularios de cada modulo (crear tribu, votacion, sala...): etiquetas como
texto, no como titulos; el h2 de la pregunta de una votacion era Anton de 28px */
.main-column .vote-form h2, .main-column form > label:not(.module-switch):not(.vote-kind-option), .main-column form label[for]:not(.module-switch) {
font-family: var(--ux-text) !important;
font-size: 15px !important;
font-weight: 700 !important;
text-transform: none !important;
letter-spacing: 0 !important;
color: var(--ux-fg) !important;
}
.form-hint {
margin: 6px 0 14px !important;
font-size: 14px !important;
line-height: 1.4;
color: var(--ux-muted) !important;
}
/* el boton de enviar de los formularios: a todo lo ancho, abajo, que se vea que es
el final. Antes unos centrados, otros a la izquierda y otros enteros */
.main-column :is(.vote-form, .tribe-form, .karvan-create, .karvan-compose, .karvan-invite) form > button[type="submit"],
.main-column :is(.vote-form, .karvan-create, .karvan-compose, .karvan-invite) > form > button[type="submit"],
.main-column form[action$="/tribes/create"][method="post"] > button[type="submit"],
.main-column form[action*="/tribes/update/"] > button[type="submit"] {
display: block !important;
width: 100% !important;
margin: 8px 0 0 !important;
}
/* ---------- iconos SVG (lanzador, barra, modulos, Karvan) ---------- */
.has-svg { display: inline-flex !important; align-items: center; justify-content: center; }
.has-svg svg { width: 100%; height: 100%; display: block; }
.ln-ico.has-svg { padding: 15px !important; }
.ln-ico.has-svg svg { width: 30px; height: 30px; }
.oasis-bottombar .bb-ico.has-svg {
width: 56px !important; height: 30px !important; padding: 3px 0 !important;
border-radius: 16px !important; font-size: 0 !important;
}
.oasis-bottombar .bb-ico.has-svg svg { width: 24px; height: 24px; }
.bb-manage-ico.has-svg, .bb-pick-ico.has-svg { width: 24px; height: 24px; }
/* ---------- barra de abajo: la pestaña actual con pastilla, como en Android ---------- */
.oasis-bottombar a.bb-item.current .bb-ico.has-svg { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
.oasis-bottombar .bb-lbl { font-family: var(--ux-text) !important; }
/* con el teclado abierto la ventana encoge: la barra se esconde y el formulario
recupera ese tercio de pantalla */
@media (max-height: 520px) {
.oasis-bottombar { display: none !important; }
body { padding-bottom: 0 !important; }
}
/* ---------- aviso de bienvenida: una linea, no un bloque ---------- */
.welcome-banner {
display: flex !important; align-items: center !important; gap: 10px !important;
padding: 8px 8px 8px 14px !important; min-height: 0 !important;
}
.welcome-banner .update-banner-icon { display: none !important; }
.welcome-banner .update-banner-text { flex: 1 1 auto; font-size: 15px !important; line-height: 1.3 !important; margin: 0 !important; }
.welcome-banner .welcome-banner-start {
flex: 0 0 auto; font-size: 16px !important; min-height: 40px !important;
padding: 6px 14px !important; margin: 0 !important; width: auto !important;
}
.welcome-banner .welcome-banner-close { margin: 0 !important; flex: 0 0 auto; }
.welcome-banner .welcome-banner-close-btn { min-height: 40px !important; width: 36px !important; margin: 0 !important; }
/* ---------- pie de una linea ---------- */
.oasis-footer.oasis-footer-mobile {
display: flex !important; flex-direction: column; align-items: center; gap: 4px;
padding: 14px 16px 18px !important; margin-top: 24px !important; text-align: center;
font-size: 13px !important;
}
.oasis-footer-mobile .footer-sync {
display: inline-flex; align-items: center; gap: 8px; min-height: 36px;
font-weight: 700; text-decoration: none !important; color: var(--ux-fg) !important;
}
.oasis-footer-mobile .footer-sync-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ux-muted); }
.oasis-footer-mobile .footer-sync.is-on .footer-sync-dot { background: var(--ux-ok); }
.oasis-footer-mobile .footer-version { color: var(--ux-muted) !important; }
/* ---------- acciones de tarjeta: borrar con confirmacion ---------- */
.content-action-confirm { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; }
.content-action-confirm > summary { list-style: none; cursor: pointer; }
.content-action-confirm > summary::-webkit-details-marker { display: none; }
.content-action-confirm[open] { flex-basis: 100%; order: 99; }
.content-action-confirm[open] > summary { background: var(--ux-danger) !important; color: #fff !important; border-color: var(--ux-danger) !important; }
.content-action-confirm .confirm-bar {
flex-basis: 100%; display: flex; align-items: center; justify-content: flex-end; gap: 10px;
margin-top: 8px; padding: 8px 8px 8px 12px; border: 2px solid var(--ux-danger); border-radius: var(--ux-radius);
}
.content-action-confirm .confirm-question { flex: 1 1 auto; font-weight: 700; font-size: 15px; text-align: left; }
.content-action-confirm form { margin: 0 !important; width: auto !important; }
.main-column .content-action-confirm .confirm-yes, .content-action-confirm .confirm-yes {
background: var(--ux-danger) !important; color: #fff !important; border-color: var(--ux-danger) !important;
box-shadow: none !important; min-height: 44px !important; margin: 0 !important; width: auto !important;
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important;
}
/* ---------- lista de tribus: una fila por tribu ---------- */
.main-column .tribe-list { display: flex !important; flex-direction: column; gap: 10px; padding: 0 !important; }
.main-column a.tribe-row {
display: flex !important; flex-wrap: nowrap !important; align-items: center; gap: 12px; width: auto !important;
padding: 10px 12px !important; min-height: 72px;
border: 2px solid var(--ux-line) !important; border-radius: var(--ux-radius) !important;
background: var(--ux-bg) !important; color: var(--ux-fg) !important; text-decoration: none !important;
}
.main-column a.tribe-row:active { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
.tribe-row .tribe-row-thumb {
flex: 0 0 52px; width: 52px !important; height: 52px !important; border-radius: 10px !important;
object-fit: cover; border: 2px solid var(--ux-line) !important; margin: 0 !important;
}
.tribe-row .tribe-row-icon { padding: 11px; background: var(--ux-soft); }
.tribe-row .tribe-row-text { flex: 1 1 0; min-width: 0; overflow: hidden; display: flex; flex-direction: column; gap: 2px; }
.tribe-row .tribe-row-text > span { display: block; max-width: 100%; margin: 0 !important; padding: 0 !important; line-height: 1.3; }
.tribe-row .tribe-row-title {
font-family: var(--ux-title) !important; font-size: 21px; line-height: 1.1; text-transform: uppercase;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tribe-row .tribe-row-facts { font-size: 13px; font-weight: 700; color: var(--ux-muted); }
.tribe-row .tribe-row-desc { font-size: 14px; color: var(--ux-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tribe-row .tribe-row-go { flex: 0 0 auto; font-size: 28px; line-height: 1; color: var(--ux-muted); }
/* ---------- votaciones ---------- */
.vote-bars { display: flex; flex-direction: column; gap: 10px; margin: 8px 0 4px; }
.vote-bar { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 4px 10px; }
.vote-bar-label { font-weight: 700; font-size: 15px; }
.vote-bar-count { font-weight: 700; font-size: 15px; min-width: 2ch; text-align: right; }
.vote-bar-meter {
grid-column: 1 / -1; width: 100%; height: 12px; appearance: none; -webkit-appearance: none;
border: 2px solid var(--ux-line); border-radius: 8px; overflow: hidden; background: var(--ux-soft);
}
.vote-bar-meter::-webkit-progress-bar { background: var(--ux-soft); }
.vote-bar-meter::-webkit-progress-value { background: var(--ux-fg); }
.vote-bar-meter::-moz-progress-bar { background: var(--ux-fg); }
.vote-bar-yes .vote-bar-meter::-webkit-progress-value, .vote-bar-green .vote-bar-meter::-webkit-progress-value { background: var(--ux-ok); }
.vote-bar-no .vote-bar-meter::-webkit-progress-value, .vote-bar-red .vote-bar-meter::-webkit-progress-value { background: var(--ux-danger); }
.vote-bar-yellow .vote-bar-meter::-webkit-progress-value { background: var(--ux-warn); }
.vote-own-note {
padding: 12px 14px !important; border-left: 4px solid var(--ux-accent) !important;
background: var(--ux-soft) !important; border-radius: 0 10px 10px 0; font-size: 15px !important;
}
/* botones de votar: rejilla de dos columnas, cada opcion un boton grande */
.main-column .vote-buttons-row-single { display: grid !important; grid-template-columns: 1fr 1fr; gap: 10px !important; }
.main-column .vote-buttons-row-single form { margin: 0 !important; width: 100% !important; }
.main-column .vote-buttons-row-single button {
width: 100% !important; min-height: 52px !important; margin: 0 !important;
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 16px !important;
}
.main-column .vote-buttons-row-single button[value="YES"], .main-column .vote-buttons-row-single button[value="GREEN"] { border-color: var(--ux-ok) !important; box-shadow: 0 4px 0 var(--ux-ok) !important; }
.main-column .vote-buttons-row-single button[value="NO"], .main-column .vote-buttons-row-single button[value="RED"] { border-color: var(--ux-danger) !important; box-shadow: 0 4px 0 var(--ux-danger) !important; }
.main-column .vote-buttons-row-single button[value="YELLOW"] { border-color: var(--ux-warn) !important; box-shadow: 0 4px 0 var(--ux-warn) !important; }
/* tipo de votacion: tarjetas de radio */
.vote-kind { display: flex; flex-direction: column; gap: 10px; margin: 4px 0 18px; }
.vote-kind-title { font-weight: 700; font-size: 15px; }
.vote-kind-option {
display: grid !important; grid-template-columns: auto 1fr; gap: 2px 12px; align-items: center;
padding: 12px 14px; border: 2px solid var(--ux-line); border-radius: var(--ux-radius); cursor: pointer;
}
.main-column .vote-kind-option input[type="radio"] { grid-row: 1 / span 2; width: 22px !important; height: 22px !important; min-height: 0 !important; margin: 0 !important; accent-color: var(--ux-fg); }
.vote-kind-option:has(input:checked) { background: var(--ux-accent); color: var(--ux-on-accent); }
.vote-kind-option:has(input:checked) .vote-kind-hint { color: var(--ux-on-accent); }
.vote-kind-name { font-weight: 700; font-size: 17px; }
.vote-kind-hint { font-size: 14px; color: var(--ux-muted); }
/* ---------- modulos: interruptores agrupados ---------- */
.module-switches { display: flex; flex-direction: column; gap: 22px; padding-bottom: 8px; }
.module-group { display: flex; flex-direction: column; gap: 8px; }
.module-group-title {
font-family: var(--ux-title) !important; font-weight: 400 !important; font-size: 20px !important;
text-transform: uppercase; margin: 0 0 4px !important; padding-bottom: 4px; border-bottom: 3px solid var(--ux-line);
}
.main-column label.module-switch {
display: grid !important; grid-template-columns: 40px minmax(0, 1fr) 52px; align-items: center; gap: 12px;
padding: 10px 12px; min-height: 64px; cursor: pointer;
border: 2px solid var(--ux-line); border-radius: var(--ux-radius);
font-family: var(--ux-text) !important; text-transform: none !important; letter-spacing: 0 !important;
}
.module-switch-ico { width: 40px; height: 40px; padding: 8px; border-radius: 10px; background: var(--ux-soft); color: var(--ux-fg); }
.module-switch-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.module-switch-name { font-weight: 700; font-size: 17px; }
.module-switch-desc { font-size: 13px; color: var(--ux-muted); line-height: 1.3; }
.module-switch:has(.switch-input:checked) { border-color: var(--ux-fg); }
/* el interruptor: la casilla de siempre con forma de pastilla */
.main-column label.module-switch input.switch-input[type="checkbox"] {
appearance: none; -webkit-appearance: none; flex: 0 0 auto;
width: 52px !important; height: 30px !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important;
border: 2px solid var(--ux-line) !important; border-radius: 16px !important; background: var(--ux-soft) !important;
position: relative; cursor: pointer; transition: background .15s ease-out;
}
.main-column label.module-switch input.switch-input[type="checkbox"]::after {
content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%;
background: var(--ux-muted); transition: transform .15s ease-out, background .15s ease-out;
}
.main-column label.module-switch input.switch-input[type="checkbox"]:checked { background: var(--ux-accent) !important; }
.main-column label.module-switch input.switch-input[type="checkbox"]:checked::after { transform: translateX(22px); background: var(--ux-on-accent); }
/* Guardar siempre a la vista, encima de la barra de abajo */
.module-save-bar {
position: sticky; bottom: calc(76px + env(safe-area-inset-bottom, 0px)); z-index: 5;
padding: 10px 0; background: var(--ux-bg);
}
.module-save-bar .create-button { width: 100% !important; margin: 0 !important; }
.module-saved {
padding: 10px 14px !important; margin: 0 0 14px !important; font-weight: 700;
border-left: 4px solid var(--ux-ok) !important; background: var(--ux-soft) !important; border-radius: 0 10px 10px 0;
}
/* la seccion apagada a la que se queria entrar */
.main-column div.module-blocked {
display: grid; grid-template-columns: 48px 1fr; gap: 12px; align-items: center;
padding: 14px; margin: 0 0 20px; border: 3px solid var(--ux-line); border-radius: var(--ux-radius); background: var(--ux-soft);
}
.module-blocked-ico { width: 48px; height: 48px; padding: 10px; border-radius: 12px; background: var(--ux-bg); }
.module-blocked-text { margin: 0 !important; font-weight: 700; font-size: 17px; }
.module-blocked form { grid-column: 1 / -1; margin: 0 !important; }
.module-blocked .create-button { width: 100% !important; margin: 0 !important; }
/* ---------- Ajustes: idiomas como fichas de un toque ---------- */
.lang-pick { display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
.main-column .lang-pick .lang-chip, .lang-pick .lang-chip {
width: 100% !important; min-height: 48px !important; margin: 0 !important; padding: 6px 10px !important;
font-size: 16px !important; text-transform: none !important; box-shadow: none !important;
border: 2px solid var(--ux-line) !important; background: var(--ux-bg) !important; color: var(--ux-fg) !important;
}
.main-column .lang-pick .lang-chip.is-current, .lang-pick .lang-chip.is-current { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
/* ---------- Karvan: barra de llamada con nombre en cada boton ---------- */
.karvan-call-bar { display: grid !important; grid-template-columns: repeat(4, 1fr); gap: 8px !important; }
.main-column .karvan-call-bar .kc-btn, .karvan-call-bar .kc-btn {
display: flex !important; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
width: 100% !important; min-height: 64px !important; padding: 8px 4px !important; margin: 0 !important;
font-size: 13px !important; text-transform: none !important; box-shadow: none !important;
}
.kc-btn .kc-ico { width: 24px; height: 24px; }
.kc-btn .kc-lbl { line-height: 1.1; }
.main-column .karvan-call-bar .kc-btn-start { background: var(--ux-ok) !important; color: #fff !important; border-color: var(--ux-ok) !important; }
.main-column .karvan-call-bar .kc-btn.on { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
.main-column .karvan-call-bar .kc-btn-stop { color: var(--ux-danger) !important; border-color: var(--ux-danger) !important; }
.kc-status:not(:empty) { margin-top: 10px; padding: 8px 12px; border-radius: 10px; background: var(--ux-soft); font-size: 14px; font-weight: 700; }
.karvan-invite { margin-top: 24px !important; padding-top: 16px !important; border-top: 2px solid var(--ux-line) !important; }
.karvan-invite-lbl { display: block; margin-bottom: 6px; font-weight: 700; }
/* ---------- foco visible para teclado y lectores ---------- */
a:focus-visible, button:focus-visible, summary:focus-visible, label.module-switch:focus-within {
outline: 3px solid var(--ux-accent) !important; outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.main-column label.module-switch input.switch-input[type="checkbox"], .main-column label.module-switch input.switch-input[type="checkbox"]::after { transition: none; }
}
/* ---------- buscador de cada modulo: campo y lupa en la misma linea ---------- */
/* solo cuando el unico control es el boton: los de Transferencias llevan rangos y
desplegables y siguen en columna */
.main-column form.filter-box:not(:has(.filter-box__select, .filter-box__number, .transfer-range)) {
display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important;
align-items: stretch !important; gap: 8px !important; width: 100% !important; margin: 0 0 12px !important;
}
.main-column form.filter-box:not(:has(.filter-box__select, .filter-box__number, .transfer-range)) .filter-box__input {
flex: 1 1 auto !important; min-width: 0 !important; width: 0 !important; height: 48px !important; margin: 0 !important;
}
.main-column form.filter-box:not(:has(.filter-box__select, .filter-box__number, .transfer-range)) .filter-box__controls {
flex: 0 0 auto !important; width: auto !important; margin: 0 !important;
}
.main-column form.filter-box .filter-box__button { min-height: 48px !important; margin: 0 !important; }
/* el recuento (TOTAL: 12 · PUBLICA: 12) como una linea discreta, no un bloque */
.main-column .activity-toolbar-row { display: flex !important; flex-direction: column !important; align-items: stretch !important; gap: 6px !important; }
.main-column .module-stats, .main-column [class*="module-stats"] { font-size: 13px !important; color: var(--ux-muted) !important; text-align: left !important; margin: 0 !important; }
/* el boton de crear de cada modulo a todo lo ancho, tambien cuando va solo en su
formulario (en Votaciones salia estrecho y en Tribus entero) */
.main-column .filters > form:has(.create-button), .main-column .mode-buttons > form:has(.create-button) { width: 100% !important; }
/* jerga fuera del movil: la tasa ECO (microgramos de CO2) no se entendia y su ficha
salia ilegible; sigue en Estadisticas */
.main-column .eco-tax-chip, .main-column [class*="eco-tax-chip"] { display: none !important; }
/* etiquetas (#asamblea) sin caja negra detras */
.main-column .card-tags, .main-column .tribe-side .card-tags { background: transparent !important; padding: 0 !important; margin: 8px 0 !important; }
/* acciones secundarias de una ficha (actualizar, PDF, compartir): una rejilla de
dos columnas, sin los huecos de 80px que dejaban los formularios sueltos */
.main-column :is(.tribe-side-actions, .doc-export-actions) {
display: grid !important; grid-template-columns: 1fr 1fr; gap: 8px !important; margin: 12px 0 !important; padding: 0 !important;
}
.main-column :is(.tribe-side-actions, .doc-export-actions) form { margin: 0 !important; width: 100% !important; padding: 0 !important; }
.main-column :is(.tribe-side-actions, .doc-export-actions) :is(button, .filter-btn, a) {
width: 100% !important; min-height: 44px !important; margin: 0 !important; padding: 6px 8px !important;
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 14px !important; box-shadow: none !important;
}
.main-column .tribe-side-actions + .doc-export-actions { margin-top: 0 !important; }
/* resultado de la votacion: ficha de color legible en los dos skins */
.main-column .vote-outcome {
display: inline-block; padding: 2px 10px !important; border-radius: 8px;
font-family: var(--ux-text) !important; font-size: 15px !important; font-weight: 700 !important;
background: var(--ux-soft) !important; color: var(--ux-fg) !important; vertical-align: middle;
}
.main-column .vote-outcome-yes { background: var(--ux-ok) !important; color: #fff !important; }
.main-column .vote-outcome-no { background: var(--ux-danger) !important; color: #fff !important; }
.main-column .job-section-title:has(.vote-outcome) { display: flex !important; align-items: center; gap: 8px; flex-wrap: wrap; }
/* ---------- margenes: uno solo, de 14px ---------- */
/* se sumaban capa a capa (contenido 20 + columna 10 + ficha 24 + lateral 20 = 74px
por lado) y el texto de una votacion o una tribu se quedaba en 264 de 412px */
body .main-content { padding-left: 14px !important; padding-right: 14px !important; }
body .main-content > .main-column { padding-left: 0 !important; padding-right: 0 !important; }
.main-column :is(.tribe-details, .tribe-side, .tribe-main, .job-section, .vote-form, .tribe-section, .tribe-content) {
padding-left: 0 !important; padding-right: 0 !important; margin-left: 0 !important; margin-right: 0 !important;
width: 100% !important; max-width: 100% !important; box-sizing: border-box;
}
.main-column .tribe-details { display: flex !important; flex-direction: column !important; gap: 18px; }
/* dentro de una ficha, un solo ritmo: 10px entre bloques (antes hueco de 16 + los
margenes de cada bloque + 20 de relleno, unos 70px entre secciones) */
.main-column .tribe-side { gap: 10px !important; padding-top: 0 !important; padding-bottom: 0 !important; }
.main-column .tribe-side > *, .main-column .tribe-main > * { margin-top: 0 !important; margin-bottom: 0 !important; }
.main-column .tribe-main { display: flex !important; flex-direction: column; gap: 14px !important; }
.main-column .tribe-card-members { text-align: left !important; padding: 0 !important; font-weight: 700; }
.main-column :is(.tribe-side-actions, .doc-export-actions) > :only-child { grid-column: 1 / -1; }
/* la imagen generica de tribu no aporta nada y ocupaba 300px: solo se ve la propia */
.main-column img[src*="default-tribe"] { display: none !important; }
/* etiquetas del lanzador largas (Configuracion): se parten con guion */
.ln-lbl { hyphens: auto; -webkit-hyphens: auto; font-size: 12.5px !important; }
/* ---------- secciones de una tribu: tira de fichas deslizable + "Mas" ---------- */
.main-column .tribe-tabs { margin: 4px 0 12px !important; }
.tribe-tabs-strip {
display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
margin: 0; padding: 4px 0 8px; scroll-snap-type: x proximity;
}
.tribe-tabs-strip::-webkit-scrollbar { display: none; }
.main-column a.tribe-tab, .tribe-tabs-more > summary {
flex: 0 0 auto; scroll-snap-align: start; display: inline-flex; align-items: center; justify-content: center;
min-height: 40px; padding: 4px 14px; border: 2px solid var(--ux-line) !important; border-radius: 20px !important;
background: var(--ux-bg) !important; color: var(--ux-fg) !important; text-decoration: none !important;
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 14px !important; white-space: nowrap;
text-transform: uppercase; letter-spacing: .02em;
}
.main-column a.tribe-tab.is-current { background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; }
.tribe-tabs-more > summary { list-style: none; cursor: pointer; margin-top: 2px; }
.tribe-tabs-more > summary::-webkit-details-marker { display: none; }
.tribe-tabs-more > summary::after { content: " ▾"; }
.tribe-tabs-more[open] > summary::after { content: " ▴"; }
.tribe-tabs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
.tribe-tabs-grid .tribe-tab { width: 100%; }
/* filas etiqueta / valor de las fichas (plazo, estado, creado el): en dos columnas */
.main-column table.tribe-info-table tr { display: grid !important; grid-template-columns: minmax(96px, 38%) 1fr; align-items: center; }
.main-column table.tribe-info-table td { display: block !important; border: 0 !important; padding: 8px 10px !important; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.main-column table.tribe-info-table tr + tr { border-top: 1px solid #ccc !important; }
/* bienvenida: "Ahora no" al final, como enlace discreto */
.main-column .welcome-skip-end { text-align: center; margin: 8px 0 0 !important; }
.main-column .welcome-skip-end .welcome-skip-btn {
background: transparent !important; border: 0 !important; box-shadow: none !important;
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important;
text-transform: none !important; text-decoration: underline; color: var(--ux-muted) !important; width: auto !important;
}
/* buzon y otros: los botones de filtro sueltos (Mas recientes, Marcar como leido)
en la letra de texto, como el resto de fichas */
.main-column .inbox-filters button, .main-column [class*="inbox"] .filter-btn:not(.create-button),
.main-column form[action^="/inbox"] button:not(.create-button) {
font-family: var(--ux-text) !important; font-weight: 700 !important; font-size: 15px !important; letter-spacing: .02em !important;
}
/* ---------- regla general de letra para botones ---------- */
/* todo boton en letra del sistema; Anton solo en el principal de cada pantalla
(el negro, .create-button) y en las tarjetas de aspecto */
.main-column :is(button, input[type="submit"], input[type="button"], .filter-btn, a.filter-btn):not(.create-button):not(.welcome-banner-start):not(.skin-card):not(.workflow-card):not(.btn-singleview) {
font-family: var(--ux-text) !important;
font-weight: 700 !important;
letter-spacing: .02em !important;
}
.main-column :is(button, input[type="submit"]):not(.create-button):not(.filter-btn):not(.btn-singleview):not(.chat-react-btn):not(.lang-chip):not(.kc-btn):not(.confirm-yes):not(.welcome-skip-btn) {
font-size: 16px !important;
}
.main-column form.welcome-skip-end { display: flex !important; justify-content: center !important; }

View file

@ -17,6 +17,47 @@
module.exports = {
en: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "More",
tribeSectionsLabel: "Sections",
voteYes: "Yes",
voteNo: "No",
voteAbstention: "Abstain",
voteGreen: "Green · agree",
voteYellow: "Yellow · doubts",
voteRed: "Red · against",
voteOwnNote: "You created this vote, so you cannot vote in it: whoever proposes stays neutral.",
voteKindLabel: "Type of vote",
voteKind_classic: "Yes / No",
voteKindHint_classic: "Yes, no, abstain and the usual options",
voteKind_traffic: "Traffic light",
voteKindHint_traffic: "Green agree · yellow doubts · red against, like in the assembly",
voteKind_scale: "Thermometer",
voteKindHint_scale: "From 1 (cold) to 5 (hot)",
deleteConfirmQuestion: "Delete it for good?",
deleteConfirmYes: "Yes, delete",
welcomeBannerTextMobile: "Add your name and photo so people know it is you.",
welcomeBannerActionMobile: "Set up",
footerPeersNone: "Not syncing with anyone yet",
footerPeersSome: "Syncing with {n}",
tribeMapUrlLabel: "Map link (optional)",
tribeHintPrivacy: "Private: only members see it, and it is encrypted. Public: the whole network sees it.",
tribeHintMode: "Open: any member can invite people. Strict: only whoever created it can invite.",
moduleBlockedText: "{name} is turned off.",
moduleBlockedAction: "Turn on {name}",
modulesOnCount: "{on} of {total} sections on",
modulesSaved: "Saved.",
karvanVoiceOn: "Voice on",
karvanCamOn: "Camera on",
karvanNoCam: "No camera permission: the call goes on with voice only.",
karvanNoMic: "No microphone permission. You can allow it in the phone settings; the chat still works.",
karvanMic: "Mic",
karvanCam: "Camera",
karvanHangUp: "Hang up",
karvanInviteLabel: "Invite someone",
karvanInviteHint: "Paste their @id: it is on their profile, under their name.",
karvanRelayHuman: "Through your node",
karvanP2pHuman: "Direct and encrypted",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Pick how Oasis looks on your phone. It applies as soon as you tap it.",
workflowsDescriptionMobile: "A workflow sets which modules are on. On the phone your skin stays as it is.",
@ -85,6 +126,47 @@ module.exports = {
spreadMore: "more"
},
es: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "Más",
tribeSectionsLabel: "Secciones",
voteYes: "Sí",
voteNo: "No",
voteAbstention: "Abstención",
voteGreen: "Verde · de acuerdo",
voteYellow: "Amarillo · dudas",
voteRed: "Rojo · en contra",
voteOwnNote: "Has creado esta votación, así que no puedes votar en ella: quien propone se queda neutral.",
voteKindLabel: "Tipo de votación",
voteKind_classic: "Sí / No",
voteKindHint_classic: "Sí, no, abstención y las opciones de siempre",
voteKind_traffic: "Semáforo",
voteKindHint_traffic: "Verde de acuerdo · amarillo dudas · rojo en contra, como en asamblea",
voteKind_scale: "Termómetro",
voteKindHint_scale: "Del 1 (frío) al 5 (caliente)",
deleteConfirmQuestion: "¿Borrar para siempre?",
deleteConfirmYes: "Sí, borrar",
welcomeBannerTextMobile: "Pon tu nombre y tu foto para que te reconozcan.",
welcomeBannerActionMobile: "Ponerlos",
footerPeersNone: "Aún no sincronizas con nadie",
footerPeersSome: "Sincronizando con {n}",
tribeMapUrlLabel: "Enlace del mapa (opcional)",
tribeHintPrivacy: "Privada: solo la ven sus miembros y va cifrada. Pública: la ve toda la red.",
tribeHintMode: "Abierta: cualquier miembro puede invitar. Estricta: solo invita quien la creó.",
moduleBlockedText: "{name} está desactivado.",
moduleBlockedAction: "Activar {name}",
modulesOnCount: "{on} de {total} secciones activas",
modulesSaved: "Guardado.",
karvanVoiceOn: "Voz activa",
karvanCamOn: "Cámara activa",
karvanNoCam: "Sin permiso de cámara: la llamada sigue solo con voz.",
karvanNoMic: "Sin permiso de micrófono. Puedes darlo en los ajustes del móvil; el chat sigue funcionando.",
karvanMic: "Micro",
karvanCam: "Cámara",
karvanHangUp: "Colgar",
karvanInviteLabel: "Invitar a alguien",
karvanInviteHint: "Pega su @id: está en su perfil, debajo del nombre.",
karvanRelayHuman: "A través de tu nodo",
karvanP2pHuman: "Directo y cifrado",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Elige cómo se ve Oasis en tu móvil. Se aplica al tocarlo.",
workflowsDescriptionMobile: "Un workflow fija qué módulos están activos. En el móvil tu skin se queda como está.",
@ -159,6 +241,47 @@ module.exports = {
totalspreads: "Réplicas totales"
},
fr: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "Plus",
tribeSectionsLabel: "Sections",
voteYes: "Oui",
voteNo: "Non",
voteAbstention: "Abstention",
voteGreen: "Vert · d'accord",
voteYellow: "Jaune · doutes",
voteRed: "Rouge · contre",
voteOwnNote: "Vous avez créé ce vote, vous ne pouvez donc pas y voter : qui propose reste neutre.",
voteKindLabel: "Type de vote",
voteKind_classic: "Oui / Non",
voteKindHint_classic: "Oui, non, abstention et les options habituelles",
voteKind_traffic: "Feu tricolore",
voteKindHint_traffic: "Vert d'accord · jaune doutes · rouge contre, comme en assemblée",
voteKind_scale: "Thermomètre",
voteKindHint_scale: "De 1 (froid) à 5 (chaud)",
deleteConfirmQuestion: "Supprimer définitivement ?",
deleteConfirmYes: "Oui, supprimer",
welcomeBannerTextMobile: "Ajoutez votre nom et votre photo pour qu'on vous reconnaisse.",
welcomeBannerActionMobile: "Ajouter",
footerPeersNone: "Aucune synchronisation pour l'instant",
footerPeersSome: "Synchronisé avec {n}",
tribeMapUrlLabel: "Lien de la carte (facultatif)",
tribeHintPrivacy: "Privée : seuls ses membres la voient, et elle est chiffrée. Publique : tout le réseau la voit.",
tribeHintMode: "Ouverte : tout membre peut inviter. Stricte : seule la personne qui l'a créée invite.",
moduleBlockedText: "{name} est désactivé.",
moduleBlockedAction: "Activer {name}",
modulesOnCount: "{on} sections actives sur {total}",
modulesSaved: "Enregistré.",
karvanVoiceOn: "Voix active",
karvanCamOn: "Caméra active",
karvanNoCam: "Pas d'accès à la caméra : l'appel continue en voix seule.",
karvanNoMic: "Pas d'accès au micro. Vous pouvez l'autoriser dans les réglages du téléphone ; le chat fonctionne toujours.",
karvanMic: "Micro",
karvanCam: "Caméra",
karvanHangUp: "Raccrocher",
karvanInviteLabel: "Inviter quelqu'un",
karvanInviteHint: "Collez son @id : il est sur son profil, sous son nom.",
karvanRelayHuman: "Via votre nœud",
karvanP2pHuman: "Direct et chiffré",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Choisis l'apparence d'Oasis sur ton téléphone. Elle s'applique dès que tu touches.",
workflowsDescriptionMobile: "Un workflow fixe les modules actifs. Sur le téléphone, ton thème ne change pas.",
@ -227,6 +350,47 @@ module.exports = {
spreadMore: "plus"
},
eu: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "Gehiago",
tribeSectionsLabel: "Atalak",
voteYes: "Bai",
voteNo: "Ez",
voteAbstention: "Abstentzioa",
voteGreen: "Berdea · ados",
voteYellow: "Horia · zalantzak",
voteRed: "Gorria · aurka",
voteOwnNote: "Bozketa hau zuk sortu duzu, beraz ezin duzu bozkatu: proposatzen duena neutral geratzen da.",
voteKindLabel: "Bozketa mota",
voteKind_classic: "Bai / Ez",
voteKindHint_classic: "Bai, ez, abstentzioa eta ohiko aukerak",
voteKind_traffic: "Semaforoa",
voteKindHint_traffic: "Berdea ados · horia zalantzak · gorria aurka, batzarrean bezala",
voteKind_scale: "Termometroa",
voteKindHint_scale: "1etik (hotza) 5era (beroa)",
deleteConfirmQuestion: "Betiko ezabatu?",
deleteConfirmYes: "Bai, ezabatu",
welcomeBannerTextMobile: "Jarri zure izena eta argazkia ezagutu zaitzaten.",
welcomeBannerActionMobile: "Jarri",
footerPeersNone: "Oraindik ez zaude inorekin sinkronizatzen",
footerPeersSome: "{n}-rekin sinkronizatzen",
tribeMapUrlLabel: "Maparen esteka (aukerakoa)",
tribeHintPrivacy: "Pribatua: kideek bakarrik ikusten dute, eta zifratuta dago. Publikoa: sare osoak ikusten du.",
tribeHintMode: "Irekia: edozein kidek gonbida dezake. Zorrotza: sortu zuenak bakarrik gonbidatzen du.",
moduleBlockedText: "{name} desaktibatuta dago.",
moduleBlockedAction: "Aktibatu {name}",
modulesOnCount: "{total} ataletatik {on} aktibo",
modulesSaved: "Gordeta.",
karvanVoiceOn: "Ahotsa piztuta",
karvanCamOn: "Kamera piztuta",
karvanNoCam: "Kamerarako baimenik ez: deiak ahotsarekin bakarrik jarraitzen du.",
karvanNoMic: "Mikrofonorako baimenik ez. Mugikorraren ezarpenetan eman dezakezu; txatak funtzionatzen jarraitzen du.",
karvanMic: "Mikroa",
karvanCam: "Kamera",
karvanHangUp: "Eskegi",
karvanInviteLabel: "Norbait gonbidatu",
karvanInviteHint: "Itsatsi bere @id-a: bere profilean dago, izenaren azpian.",
karvanRelayHuman: "Zure nodoaren bidez",
karvanP2pHuman: "Zuzena eta zifratua",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Aukeratu nolakoa den Oasis zure mugikorrean. Ukitu bezain laster aplikatzen da.",
workflowsDescriptionMobile: "Workflow batek zein modulu dauden aktibo finkatzen du. Mugikorrean zure itxura ez da aldatzen.",
@ -295,6 +459,47 @@ module.exports = {
spreadMore: "gehiago"
},
de: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "Mehr",
tribeSectionsLabel: "Bereiche",
voteYes: "Ja",
voteNo: "Nein",
voteAbstention: "Enthaltung",
voteGreen: "Grün · einverstanden",
voteYellow: "Gelb · Zweifel",
voteRed: "Rot · dagegen",
voteOwnNote: "Du hast diese Abstimmung erstellt und kannst daher nicht abstimmen: wer vorschlägt, bleibt neutral.",
voteKindLabel: "Art der Abstimmung",
voteKind_classic: "Ja / Nein",
voteKindHint_classic: "Ja, nein, Enthaltung und die üblichen Optionen",
voteKind_traffic: "Ampel",
voteKindHint_traffic: "Grün einverstanden · gelb Zweifel · rot dagegen, wie im Plenum",
voteKind_scale: "Thermometer",
voteKindHint_scale: "Von 1 (kalt) bis 5 (heiß)",
deleteConfirmQuestion: "Endgültig löschen?",
deleteConfirmYes: "Ja, löschen",
welcomeBannerTextMobile: "Füge deinen Namen und dein Foto hinzu, damit man dich erkennt.",
welcomeBannerActionMobile: "Einrichten",
footerPeersNone: "Noch mit niemandem synchronisiert",
footerPeersSome: "Synchronisiert mit {n}",
tribeMapUrlLabel: "Kartenlink (optional)",
tribeHintPrivacy: "Privat: nur Mitglieder sehen sie, und sie ist verschlüsselt. Öffentlich: das ganze Netz sieht sie.",
tribeHintMode: "Offen: jedes Mitglied kann einladen. Streng: nur wer sie erstellt hat, lädt ein.",
moduleBlockedText: "{name} ist ausgeschaltet.",
moduleBlockedAction: "{name} einschalten",
modulesOnCount: "{on} von {total} Bereichen aktiv",
modulesSaved: "Gespeichert.",
karvanVoiceOn: "Stimme an",
karvanCamOn: "Kamera an",
karvanNoCam: "Keine Kameraberechtigung: der Anruf läuft nur mit Stimme weiter.",
karvanNoMic: "Keine Mikrofonberechtigung. Du kannst sie in den Telefoneinstellungen erlauben; der Chat funktioniert weiter.",
karvanMic: "Mikro",
karvanCam: "Kamera",
karvanHangUp: "Auflegen",
karvanInviteLabel: "Jemanden einladen",
karvanInviteHint: "Füge die @id ein: sie steht im Profil, unter dem Namen.",
karvanRelayHuman: "Über deinen Knoten",
karvanP2pHuman: "Direkt und verschlüsselt",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Wähle, wie Oasis auf deinem Handy aussieht. Es wird beim Antippen übernommen.",
workflowsDescriptionMobile: "Ein Workflow legt fest, welche Module aktiv sind. Auf dem Handy bleibt dein Skin, wie er ist.",
@ -363,6 +568,47 @@ module.exports = {
spreadMore: "mehr"
},
it: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "Altro",
tribeSectionsLabel: "Sezioni",
voteYes: "Sì",
voteNo: "No",
voteAbstention: "Astensione",
voteGreen: "Verde · d'accordo",
voteYellow: "Giallo · dubbi",
voteRed: "Rosso · contrario",
voteOwnNote: "Hai creato tu questa votazione, quindi non puoi votare: chi propone resta neutrale.",
voteKindLabel: "Tipo di votazione",
voteKind_classic: "Sì / No",
voteKindHint_classic: "Sì, no, astensione e le opzioni di sempre",
voteKind_traffic: "Semaforo",
voteKindHint_traffic: "Verde d'accordo · giallo dubbi · rosso contrario, come in assemblea",
voteKind_scale: "Termometro",
voteKindHint_scale: "Da 1 (freddo) a 5 (caldo)",
deleteConfirmQuestion: "Eliminare per sempre?",
deleteConfirmYes: "Sì, elimina",
welcomeBannerTextMobile: "Aggiungi nome e foto perché ti riconoscano.",
welcomeBannerActionMobile: "Aggiungi",
footerPeersNone: "Non sincronizzi ancora con nessuno",
footerPeersSome: "In sincronia con {n}",
tribeMapUrlLabel: "Link della mappa (facoltativo)",
tribeHintPrivacy: "Privata: la vedono solo i membri ed è cifrata. Pubblica: la vede tutta la rete.",
tribeHintMode: "Aperta: qualsiasi membro può invitare. Rigida: invita solo chi l'ha creata.",
moduleBlockedText: "{name} è disattivato.",
moduleBlockedAction: "Attiva {name}",
modulesOnCount: "{on} sezioni attive su {total}",
modulesSaved: "Salvato.",
karvanVoiceOn: "Voce attiva",
karvanCamOn: "Fotocamera attiva",
karvanNoCam: "Nessun permesso per la fotocamera: la chiamata continua solo in voce.",
karvanNoMic: "Nessun permesso per il microfono. Puoi darlo nelle impostazioni del telefono; la chat continua a funzionare.",
karvanMic: "Micro",
karvanCam: "Fotocamera",
karvanHangUp: "Riaggancia",
karvanInviteLabel: "Invita qualcuno",
karvanInviteHint: "Incolla il suo @id: è nel suo profilo, sotto il nome.",
karvanRelayHuman: "Tramite il tuo nodo",
karvanP2pHuman: "Diretto e cifrato",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Scegli come appare Oasis sul tuo telefono. Si applica appena lo tocchi.",
workflowsDescriptionMobile: "Un workflow stabilisce quali moduli sono attivi. Sul telefono la tua skin resta com'è.",
@ -431,6 +677,47 @@ module.exports = {
spreadMore: "altro"
},
pt: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "Mais",
tribeSectionsLabel: "Secções",
voteYes: "Sim",
voteNo: "Não",
voteAbstention: "Abstenção",
voteGreen: "Verde · de acordo",
voteYellow: "Amarelo · dúvidas",
voteRed: "Vermelho · contra",
voteOwnNote: "Criaste esta votação, por isso não podes votar nela: quem propõe fica neutro.",
voteKindLabel: "Tipo de votação",
voteKind_classic: "Sim / Não",
voteKindHint_classic: "Sim, não, abstenção e as opções de sempre",
voteKind_traffic: "Semáforo",
voteKindHint_traffic: "Verde de acordo · amarelo dúvidas · vermelho contra, como em assembleia",
voteKind_scale: "Termómetro",
voteKindHint_scale: "De 1 (frio) a 5 (quente)",
deleteConfirmQuestion: "Apagar para sempre?",
deleteConfirmYes: "Sim, apagar",
welcomeBannerTextMobile: "Põe o teu nome e a tua foto para que te reconheçam.",
welcomeBannerActionMobile: "Pôr",
footerPeersNone: "Ainda não sincronizas com ninguém",
footerPeersSome: "A sincronizar com {n}",
tribeMapUrlLabel: "Ligação do mapa (opcional)",
tribeHintPrivacy: "Privada: só os membros a veem e vai cifrada. Pública: toda a rede a vê.",
tribeHintMode: "Aberta: qualquer membro pode convidar. Estrita: só convida quem a criou.",
moduleBlockedText: "{name} está desativado.",
moduleBlockedAction: "Ativar {name}",
modulesOnCount: "{on} de {total} secções ativas",
modulesSaved: "Guardado.",
karvanVoiceOn: "Voz ativa",
karvanCamOn: "Câmara ativa",
karvanNoCam: "Sem permissão de câmara: a chamada continua só com voz.",
karvanNoMic: "Sem permissão de microfone. Podes dá-la nas definições do telemóvel; o chat continua a funcionar.",
karvanMic: "Micro",
karvanCam: "Câmara",
karvanHangUp: "Desligar",
karvanInviteLabel: "Convidar alguém",
karvanInviteHint: "Cola o @id da pessoa: está no perfil, por baixo do nome.",
karvanRelayHuman: "Através do teu nó",
karvanP2pHuman: "Direto e cifrado",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Escolhe como o Oasis aparece no teu telemóvel. Aplica-se ao tocar.",
workflowsDescriptionMobile: "Um workflow define que módulos estão ativos. No telemóvel o teu tema fica como está.",
@ -499,6 +786,47 @@ module.exports = {
spreadMore: "mais"
},
zh: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "更多",
tribeSectionsLabel: "板块",
voteYes: "赞成",
voteNo: "反对",
voteAbstention: "弃权",
voteGreen: "绿 · 同意",
voteYellow: "黄 · 有疑问",
voteRed: "红 · 反对",
voteOwnNote: "这是你发起的投票,所以你不能投票:发起人保持中立。",
voteKindLabel: "投票类型",
voteKind_classic: "是 / 否",
voteKindHint_classic: "赞成、反对、弃权及常用选项",
voteKind_traffic: "红绿灯",
voteKindHint_traffic: "绿同意 · 黄有疑问 · 红反对,和集会上一样",
voteKind_scale: "温度计",
voteKindHint_scale: "从 1(冷)到 5(热)",
deleteConfirmQuestion: "永久删除?",
deleteConfirmYes: "是,删除",
welcomeBannerTextMobile: "添加你的名字和照片,让大家认出你。",
welcomeBannerActionMobile: "设置",
footerPeersNone: "尚未与任何人同步",
footerPeersSome: "正在与 {n} 个节点同步",
tribeMapUrlLabel: "地图链接(可选)",
tribeHintPrivacy: "私密:只有成员能看到,并且加密。公开:整个网络都能看到。",
tribeHintMode: "开放:任何成员都可以邀请。严格:只有创建者可以邀请。",
moduleBlockedText: "{name} 已关闭。",
moduleBlockedAction: "开启 {name}",
modulesOnCount: "已开启 {on}/{total} 个板块",
modulesSaved: "已保存。",
karvanVoiceOn: "语音已开",
karvanCamOn: "摄像头已开",
karvanNoCam: "没有摄像头权限:通话仅以语音继续。",
karvanNoMic: "没有麦克风权限。可以在手机设置中允许;聊天仍可使用。",
karvanMic: "麦克风",
karvanCam: "摄像头",
karvanHangUp: "挂断",
karvanInviteLabel: "邀请某人",
karvanInviteHint: "粘贴对方的 @id:在其个人资料中,名字下方。",
karvanRelayHuman: "通过你的节点",
karvanP2pHuman: "直连且加密",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "选择 Oasis 在手机上的外观。点一下就会生效。",
workflowsDescriptionMobile: "工作流决定启用哪些模块。在手机上,你的外观保持不变。",
@ -567,6 +895,47 @@ module.exports = {
spreadMore: "更多"
},
ar: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "المزيد",
tribeSectionsLabel: "الأقسام",
voteYes: "نعم",
voteNo: "لا",
voteAbstention: "امتناع",
voteGreen: "أخضر · موافق",
voteYellow: "أصفر · شكوك",
voteRed: "أحمر · ضد",
voteOwnNote: "أنت من أنشأ هذا التصويت، لذا لا يمكنك التصويت فيه: من يقترح يبقى محايدًا.",
voteKindLabel: "نوع التصويت",
voteKind_classic: "نعم / لا",
voteKindHint_classic: "نعم، لا، امتناع والخيارات المعتادة",
voteKind_traffic: "إشارة المرور",
voteKindHint_traffic: "أخضر موافق · أصفر شكوك · أحمر ضد، كما في الجمعية",
voteKind_scale: "مقياس الحرارة",
voteKindHint_scale: "من 1 (بارد) إلى 5 (ساخن)",
deleteConfirmQuestion: "حذف نهائيًا؟",
deleteConfirmYes: "نعم، احذف",
welcomeBannerTextMobile: "أضف اسمك وصورتك ليتعرّف عليك الآخرون.",
welcomeBannerActionMobile: "إضافة",
footerPeersNone: "لا مزامنة مع أحد بعد",
footerPeersSome: "مزامنة مع {n}",
tribeMapUrlLabel: "رابط الخريطة (اختياري)",
tribeHintPrivacy: "خاصة: يراها أعضاؤها فقط وهي مشفّرة. عامة: تراها الشبكة كلها.",
tribeHintMode: "مفتوحة: يمكن لأي عضو أن يدعو. صارمة: لا يدعو إلا من أنشأها.",
moduleBlockedText: "{name} مُعطّل.",
moduleBlockedAction: "تفعيل {name}",
modulesOnCount: "{on} من {total} أقسام مفعّلة",
modulesSaved: "تم الحفظ.",
karvanVoiceOn: "الصوت مفعّل",
karvanCamOn: "الكاميرا مفعّلة",
karvanNoCam: "لا إذن للكاميرا: تستمر المكالمة بالصوت فقط.",
karvanNoMic: "لا إذن للميكروفون. يمكنك السماح به من إعدادات الهاتف؛ الدردشة تعمل.",
karvanMic: "ميكروفون",
karvanCam: "كاميرا",
karvanHangUp: "إنهاء",
karvanInviteLabel: "ادعُ شخصًا",
karvanInviteHint: "الصق معرّفه @id: تجده في ملفه تحت اسمه.",
karvanRelayHuman: "عبر عقدتك",
karvanP2pHuman: "مباشر ومشفّر",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "اختر مظهر Oasis على هاتفك. يُطبَّق بمجرد اللمس.",
workflowsDescriptionMobile: "يحدد سير العمل الوحدات المفعّلة. على الهاتف يبقى مظهرك كما هو.",
@ -635,6 +1004,47 @@ module.exports = {
spreadMore: "المزيد"
},
hi: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "और",
tribeSectionsLabel: "खंड",
voteYes: "हाँ",
voteNo: "नहीं",
voteAbstention: "तटस्थ",
voteGreen: "हरा · सहमत",
voteYellow: "पीला · संदेह",
voteRed: "लाल · विरोध",
voteOwnNote: "यह मतदान आपने बनाया है, इसलिए आप इसमें वोट नहीं दे सकते: प्रस्ताव रखने वाला तटस्थ रहता है।",
voteKindLabel: "मतदान का प्रकार",
voteKind_classic: "हाँ / नहीं",
voteKindHint_classic: "हाँ, नहीं, तटस्थ और सामान्य विकल्प",
voteKind_traffic: "ट्रैफ़िक लाइट",
voteKindHint_traffic: "हरा सहमत · पीला संदेह · लाल विरोध, सभा की तरह",
voteKind_scale: "थर्मामीटर",
voteKindHint_scale: "1 (ठंडा) से 5 (गरम) तक",
deleteConfirmQuestion: "हमेशा के लिए हटाएँ?",
deleteConfirmYes: "हाँ, हटाएँ",
welcomeBannerTextMobile: "अपना नाम और फ़ोटो जोड़ें ताकि लोग आपको पहचानें।",
welcomeBannerActionMobile: "जोड़ें",
footerPeersNone: "अभी किसी से सिंक नहीं",
footerPeersSome: "{n} से सिंक हो रहा है",
tribeMapUrlLabel: "नक्शे का लिंक (वैकल्पिक)",
tribeHintPrivacy: "निजी: केवल सदस्य देखते हैं, और यह एन्क्रिप्टेड है। सार्वजनिक: पूरा नेटवर्क देखता है।",
tribeHintMode: "खुला: कोई भी सदस्य आमंत्रित कर सकता है। सख़्त: केवल बनाने वाला आमंत्रित करता है।",
moduleBlockedText: "{name} बंद है।",
moduleBlockedAction: "{name} चालू करें",
modulesOnCount: "{total} में से {on} खंड चालू",
modulesSaved: "सहेजा गया।",
karvanVoiceOn: "आवाज़ चालू",
karvanCamOn: "कैमरा चालू",
karvanNoCam: "कैमरे की अनुमति नहीं: कॉल केवल आवाज़ से जारी है।",
karvanNoMic: "माइक्रोफ़ोन की अनुमति नहीं। फ़ोन की सेटिंग में दे सकते हैं; चैट चलती रहेगी।",
karvanMic: "माइक",
karvanCam: "कैमरा",
karvanHangUp: "कॉल काटें",
karvanInviteLabel: "किसी को आमंत्रित करें",
karvanInviteHint: "उनका @id चिपकाएँ: यह उनकी प्रोफ़ाइल में नाम के नीचे है।",
karvanRelayHuman: "आपके नोड के ज़रिए",
karvanP2pHuman: "सीधा और एन्क्रिप्टेड",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "चुनें कि आपके फ़ोन पर Oasis कैसा दिखे। छूते ही लागू हो जाता है।",
workflowsDescriptionMobile: "वर्कफ़्लो तय करता है कि कौन-से मॉड्यूल चालू हैं। फ़ोन पर आपकी स्किन वैसी ही रहती है।",
@ -703,6 +1113,47 @@ module.exports = {
spreadMore: "और"
},
ru: {
// usabilidad (19/10/2026): votaciones, borrar, modulos, Karvan, pie
tribeSectionsMore: "Ещё",
tribeSectionsLabel: "Разделы",
voteYes: "Да",
voteNo: "Нет",
voteAbstention: "Воздержаться",
voteGreen: "Зелёный · за",
voteYellow: "Жёлтый · сомнения",
voteRed: "Красный · против",
voteOwnNote: "Вы создали это голосование, поэтому не можете в нём голосовать: кто предлагает, остаётся нейтральным.",
voteKindLabel: "Тип голосования",
voteKind_classic: "Да / Нет",
voteKindHint_classic: "Да, нет, воздержаться и обычные варианты",
voteKind_traffic: "Светофор",
voteKindHint_traffic: "Зелёный за · жёлтый сомнения · красный против, как на собрании",
voteKind_scale: "Термометр",
voteKindHint_scale: "От 1 (холодно) до 5 (горячо)",
deleteConfirmQuestion: "Удалить навсегда?",
deleteConfirmYes: "Да, удалить",
welcomeBannerTextMobile: "Добавьте имя и фото, чтобы вас узнавали.",
welcomeBannerActionMobile: "Добавить",
footerPeersNone: "Пока ни с кем не синхронизировано",
footerPeersSome: "Синхронизация с {n}",
tribeMapUrlLabel: "Ссылка на карту (необязательно)",
tribeHintPrivacy: "Закрытое: видят только участники, и оно зашифровано. Открытое: видит вся сеть.",
tribeHintMode: "Открытое: приглашать может любой участник. Строгое: приглашает только создатель.",
moduleBlockedText: "{name} выключен.",
moduleBlockedAction: "Включить {name}",
modulesOnCount: "Включено {on} из {total} разделов",
modulesSaved: "Сохранено.",
karvanVoiceOn: "Голос включён",
karvanCamOn: "Камера включена",
karvanNoCam: "Нет доступа к камере: звонок продолжается только голосом.",
karvanNoMic: "Нет доступа к микрофону. Его можно разрешить в настройках телефона; чат продолжает работать.",
karvanMic: "Микрофон",
karvanCam: "Камера",
karvanHangUp: "Завершить",
karvanInviteLabel: "Пригласить кого-нибудь",
karvanInviteHint: "Вставьте его @id: он в профиле, под именем.",
karvanRelayHuman: "Через ваш узел",
karvanP2pHuman: "Напрямую и зашифровано",
// skins y workflows en el movil (settings_view.js)
skinIntroMobile: "Выберите, как Oasis выглядит на телефоне. Применяется сразу по нажатию.",
workflowsDescriptionMobile: "Рабочий процесс задаёт, какие модули включены. На телефоне ваше оформление не меняется.",

View file

@ -117,7 +117,7 @@
function setLive(on) {
if (!liveTxt) return;
liveTxt.textContent = on ? "🔒 P2P (WebRTC)" : "server relay";
liveTxt.textContent = on ? (liveWrap && liveWrap.getAttribute("data-txt-p2p") || "🔒 P2P (WebRTC)") : (liveWrap && liveWrap.getAttribute("data-txt-relay") || "server relay");
if (liveWrap) liveWrap.className = "karvan-live" + (on ? " on" : "");
}
@ -173,7 +173,9 @@
// --- media / videollamada (Fase 1) ---
function setCallActive(on) { if (callRoot) callRoot.className = "karvan-call" + (on ? " active" : ""); }
function setStatus(msg) { if (statusEl) statusEl.textContent = msg || ""; }
function statusText() { var p = []; if (micActive) p.push("mic ON"); if (camActive) p.push("cam ON"); return p.length ? p.join(" · ") : ""; }
// textos traducidos: los pone la vista en data-txt-* de #karvan-call
function txt(k, def) { return (callRoot && callRoot.getAttribute("data-txt-" + k)) || def; }
function statusText() { var p = []; if (micActive) p.push(txt("mic", "Voice on")); if (camActive) p.push(txt("cam", "Camera on")); return p.length ? p.join(" · ") : ""; }
function updateBtns() {
if (btnMic) btnMic.className = "kc-btn" + (micActive ? " on" : "");
if (btnCam) btnCam.className = "kc-btn" + (camActive ? " on" : "");
@ -215,7 +217,7 @@
}
function onMediaErr(kind, x) {
var name = (x && x.name) || "", msg = (x && x.message) || String(x);
if (name === "NotAllowedError" || /denied|permission/i.test(msg)) setStatus("⛔ Cámara/micro denegados (en el móvil, pendiente del wrapper). El chat sigue.");
if (name === "NotAllowedError" || /denied|permission/i.test(msg)) setStatus(kind === "video" ? txt("nocam", "No camera permission: the call goes on with voice only.") : txt("nomic", "No microphone permission. You can allow it in the phone settings; the chat still works."));
else if (name === "NotFoundError") setStatus("⚠ No hay " + (kind === "audio" ? "micrófono" : "cámara") + ".");
else if (name === "NotReadableError") setStatus("⚠ Dispositivo ocupado.");
else setStatus("⚠ " + (name || "error") + ": " + msg);
@ -230,16 +232,13 @@
setCallActive(true); updateBtns(); setStatus(statusText());
}).catch(function (x) { onMediaErr(kind, x); });
}
// Llamar empieza solo con voz: pedir la camara de entrada hacia que, si se
// negaba, no arrancara nada aunque el micro estuviera permitido. El video se
// anade despues con su boton
function startCall() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
setStatus("📞 pidiendo cámara y micro…");
navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function (s) {
s.getTracks().forEach(addLocalTrack);
micActive = true; camActive = true; setCallActive(true); updateBtns(); setStatus(statusText());
}).catch(function (x) {
if (x && x.name === "NotFoundError") { getMedia("audio"); return; } // sin cámara → intenta solo audio
onMediaErr("call", x);
});
if (micActive) return;
getMedia("audio");
}
function hangUp() {
if (localStream) localStream.getTracks().forEach(function (t) { t.stop(); localStream.removeTrack(t); });