interfaz: lanzador de modulos en lugar de los hexagonos

La portada ordena los modulos por secciones en una rejilla de iconos, como el
cajon de apps del movil. Solo salen los modulos activos y las secciones que
tienen alguno; Personal y Social de la topbar parten el lanzador en dos.

Fuera los enlaces que ya no existen en 1.3.1 (/legacy, /public/latest/*) y
dentro los modulos nuevos: wiki, rooms, emergencias, mailing, campanas...

1.3.1 trae en mobile.css su propio chrome movil (cabecera fija y cajones); se
apaga desde el tema para que no se monte encima del de la rama.
This commit is contained in:
oasis-bot 2026-10-18 02:52:17 +02:00
parent a11a730c26
commit 5a04113330
2 changed files with 152 additions and 166 deletions

View file

@ -1,6 +1,6 @@
"use strict";
// Navegacion propia de la rama experimental para movil: topbar, barra inferior
// personalizable y rejilla de hexagonos.
// personalizable y lanzador de modulos.
//
// Vive fuera de main_views.js a proposito: upstream reescribe ese fichero en cada
// release (de 0.9.1 a 0.9.5 movio +256/-502 lineas) y esta interfaz se perdia en
@ -12,7 +12,7 @@ const { getConfig } = require("../configs/config-manager.js");
const sharedState = require("../configs/shared-state");
module.exports = ({ i18n }) => {
// === Oasis UX (portado de 0.8.7_UX): topbar + bottombar + hive ===
// === Oasis UX (portado de 0.8.7_UX): topbar + bottombar + lanzador ===
// topbar fija arriba: [logo] [Personal] [Community] [avatar] en una línea (sin título OASIS)
const renderMobileTopbar = () => {
// solo en movil: en escritorio manda la cabecera de upstream
@ -39,6 +39,8 @@ module.exports = ({ i18n }) => {
{ key: 'tribes', href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' },
{ key: 'chats', href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' },
{ key: 'karvan', href: '/karvan', glyph: '⧗', labelKey: 'karvanTitle', mod: 'karvanMod' },
{ key: 'rooms', href: '/rooms', glyph: 'ꕢ', labelKey: 'roomsTitle', mod: 'roomsMod' },
{ key: 'wiki', href: '/wiki', glyph: 'ꖿ', labelKey: 'wikiTitle', mod: 'wikiMod' },
{ key: 'polls', href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' },
{ key: 'blogs', href: '/blogs', glyph: 'ꔫ', labelKey: 'blogTitle', mod: 'blogsMod' },
{ key: 'data', href: '/data', glyph: 'ꘑ', labelKey: 'dataTitle', mod: 'dataMod' },
@ -119,71 +121,104 @@ module.exports = ({ i18n }) => {
);
};
// Lanzador de modulos de la home: secciones con una rejilla de iconos, como el
// cajon de apps del movil. Solo salen los modulos activos y las secciones que
// tienen alguno, asi que con un workflow corto (activistas) cabe en una pantalla.
const GROUPS = [
{ id: 'community', titleKey: 'menuNetwork', side: 'community', mods: [
{ href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null },
{ href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' },
{ href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' },
{ href: '/karvan', glyph: '⧗', labelKey: 'karvanTitle', mod: 'karvanMod' },
{ href: '/rooms', glyph: 'ꕢ', labelKey: 'roomsTitle', mod: 'roomsMod' },
{ href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' },
{ href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' },
{ href: '/wiki', glyph: 'ꖿ', labelKey: 'wikiTitle', mod: 'wikiMod' },
{ href: '/blogs', glyph: 'ꔫ', labelKey: 'blogTitle', mod: 'blogsMod' },
{ href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' },
{ href: '/mentions', glyph: '✺', labelKey: 'mentions', mod: null },
{ href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' },
{ href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' },
{ href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' },
{ href: '/graphos', glyph: '⌬', labelKey: 'modulesGraphosLabel', mod: 'graphosMod' },
{ href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null },
{ href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' } ] },
{ id: 'media', titleKey: 'menuMedia', side: 'community', mods: [
{ href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' },
{ href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' },
{ href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' },
{ href: '/podcasts', glyph: '◉', labelKey: 'podcastsTitle', mod: 'podcastsMod' },
{ href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' },
{ href: '/files', glyph: 'ꖃ', labelKey: 'filesTitle', mod: 'filesMod' },
{ href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' },
{ href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' } ] },
{ id: 'creative', titleKey: 'menuCreative', side: 'community', mods: [
{ href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' },
{ href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' },
{ href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' } ] },
{ id: 'office', titleKey: 'menuOffice', side: 'personal', mods: [
{ href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' },
{ href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' },
{ href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' },
{ href: '/campaigns', glyph: '⚑', labelKey: 'campaignsTitle', mod: 'campaignsMod' },
{ href: '/mailing', glyph: '✉', labelKey: 'mailingTitle', mod: 'mailingMod' },
{ href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' },
{ href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' },
{ href: '/reports', glyph: 'ꕤ', labelKey: 'reportsTitle', mod: 'reportsMod' } ] },
{ id: 'governance', titleKey: 'menuGovernance', side: 'personal', mods: [
{ href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' },
{ href: '/emergencies', glyph: '✚', labelKey: 'emergenciesTitle', mod: 'emergenciesMod' },
{ href: '/school', glyph: 'ꗳ', labelKey: 'schoolTitle', mod: 'schoolMod' },
{ href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' },
{ href: '/parliament', glyph: 'ꗞ', labelKey: 'parliamentTitle', mod: 'parliamentMod' },
{ href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' } ] },
{ id: 'economy', titleKey: 'menuEconomy', side: 'personal', mods: [
{ href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' },
{ href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' },
{ href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' },
{ href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' },
{ href: '/housing', glyph: '⌂', labelKey: 'housingTitle', mod: 'housingMod' },
{ href: '/logistics', glyph: '⇄', labelKey: 'logisticsTitle', mod: 'logisticsMod' },
{ href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' },
{ href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' } ] },
{ id: 'personal', titleKey: 'menuPersonal', side: 'personal', mods: [
{ href: '/profile', glyph: '⚉', labelKey: 'profile', mod: null },
{ href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' },
{ href: '/favorites', glyph: 'ꘝ', labelKey: 'favoritesTitle', mod: 'favoritesMod' },
{ href: '/phone', glyph: '☏', labelKey: 'phoneTitle', mod: 'phoneMod' },
{ href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null },
{ href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' },
{ href: '/logs', glyph: 'ꗯ', labelKey: 'logsTitle', mod: 'logsMod' } ] },
{ id: 'tools', titleKey: 'menuTools', side: 'personal', mods: [
{ href: '/invites', glyph: 'ꔹ', labelKey: 'invites', mod: 'invitesMod' },
{ href: '/backup', glyph: 'ꖤ', labelKey: 'backupTitle', mod: 'backupMod' },
{ href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' },
{ href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' },
{ href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null },
{ href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null },
{ href: '/modules', glyph: 'ꗣ', labelKey: 'modules', mod: null },
{ href: '/settings', glyph: '⚙', labelKey: 'settings', mod: null } ] },
];
const renderHiveNav = () => {
if (process.env.OASIS_MOBILE !== '1') return "";
const cfg = getConfig();
const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined;
const CATS = [
{ id: 'personal', emoji: '⚉', title: i18n.menuPersonal, mods: [
{ href: '/profile', glyph: '⚉', labelKey: 'profile', mod: null }, { href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null },
{ href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, { href: '/favorites', glyph: 'ꘝ', labelKey: 'favoritesTitle', mod: 'favoritesMod' },
{ href: '/logs', glyph: 'ꗯ', labelKey: 'logsTitle', mod: 'logsMod' }, { href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' },
{ href: '/modules', glyph: 'ꗣ', labelKey: 'modules', mod: null }, { href: '/settings', glyph: '⚙', labelKey: 'settings', mod: null } ] },
{ id: 'governance', emoji: '⚖', title: i18n.menuGovernance, mods: [
{ href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null }, { href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' },
{ href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, { href: '/parliament', glyph: 'ꗞ', labelKey: 'parliamentTitle', mod: 'parliamentMod' },
{ href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' } ] },
{ id: 'office', emoji: '⌂', title: i18n.menuOffice, mods: [
{ href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, { href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' }, { href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' },
{ href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, { href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' },
{ href: '/reports', glyph: 'ꕥ', labelKey: 'reportsTitle', mod: 'reportsMod' } ] },
{ id: 'economy', emoji: '¤', title: i18n.menuEconomy, mods: [
{ href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, { href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' },
{ href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, { href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' },
{ href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, { href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' } ] },
{ id: 'tools', emoji: '⚒', title: i18n.menuTools, mods: [
{ href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, { href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null },
{ href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, { href: '/invites', glyph: 'ꔹ', labelKey: 'invites', mod: 'invitesMod' },
{ href: '/legacy', glyph: 'ꖤ', labelKey: 'legacy', mod: 'legacyMod' }, { href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null },
{ href: '/data', glyph: 'ꘑ', labelKey: 'dataTitle', mod: 'dataMod' } ] },
{ id: 'network', emoji: '☍', title: i18n.menuNetwork, mods: [
{ href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, { href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' },
{ href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, { href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' },
{ href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, { href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' },
{ href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, { href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' },
{ href: '/karvan', glyph: '⧗', labelKey: 'karvanTitle', mod: 'karvanMod' } ] },
{ id: 'blogs', emoji: '✦', title: i18n.menuBlogs, mods: [
{ href: '/blogs', glyph: 'ꔫ', labelKey: 'blogTitle', mod: 'blogsMod' }, { href: '/mentions', glyph: '✺', labelKey: 'mentions', mod: null }, { href: '/public/latest', glyph: '☄', labelKey: 'latest', mod: 'latestMod' },
{ href: '/public/latest/threads', glyph: '♺', labelKey: 'threads', mod: 'threadsMod' }, { href: '/public/latest/topics', glyph: 'ϟ', labelKey: 'topics', mod: 'topicsMod' },
{ href: '/public/latest/summaries', glyph: '※', labelKey: 'summaries', mod: 'summariesMod' }, { href: '/public/popular/day', glyph: '⌘', labelKey: 'popular', mod: 'popularMod' },
{ href: '/public/latest/extended', glyph: '∞', labelKey: 'multiverse', mod: 'multiverseMod' } ] },
{ id: 'creative', emoji: '✎', title: i18n.menuCreative, mods: [
{ href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, { href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' },
{ href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, { href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' } ] },
{ id: 'media', emoji: '▤', title: i18n.menuMedia, mods: [
{ href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, { href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' },
{ href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, { href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' },
{ href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' }, { href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' } ] },
{ id: 'fediverse', emoji: '⌬', title: i18n.fediverse, mods: [
{ href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' } ] },
];
// filtro server-side (lo pone el middleware desde ?hive=): personal = CATS[0..4], community = CATS[5..9], vacío = las 10
// Personal/Social de la topbar (lo pone el middleware desde ?hive=) parte el lanzador en dos
const hiveFilter = (typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || "";
const shown = hiveFilter === "personal" ? CATS.slice(0, 5) : hiveFilter === "community" ? CATS.slice(5) : CATS;
const rowSizes = shown.length <= 5 ? [3, 2] : [3, 4, 3];
const catHex = (c) => label({ for: `hive-${c.id}`, class: 'hex-cat' },
span({ class: 'hx-ico' }, c.emoji), span({ class: 'hx-lbl' }, c.title));
const modHex = (m) => a({ class: 'hex-mod', href: m.href },
span({ class: 'hx-ico' }, m.glyph), span({ class: 'hx-lbl' }, i18n[m.labelKey] || ''));
const radios = shown.map(c => input({ type: 'radio', name: 'hive-cat', id: `hive-${c.id}`, class: 'hive-radio' }));
const catRows = [];
let idx = 0;
for (const size of rowSizes) { catRows.push(div({ class: 'hive-row' }, ...shown.slice(idx, idx + size).map(catHex))); idx += size; }
const grid = div({ class: 'hive-grid hive-cats' }, ...catRows);
const panels = shown.map(c => div({ class: 'hive-panel', id: `hive-panel-${c.id}` },
div({ class: 'hive-mods' }, ...c.mods.filter(m => modOn(m.mod)).map(modHex))
));
return div({ class: 'hive-nav' }, ...radios, grid, ...panels);
const app = (m) => a({ class: 'ln-app', href: m.href },
span({ class: 'ln-ico', 'aria-hidden': 'true' }, m.glyph),
span({ class: 'ln-lbl' }, i18n[m.labelKey] || m.href.slice(1)));
const groups = GROUPS
.filter(g => !hiveFilter || g.side === hiveFilter)
.map(g => ({ g, mods: g.mods.filter(m => modOn(m.mod)) }))
.filter(x => x.mods.length);
return nav({ class: 'launcher', 'aria-label': i18n.modules || 'Modules' },
...groups.map(({ g, mods }) => div({ class: 'ln-group' },
h3({ class: 'ln-title' }, i18n[g.titleKey] || g.id),
div({ class: 'ln-grid' }, ...mods.map(app))
))
);
};