Systemmenue im Logo, in beiden Leisten

Das ■ HONEYOS ist jetzt ein Menue statt eines Links: Schreibtisch,
Einstellungen, HoneyBar einklappen, Abmelden. In fremden Apps sind es
Links auf den Schreibtisch (?einstellungen=1 / ?abmelden=1), weil die
HoneyBar dort nicht schreiben darf — Sitzung und CSRF-Pruefwort ueber-
leben die Site-Grenze nicht, und ein Abmelden per Aufruf koennte jede
Seite ausloesen.

Der Abmeldeknopf in der Menueleiste faellt weg (stand doppelt), und
Abmelden erscheint nur, wenn HoneyOS ueberhaupt anmeldet — dafuer gibt
/api/apps jetzt oidc mit heraus. Nebenbei: restliche 'Leiste' im Text
zu 'HoneyBar' vereinheitlicht.
This commit is contained in:
marc committed 2026-08-29 21:23:11 +02:00
1 parent 52deffa30e
commit 0a5cd94ba0
12 files changed
+244 -51

No files matched your search

+10 -2
View File
@@ -769,6 +769,14 @@ body {
box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.6);
}
.startmenu[data-open="1"] { display: flex; }
/* Dasselbe Menü, nur andersherum aufgehängt: das Startmenü wächst aus der
Taskleiste nach oben, das Systemmenü aus der Menüleiste nach unten. */
.sysmenu {
top: calc(32px + env(safe-area-inset-top) + 2px);
bottom: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
}
.mb-logo[aria-expanded="true"] { background: var(--p-faint); }
.startmenu .sm-head {
padding: 4px 8px;
font-size: 10px;
@@ -846,7 +854,7 @@ body {
}
.login-note b { color: var(--p-mid); font-weight: 400; }
/* Nutzer in der Leiste */
/* Nutzer in der HoneyBar */
/* ── App-Verwaltung ────────────────────────────────────────────────────── */
.apped { display: flex; flex-direction: column; gap: 10px; }
@@ -980,7 +988,7 @@ body {
.wx-hit:hover { border-color: var(--p-faint); color: var(--p); }
/* Wetter in der Menüleiste — dasselbe Maß wie in honeybar.js, damit die
Leiste hier und die in den Apps nicht unterschiedlich aussehen. */
HoneyBar hier und die in den Apps nicht unterschiedlich aussehen. */
.mb-wx {
display: flex;
align-items: center;
+80 -16
View File
@@ -56,6 +56,8 @@
let apps = DEFAULT_APPS;
let settings = null;
// Ob HoneyOS ueberhaupt anmeldet — ohne Anbieter gaebe es nichts abzumelden.
let oidc = false;
// Was die Seite oben schon hatte, bevor die HoneyBar sie heruntergeschoben hat.
let urspruenglichesPolster = null;
@@ -149,6 +151,7 @@
wie ein anderes Logo aus. */
.home .hex { font-size: 10px; vertical-align: 2px; }
.home:hover { border-color: var(--p-dim); }
.home[aria-expanded="true"] { border-color: var(--p-dim); background: var(--p-faint); }
/* Bei vielen Apps schiebt man den Streifen seitwärts. Ohne
flex-shrink:0 an den Knöpfen quetscht das Raster sie stattdessen
zusammen, bis nur noch Buchstabenreste dastehen — am Handy sofort.
@@ -183,6 +186,11 @@
background: var(--bg); border: 1px solid var(--p-dim); padding: 4px;
display: none; flex-direction: column; gap: 2px;
box-shadow: 0 10px 30px rgba(0,0,0,.6);
/* Die Menues haengen neben der HoneyBar, nicht darin — ohne eigene
Schrift erben sie die der fremden App und stehen dann in deren
Fliesstext da. Genau so sah es aus. */
font: 12px/1.2 ui-monospace, "SF Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
color: var(--p); text-shadow: var(--glow);
}
.menu[data-open="1"] { display: flex; }
.menu a, .menu button { text-align: left; padding: 6px 8px; color: var(--p-mid); }
@@ -192,11 +200,14 @@
padding: 4px 8px; font-size: 10px; letter-spacing: .18em;
text-transform: uppercase; color: var(--p-dim);
}
/* Dasselbe Menue, nur an der anderen Seite: das Systemmenue haengt am
Logo links, das App-Menue am Pfeil rechts. */
.menu.sys { left: 6px; right: auto; }
@media (max-width: 560px) { .clock { display: none; } }
</style>
<div class="bar">
<a class="home" id="home" title="HoneyOS"><span class="hex">&#9632;</span> HONEY<b>OS</b></a>
<button class="home" id="home" title="HoneyOS-Menü" aria-haspopup="true" aria-expanded="false"><span class="hex">&#9632;</span> HONEY<b>OS</b></button>
<nav class="apps" id="apps"></nav>
<div class="right">
<span class="wx" id="wx" hidden></span>
@@ -205,11 +216,19 @@
</div>
</div>
<div class="menu sys" id="sysmenu" role="menu">
<div class="head">honeyos</div>
<a id="mHome" role="menuitem">Schreibtisch</a>
<a id="mSettings" role="menuitem">Einstellungen &hellip;</a>
<hr />
<button id="hide" role="menuitem">HoneyBar einklappen</button>
<hr id="mLogoutSep" hidden />
<a id="mLogout" role="menuitem" hidden>Abmelden</a>
</div>
<div class="menu" id="menu" role="menu">
<div class="head">wechseln zu</div>
<div id="menuApps"></div>
<hr />
<button id="hide" role="menuitem">honeybar einklappen</button>
</div>`;
/* ── Aussehen ─────────────────────────────────────────────────────────────
@@ -327,30 +346,46 @@
clock();
setInterval(clock, 20000);
root.getElementById("home").href = homeUrl;
systemZiele();
}
function verdrahten() {
if (verdrahtet) return;
verdrahtet = true;
const menu = root.getElementById("menu");
const btn = root.getElementById("menuBtn");
btn.addEventListener("click", (e) => {
e.stopPropagation();
const open = menu.dataset.open === "1";
menu.dataset.open = open ? "0" : "1";
btn.setAttribute("aria-expanded", String(!open));
// Zwei Menues, gleiche Mechanik: links das System, rechts die Apps.
// Eines auf heisst anderes zu — zwei offene Felder in 32 px Hoehe
// ueberdecken sich gegenseitig.
const paare = [
[root.getElementById("sysmenu"), root.getElementById("home")],
[root.getElementById("menu"), root.getElementById("menuBtn")],
];
const close = () => {
paare.forEach(([m, b]) => {
m.dataset.open = "0";
b.setAttribute("aria-expanded", "false");
});
};
paare.forEach(([menu, btn]) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const auf = menu.dataset.open === "1";
close();
if (!auf) {
menu.dataset.open = "1";
btn.setAttribute("aria-expanded", "true");
}
});
});
// Klick daneben schließt — das Ereignis muss die Grenze des Shadow DOM
// in beide Richtungen überleben, deshalb beide Wurzeln.
const close = () => {
menu.dataset.open = "0";
btn.setAttribute("aria-expanded", "false");
};
document.addEventListener("click", close);
root.addEventListener("click", (e) => {
if (!e.composedPath().includes(menu) && e.target !== btn) close();
const drin = paare.some(([m, b]) => e.composedPath().includes(m) || e.target === b);
if (!drin) close();
});
root.getElementById("hide").addEventListener("click", () => {
@@ -364,6 +399,34 @@
});
}
/**
* Die drei Wege ins System. Alle drei sind Links auf den Schreibtisch, keine
* Aktionen hier — und das ist keine Bequemlichkeit, sondern die Grenze:
*
* Einstellungen und Abmelden **schreiben**. Dafuer braucht der Server die
* HoneyOS-Sitzung, und die schickt der Browser aus einer fremden App nicht
* mit; liegt alles unter einer Domain, kommt sie zwar mit, aber das
* CSRF-Prüfwort steht in einem Cookie der HoneyOS-Adresse, das dieses Skript
* von hier aus nicht lesen darf. Und ein Abmelden, das ein simpler Aufruf
* erledigt, koennte jede fremde Seite ausloesen.
*
* Also: die HoneyBar zeigt den Weg, gehandelt wird dort, wo man angemeldet
* ist. Der Schreibtisch sieht die Frage in der Adresse und macht sie auf.
*/
function systemZiele() {
const ziel = (q) => {
const u = new URL(homeUrl, location.href);
u.search = q;
return u.href;
};
root.getElementById("mHome").href = new URL(homeUrl, location.href).href;
root.getElementById("mSettings").href = ziel("einstellungen=1");
const ab = root.getElementById("mLogout");
ab.href = ziel("abmelden=1");
ab.hidden = !oidc;
root.getElementById("mLogoutSep").hidden = !oidc;
}
/**
* Eingeklappt bleibt ein 3 px hoher Strich am oberen Rand stehen. Er nimmt
* praktisch keinen Platz, liegt über dem Inhalt statt ihn zu schieben — und
@@ -457,6 +520,7 @@
apps = [{ id: "honeyos", name: "DESKTOP", url: homeUrl }].concat(data.apps);
}
settings = data.settings || null;
oidc = !!data.oidc;
}
} catch {
/* nicht angemeldet, kein CORS, kein Netz → eingebaute Liste;
+3 -2
View File
@@ -30,7 +30,9 @@
<div class="desktop" id="desktop" hidden>
<!-- Menüleiste: dieselbe Anatomie wie honeybar.js in den Apps -->
<header class="menubar">
<button class="mb-logo" id="btnAbout" title="Über HoneyOS">
<!-- Das Logo ist das Systemmenü — dasselbe wie in der HoneyBar
fremder Apps, damit HoneyOS von überall gleich erreichbar ist. -->
<button class="mb-logo" id="btnSystem" title="HoneyOS-Menü" aria-haspopup="true" aria-expanded="false">
<span class="hex">&#9632;</span> HONEY<b>OS</b>
</button>
<nav class="mb-apps" id="mbApps" aria-label="Anwendungen"></nav>
@@ -38,7 +40,6 @@
<span class="mb-wx" id="mbWx" hidden></span>
<span class="mb-clock" id="clock">--:--</span>
<button class="mb-btn" id="btnSettings" title="Einstellungen (S)">&#9881;</button>
<button class="mb-btn" id="btnLogout" title="Abmelden" hidden>&#9211;</button>
</div>
</header>
+82 -6
View File
@@ -110,7 +110,6 @@ function runBoot() {
function renderShell() {
$("desktop").hidden = false;
$("btnLogout").hidden = !state.oidc;
renderMenubarApps();
renderMode();
tickClock();
@@ -792,6 +791,60 @@ function openAbout() {
/* ── Hilfsmittel ───────────────────────────────────────────────────────── */
/* ── Systemmenü im Logo ────────────────────────────────────────────────────
* Der Zwilling des Menüs in honeybar.js: gleiche Punkte, gleiche Reihenfolge.
* Hier sind es Handlungen, dort Links hierher — denn nur hier ist man
* angemeldet. Wer aus Aether auf „Einstellungen“ klickt, landet auf genau
* demselben Feld, das dieser Punkt aufmacht.
*/
function buildSystemMenu() {
const menu = document.createElement("div");
menu.className = "startmenu sysmenu";
menu.id = "sysmenu";
$("desktop").appendChild(menu);
const btn = $("btnSystem");
btn.addEventListener("click", (e) => {
e.stopPropagation();
if (menu.dataset.open === "1") return closeSystemMenu();
closeStartMenu();
const terminal = state.settings.mode === "terminal";
menu.innerHTML =
`<div class="sm-head">honeyos</div>
<button data-do="about">Über HoneyOS</button>
<button data-do="settings">Einstellungen</button>
<hr />
<button data-do="mode">${terminal ? "▣ Desktop-Modus" : "↩ Terminal-Modus"}</button>` +
(state.oidc ? `<hr /><button data-do="logout">⏻ Abmelden</button>` : "");
menu.querySelectorAll("[data-do]").forEach((b) =>
b.addEventListener("click", () => {
closeSystemMenu();
({
about: openAbout,
settings: openSettings,
mode: () => setMode(terminal ? "desktop" : "terminal"),
logout,
})[b.dataset.do]();
})
);
menu.dataset.open = "1";
btn.setAttribute("aria-expanded", "true");
});
document.addEventListener("click", closeSystemMenu);
}
function closeSystemMenu() {
const menu = $("sysmenu");
if (!menu) return;
menu.dataset.open = "0";
$("btnSystem").setAttribute("aria-expanded", "false");
}
/* ── Startmenü ─────────────────────────────────────────────────────────── */
function buildStartMenu() {
@@ -805,6 +858,10 @@ function buildStartMenu() {
e.stopPropagation();
const open = menu.dataset.open === "1";
if (open) return closeStartMenu();
// Beide Menüs offen ginge, sähe aber aus wie ein Fehler: Der Klick auf den
// Startknopf hält sein Ereignis auf, also erreicht es den Schließer des
// Systemmenüs nicht von selbst.
closeSystemMenu();
menu.innerHTML = `<div class="sm-head">starten</div>` +
state.cfg.apps.map((a) => `<button data-app="${a.id}">${escapeHtml(a.name)}</button>`).join("") +
@@ -812,8 +869,10 @@ function buildStartMenu() {
<button data-win="system">System</button>
<button data-win="settings">Einstellungen</button>
<button data-win="about">Über HoneyOS</button>
<hr /><button data-mode="terminal">↩ Terminal-Modus</button>
<button data-logout="1">⏻ Abmelden</button>`;
<hr /><button data-mode="terminal">↩ Terminal-Modus</button>` +
// Ohne Anbieter gibt es nichts abzumelden — im Systemmenü steht der
// Punkt aus demselben Grund nur bei Anmeldung.
(state.oidc ? `<button data-logout="1">⏻ Abmelden</button>` : "");
menu.querySelectorAll("[data-app]").forEach((b) =>
b.addEventListener("click", () => {
@@ -834,7 +893,7 @@ function buildStartMenu() {
closeStartMenu();
setMode("terminal");
});
menu.querySelector("[data-logout]").addEventListener("click", logout);
menu.querySelector("[data-logout]")?.addEventListener("click", logout);
menu.dataset.open = "1";
btn.setAttribute("aria-expanded", "true");
@@ -922,6 +981,13 @@ async function main() {
loadLocalSettings();
applySettings();
// ?abmelden=1 kommt aus dem Systemmenü der HoneyBar. Es hier zu erledigen
// statt am Server heißt: Die Abmeldung bleibt ein POST von dieser Seite aus,
// und keine fremde Seite kann sie durch ein eingebettetes Bild auslösen.
if (new URLSearchParams(location.search).get("abmelden") === "1") {
return logout();
}
let pub = { oidc: false, buttonLabel: "Anmelden" };
try {
pub = await api("/api/public");
@@ -948,18 +1014,26 @@ async function main() {
renderShell();
buildStartMenu();
buildSystemMenu();
probeApps();
setInterval(tickClock, 15000);
setInterval(probeApps, 120000);
$("btnSettings").addEventListener("click", openSettings);
$("btnAbout").addEventListener("click", openAbout);
$("btnLogout").addEventListener("click", logout);
$("btnClose").addEventListener("click", closeModal);
$("modal").addEventListener("click", (e) => {
if (e.target === $("modal")) closeModal();
});
// Was die HoneyBar aus einer fremden App herüberreicht: Sie darf selbst
// nichts schreiben (siehe honeybar.js), also stellt sie die Frage in der
// Adresse und der Schreibtisch beantwortet sie. Danach kommt die Adresse
// weg — ein Neuladen soll die Einstellungen nicht wieder aufreißen.
if (new URLSearchParams(location.search).get("einstellungen") === "1") {
history.replaceState(null, "", location.pathname);
openSettings();
}
window.addEventListener("keydown", (e) => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName);
@@ -968,6 +1042,8 @@ async function main() {
// einem gleichzeitig das oberste Fenster weg.
const menu = $("startmenu");
if (menu && menu.dataset.open === "1") return closeStartMenu();
const sys = $("sysmenu");
if (sys && sys.dataset.open === "1") return closeSystemMenu();
return state.settings.mode === "desktop" ? closeTopWindow() : closeModal();
}
if (typing) return;
+1 -1
View File
@@ -13,7 +13,7 @@ function greeting() {
*
* Stand früher rechts in der Menüleiste. Dort war es das einzige Stück, das
* die HoneyBar in den Apps nicht hat — beide sollen aber gleich aussehen, sonst
* merkt man beim Wechsel, dass es zwei verschiedene Leisten sind. Auf dem
* merkt man beim Wechsel, dass es zwei verschiedene HoneyBars sind. Auf dem
* Schreibtisch ist ohnehin Platz dafür, und die Begrüßung gehört dazu.
*/
function userHtml() {
+3 -3
View File
@@ -53,9 +53,9 @@ export function renderWeather() {
// Und in der HoneyBar. Die Menüleiste hier ist zwar anderer Code als
// honeybar.js in den Apps, aber für den, der davorsitzt, dieselbe HoneyBar —
// ein Schalter, der nur woanders wirkt, wäre nicht zu erklären.
const inLeiste = state.settings.weatherBar === "on" && w;
bar.hidden = !inLeiste;
if (inLeiste) {
const inBar = state.settings.weatherBar === "on" && w;
bar.hidden = !inBar;
if (inBar) {
bar.innerHTML = `${glyphSvg(w.glyph)}<span class="mb-wx-temp">${Math.round(w.grad)}°</span>`;
bar.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
}