Files
HoneyOS/web/honeybar/honeybar.js
T
marc 3f8b98b869 Eine Biene als Wappen, an allen vier Stellen
Aus dem Quadrat links in beiden Leisten wird eine Pixelbiene von der Seite:
braune Streifen, schwarze Beine, Fluegel mit Kerbe. Sie steht in genau einem
Raster (tools/biene.py, 16x16 Zeichen); daraus entstehen SVG, PNG und eine
Fassung aus Schriftzeichen.

SVG statt PNG in den Leisten: Die HoneyBar laeuft in fremden Apps, und ob ein
<img> dort laedt, entscheidet deren CSP. Markup kann niemand blockieren.

Im System-Fenster und ueber dem Startlauf steht die Biene aus Schriftzeichen —
beides sind Textflaechen, ein Bild darin waere ein Fremdkoerper.

Das Wappen liegt an drei Stellen (Datei, Modul, HoneyBar-Zeile), weil die
Leiste nichts nachladen kann. TestWappenIstUeberallDasselbe haelt sie gleich.
2026-09-02 10:21:03 +02:00

610 lines
28 KiB
JavaScript

/*
* HoneyBar — die HoneyBar zum Einhängen in eine fremde App.
*
* <script src="https://os.honeyy.duckdns.org/honeybar/honeybar.js"
* data-app="aether"></script>
*
* Eine Zeile, kein Build, keine Abhängigkeit. Die HoneyBar rendert in einem
* Shadow DOM — die Stile der Gastgeber-App kommen nicht rein, unsere nicht
* raus. Das ist der ganze Grund für die Konstruktion: Aether ist React +
* Tailwind, InkNote React + eigene Palette, und beide sollen von der HoneyBar
* nichts merken außer den 32 Pixeln oben.
*
* data-Attribute am <script>:
* data-app (Pflicht) id der App, in der die HoneyBar steckt
* data-apps URL der App-Liste; ohne das /api/apps neben dem Skript
* data-key Schlüssel für die App-Liste. Nötig, wenn HoneyOS auf einer
* anderen Site liegt als die App: dann schickt der Browser die
* HoneyOS-Sitzung nicht mit, und ohne zweiten Ausweis bliebe die
* HoneyBar ohne Liste.
* data-home URL des HoneyOS-Desktops
* data-push "body" (Standard) schiebt den Seiteninhalt nach unten,
* "none" legt die HoneyBar darüber
*
* Wichtig zu wissen: localStorage ist pro Domain. Was auf os.honeyy… steht,
* sieht diese HoneyBar auf notes.honeyy… nicht — die HoneyBar ist ein Schalter,
* kein geteilter Zustand. Gemeinsame Sitzung wäre echtes SSO und ist eine
* eigene Baustelle.
*/
(function () {
"use strict";
if (window.__honeyBar) return; // zweimal eingebunden ist kein Fehler
window.__honeyBar = true;
const script = document.currentScript;
const d = (k, fb) => (script && script.dataset[k]) || fb;
const BAR_H = 32;
const LS_HIDDEN = "honeybar.hidden";
// Notliste, falls der Server nichts herausgibt: nur der Weg zurück zum
// Schreibtisch. Erfundene App-Adressen stünden hier sonst ewig fest.
const DEFAULT_APPS = [{ id: "honeyos", name: "DESKTOP", url: "" }];
const current = d("app", "");
// Ohne data-home führt der Weg zurück dorthin, wo dieses Skript herkommt.
const homeUrl = d("home", script ? new URL("/", script.src).href : "/");
const pushMode = d("push", "body");
// Ohne data-apps fragt die HoneyBar den HoneyOS-Server, von dem sie selbst
// geladen wurde. Er gibt die Liste nur an angemeldete Browser heraus und
// nur an Ursprünge, die selbst als App eingetragen sind.
const appsUrl = d("apps", script ? new URL("../api/apps", script.src).href : "");
const appsKey = d("key", "");
// Dieselbe Herkunft wie die App-Liste: Wo /api/apps liegt, liegt /api/weather.
const wetterUrl = appsUrl ? appsUrl.replace(/\/apps(\?|$)/, "/weather$1") : "";
let apps = DEFAULT_APPS;
let settings = null;
let glyphs = {};
// Was die Seite oben schon hatte, bevor die HoneyBar sie heruntergeschoben hat.
let urspruenglichesPolster = null;
/* ── Aufbau ───────────────────────────────────────────────────────────── */
// Als installierte App liegt der Inhalt unter der Statusleiste des Geraets
// (die Apps setzen viewport-fit=cover, damit der Hintergrund bis an den Rand
// reicht). Eine HoneyBar, die stur bei 0 anfaengt, liegt dort unter Uhrzeit und
// Akkustand. Deshalb waechst sie um die Aussparung und polstert sich selbst;
// im Browser ist env() null und alles bleibt bei 32 px.
const SAFE = "env(safe-area-inset-top, 0px)";
const HOEHE = "calc(" + BAR_H + "px + " + SAFE + ")";
/* Alle Pixelraster kommen mit der App-Liste vom Server (web/glyphs.json).
Frueher standen die Wettersymbole hier noch einmal abgetippt — genau das
war schon einmal auseinandergelaufen. Diese Datei bleibt trotzdem eine
einzelne ohne zweite Anfrage: Sie fragt ohnehin nach der App-Liste, und
die bringt die Raster einfach mit. */
function rasterSvg(grid) {
if (!grid) return "";
let d = "";
grid.forEach((row, y) => {
let run = 0;
for (let x = 0; x <= row.length; x++) {
if (row[x] === "#") run++;
else if (run) { d += `<rect x="${x - run}" y="${y}" width="${run}" height="1"/>`; run = 0; }
}
});
return `<svg viewBox="0 0 12 12" fill="currentColor" shape-rendering="crispEdges" aria-hidden="true">${d}</svg>`;
}
const host = document.createElement("honey-bar");
host.style.cssText = [
"position:fixed", "top:0", "left:0", "right:0",
"height:" + HOEHE,
"padding-top:" + SAFE,
"box-sizing:border-box",
"z-index:2147483000",
"display:block",
"color-scheme:dark",
].join(";");
/* Das Wappen. Es steht hier als Markup, weil die HoneyBar in fremden
* Apps läuft: Ein <img> holte eine Datei von HoneyOS, und ob das geht,
* entscheidet die CSP der fremden App. Erzeugt von tools/biene.py —
* dieselbe Zeile steht in web/img/biene.svg (ein Test hält sie gleich).
*/
const WAPPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><rect x="4" y="0" width="2" height="1" fill="#f5f2e7"/><rect x="9" y="0" width="2" height="1" fill="#f5f2e7"/><rect x="2" y="1" width="5" height="1" fill="#f5f2e7"/><rect x="8" y="1" width="5" height="1" fill="#f5f2e7"/><rect x="2" y="2" width="11" height="1" fill="#f5f2e7"/><rect x="14" y="2" width="1" height="1" fill="#3b2412"/><rect x="3" y="3" width="9" height="1" fill="#f5f2e7"/><rect x="13" y="3" width="2" height="1" fill="#3b2412"/><rect x="3" y="4" width="11" height="1" fill="#ffd060"/><rect x="2" y="5" width="2" height="1" fill="#f2b33d"/><rect x="4" y="5" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="5" width="2" height="1" fill="#f2b33d"/><rect x="8" y="5" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="5" width="5" height="1" fill="#f2b33d"/><rect x="1" y="6" width="3" height="1" fill="#f2b33d"/><rect x="4" y="6" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="6" width="2" height="1" fill="#f2b33d"/><rect x="8" y="6" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="6" width="2" height="1" fill="#f2b33d"/><rect x="12" y="6" width="1" height="1" fill="#181410"/><rect x="13" y="6" width="1" height="1" fill="#63c7c0"/><rect x="14" y="6" width="1" height="1" fill="#f2b33d"/><rect x="0" y="7" width="1" height="1" fill="#5c3a1c"/><rect x="1" y="7" width="3" height="1" fill="#f2b33d"/><rect x="4" y="7" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="7" width="2" height="1" fill="#f2b33d"/><rect x="8" y="7" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="7" width="2" height="1" fill="#f2b33d"/><rect x="12" y="7" width="2" height="1" fill="#181410"/><rect x="14" y="7" width="1" height="1" fill="#f2b33d"/><rect x="1" y="8" width="3" height="1" fill="#f2b33d"/><rect x="4" y="8" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="8" width="2" height="1" fill="#f2b33d"/><rect x="8" y="8" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="8" width="5" height="1" fill="#f2b33d"/><rect x="2" y="9" width="1" height="1" fill="#d2952a"/><rect x="3" y="9" width="2" height="1" fill="#462b14"/><rect x="5" y="9" width="2" height="1" fill="#d2952a"/><rect x="7" y="9" width="2" height="1" fill="#462b14"/><rect x="9" y="9" width="6" height="1" fill="#d2952a"/><rect x="3" y="10" width="2" height="1" fill="#181410"/><rect x="7" y="10" width="2" height="1" fill="#181410"/><rect x="11" y="10" width="2" height="1" fill="#181410"/><rect x="3" y="11" width="2" height="1" fill="#181410"/><rect x="7" y="11" width="2" height="1" fill="#181410"/><rect x="11" y="11" width="2" height="1" fill="#181410"/></svg>';
const root = host.attachShadow({ mode: "open" });
root.innerHTML = `
<style>
:host, * { box-sizing: border-box; }
/* Dieselben Röhren wie auf dem Schreibtisch. Welche gilt, sagt der
Server; bis seine Antwort da ist, leuchtet Bernstein. */
:host {
--p: #ffb641;
--p-mid: #c8842a;
--p-dim: #6b4718;
--p-faint: #2a1c0c;
--bg: #140d07;
--glow: 0 0 5px rgba(255, 182, 65, .4);
--scan: rgba(0, 0, 0, .13);
}
.bar {
display: flex; align-items: center; gap: 10px;
height: ${BAR_H}px; padding: 0 8px;
background: var(--bg);
border-bottom: 1px solid var(--p-dim);
color: var(--p);
font: 12px/1 ui-monospace, "SF Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
text-shadow: var(--glow);
user-select: none;
}
.bar::after {
/* Scanlines, damit die HoneyBar zum Desktop gehört */
content: ""; position: absolute; inset: 0; pointer-events: none;
background: repeating-linear-gradient(to bottom, var(--scan) 0 1px, transparent 1px 3px);
}
a, button {
font: inherit; color: inherit; background: none;
border: 1px solid transparent; cursor: pointer;
text-decoration: none; padding: 3px 9px; white-space: nowrap;
}
.home { letter-spacing: .14em; color: var(--p); padding-left: 2px; }
.home b { color: var(--p-mid); font-weight: 400; }
/* Dasselbe Wappen wie in der Menüleiste des Schreibtischs, in derselben
Größe. Es bleibt gelb, während der Rest die Röhre wechselt: Ein
Zeichen, das die Farbe mitwechselt, ist ein Symbol — eines, das seine
Farbe behält, ist ein Wappen. */
.home .hex { display: inline-block; width: 20px; height: 20px; vertical-align: -6px; }
.home .hex svg { display: block; width: 100%; height: 100%; }
.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.
Der Schatten rechts zeigt an, dass es weitergeht. */
/* Nur der Streifen in der Leiste. Wer eine weitere Klasse "apps"
vergibt, erbt hier Maske und Bildlauf mit. */
.apps {
display: flex; gap: 4px; flex: 1 1 auto; min-width: 0;
overflow-x: auto; scrollbar-width: none;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
mask-image: linear-gradient(90deg, #000 calc(100% - 18px), transparent);
}
.apps::-webkit-scrollbar { display: none; }
.app { color: var(--p-mid); letter-spacing: .08em; flex: 0 0 auto; scroll-snap-align: end; }
/* Symbol und Name in einer Zeile. Das Symbol traegt die Textfarbe, also
stimmt es in jeder Roehre und im umgekehrten Zustand von selbst. */
.app, .menu a[role="menuitem"] { display: inline-flex; align-items: center; gap: 6px; }
.ico { display: block; width: 12px; height: 12px; flex: 0 0 auto; }
.ico svg { width: 12px; height: 12px; display: block; }
.app:hover { color: var(--p); border-color: var(--p-dim); }
.app[aria-current="page"] {
background: var(--p); border-color: var(--p); color: var(--bg); text-shadow: none;
}
.right { display: flex; align-items: center; gap: 8px; }
.clock { color: var(--p-mid); font-variant-numeric: tabular-nums; }
/* Anzeige, kein Knopf: Wer darauf klickt, erwartet, dass etwas passiert.
Eingestellt wird das Wetter dort, wo alles eingestellt wird. */
.wx { display: flex; align-items: center; gap: 5px; color: var(--p-mid); }
.wx[hidden] { display: none; }
.wx svg { width: 14px; height: 14px; display: block; }
/* Am Handy nur das Symbol: 32 px Höhe und die Apps daneben lassen der
Temperatur keinen Platz mehr. */
@media (max-width: 700px) { .wx .grad { display: none; } }
.menu-btn { border-color: var(--p-dim); line-height: 1; padding: 3px 7px; }
.menu-btn:hover { background: var(--p-faint); }
.menu {
position: absolute; top: ${BAR_H + 2}px; right: 6px; min-width: 190px;
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; }
/* Die Listenpunkte des Systemmenues. Sie stehen VOR der Schublade:
Die Regel "menu a" ist spezifischer als die Klasse der Kachel, und
danach gewaehlt haetten die Kacheln Polster und Ausrichtung der Liste
geerbt statt ihrer eigenen. Genau das war passiert. */
.menu a, .menu button { text-align: left; padding: 6px 8px; color: var(--p-mid); }
.menu a:hover, .menu button:hover { background: var(--p-faint); color: var(--p); }
/* Die Schublade: Symbol, Name, Untertitel. Sie wird absichtlich
aufgemacht, weil man etwas sucht — also wird gelesen, nicht nur
wiedererkannt. Deshalb stehen hier immer Namen, anders als im
Streifen oben, wo ein Blick reichen muss. */
/* NICHT .menu.apps nennen: Der Streifen oben heisst .apps und blendet
seine rechten 18 px mit einer Maske aus, damit man sieht, dass es
seitwaerts weitergeht. Eine zweite Klasse "apps" am Menue holt diese
Maske mit — die Schublade sah rechts aus, als loese sie sich auf. */
.menu.schublade { min-width: 318px; max-width: min(360px, calc(100vw - 12px)); }
/* minmax(0, 1fr): Ohne das darf eine lange Beschreibung ihre Spalte
auseinanderdruecken, und die Schublade wird breiter als der Schirm. */
.drawer { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2px; }
/* .menu .d-app, nicht .d-app: Die Kachel ist ein <a> im Menue, und
"menu a" oben ist spezifischer als eine einzelne Klasse — die
Kacheln trugen dadurch Polster und Ausrichtung der Listenpunkte.
Reihenfolge hilft dagegen nicht, nur Spezifitaet. */
.menu .d-app {
display: flex; flex-direction: column; align-items: center; gap: 5px;
padding: 9px 4px; text-align: center; color: var(--p-mid);
border: 1px solid transparent; min-width: 0;
}
/* Die Regel fuer a und button oben setzt nowrap — richtig fuer den
Streifen, falsch
hier: Der Untertitel soll umbrechen statt abgeschnitten zu werden. */
.menu .d-name, .menu .d-sub { max-width: 100%; }
.menu .d-sub { white-space: normal; }
.menu .d-name { overflow: hidden; text-overflow: ellipsis; }
.menu .d-app:hover { background: none; border-color: var(--p-dim); color: var(--p); }
.menu .d-app[aria-current="page"] { border-color: var(--p); color: var(--p); }
.menu .d-app .ico { width: 24px; height: 24px; }
.menu .d-app .ico svg { width: 24px; height: 24px; }
.menu .d-name { letter-spacing: .08em; }
/* Der Untertitel darf umbrechen, aber nicht wachsen — sonst stehen die
Kacheln unterschiedlich hoch. */
.menu .d-sub {
font-size: 10px; line-height: 1.25; color: var(--p-dim);
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden;
}
.menu hr { border: 0; border-top: 1px solid var(--p-faint); margin: 3px 0; }
.menu .head {
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">
<button class="home" id="home" title="HoneyOS-Menü" aria-haspopup="true" aria-expanded="false"><span class="hex">${WAPPEN}</span> HONEY<b>OS</b></button>
<nav class="apps" id="apps"></nav>
<div class="right">
<span class="wx" id="wx" hidden></span>
<span class="clock" id="clock"></span>
<button class="menu-btn" id="menuBtn" aria-haspopup="true" aria-expanded="false">&#9662;</button>
</div>
</div>
<div class="menu sys" id="sysmenu" role="menu">
<div class="head">honeyos</div>
<a id="mHome" role="menuitem">Schreibtisch</a>
<hr />
<button id="hide" role="menuitem">HoneyBar einklappen</button>
</div>
<div class="menu schublade" id="menu" role="menu">
<div class="head">wechseln zu</div>
<div class="drawer" id="menuApps"></div>
</div>`;
/* ── Aussehen ─────────────────────────────────────────────────────────────
* Dieselben drei Röhren wie in style.css. Sie hier zu wiederholen ist die
* bewusste Entscheidung: die HoneyBar soll ohne zweite Datei auskommen, damit
* sie in jeder fremden App mit einer Zeile funktioniert.
*/
const ROEHREN = {
amber: { p: "#ffb641", mid: "#c8842a", dim: "#6b4718", faint: "#2a1c0c", bg: "#140d07", glow: "255,182,65" },
green: { p: "#5cff8f", mid: "#2fa85c", dim: "#17512c", faint: "#08200f", bg: "#071206", glow: "92,255,143" },
ice: { p: "#cfe8ff", mid: "#7fa6c4", dim: "#395366", faint: "#12202a", bg: "#0a1118", glow: "207,232,255" },
};
function applySettings(s) {
if (!s) return;
const t = ROEHREN[s.phosphor] || ROEHREN.amber;
const bar = root.querySelector(".bar");
if (!bar) return;
const set = (k, v) => bar.parentNode.host.style.setProperty(k, v);
set("--p", t.p);
set("--p-mid", t.mid);
set("--p-dim", t.dim);
set("--p-faint", t.faint);
set("--bg", t.bg);
set("--glow", s.glow === "off" ? "none" : `0 0 5px rgba(${t.glow},.4)`);
set("--scan", s.scanlines === "off" ? "transparent" : "rgba(0,0,0,.13)");
}
/* ── Verhalten ────────────────────────────────────────────────────────── */
function render() {
const nav = root.getElementById("apps");
const menuApps = root.getElementById("menuApps");
nav.innerHTML = "";
menuApps.innerHTML = "";
// Der Schalter gilt nur fuer den Streifen. Im Aufklappmenue stehen die
// Symbole immer: Dort ist Platz, und dort sucht man eine App, statt sie
// im Vorbeigehen wiederzuerkennen.
const symboleImStreifen = !settings || settings.barGlyphs !== "off";
const symbol = (app) => {
const grid = glyphs[app.glyph] || glyphs.box;
if (!grid) return null;
const span = document.createElement("span");
span.className = "ico";
span.innerHTML = rasterSvg(grid);
return span;
};
apps.forEach((app) => {
if (app.id === "honeyos") return; // steckt schon links im Logo
const a = document.createElement("a");
a.className = "app";
a.href = app.url;
if (symboleImStreifen) {
const ico = symbol(app);
if (ico) a.appendChild(ico);
}
a.appendChild(document.createTextNode(app.name));
if (app.id === current) a.setAttribute("aria-current", "page");
nav.appendChild(a);
const m = document.createElement("a");
m.className = "d-app";
m.href = app.url;
m.setAttribute("role", "menuitem");
const ico = symbol(app);
if (ico) m.appendChild(ico);
const name = document.createElement("span");
name.className = "d-name";
name.textContent = app.name;
m.appendChild(name);
if (app.subtitle) {
const sub = document.createElement("span");
sub.className = "d-sub";
sub.textContent = app.subtitle;
m.appendChild(sub);
}
if (app.id === current) m.setAttribute("aria-current", "page");
menuApps.appendChild(m);
});
}
/**
* Das Wetter neben der Uhr — nur wenn in HoneyOS eingeschaltet und ein Ort
* eingestellt ist. Der Server antwortet sonst mit 204, und dann bleibt hier
* schlicht nichts stehen: lieber kein Symbol als eines ohne Aussage.
*/
async function wetter() {
if (!settings || settings.weatherBar !== "on") return;
let w;
try {
if (!wetterUrl) return;
const res = await fetch(wetterUrl, {
cache: "no-store",
credentials: "include",
headers: appsKey ? { "X-HoneyOS-Key": appsKey } : {},
});
if (res.status !== 200) return;
w = await res.json();
} catch {
return; // offline oder HoneyOS aus — die App geht das nichts an
}
const el = root.getElementById("wx");
el.innerHTML = rasterSvg(glyphs[w.glyph] || glyphs.wolke) + `<span class="grad">${Math.round(w.grad)}°</span>`;
el.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
el.hidden = false;
}
function clock() {
root.getElementById("clock").textContent = new Date().toLocaleTimeString("de-DE", {
hour: "2-digit",
minute: "2-digit",
});
}
// Einmal verdrahtet, nicht bei jedem Einblenden neu.
//
// Der Fehler dahinter: mount() hing seine Zuhörer selbst an. Wer die HoneyBar
// einklappte und über den Strich zurückholte, hatte sie doppelt — der Klick
// auf das Menü öffnete und schloss es im selben Zug, und der Punkt
// „honeybar einklappen" war nicht mehr erreichbar.
let verdrahtet = false;
function mount() {
document.body.appendChild(host);
// Der Seite Platz verschaffen. `--honeybar-h` steht bereit, falls eine App
// ihre eigene 100vh-Rechnung anpassen muss (siehe README).
document.documentElement.style.setProperty("--honeybar-h", HOEHE);
if (pushMode === "body") {
// Nicht ausrechnen, sondern rechnen lassen: env() kennt nur der Browser,
// und erst beim Anzeigen. Der vorherige Wert bleibt als Summand stehen,
// damit eine App mit eigenem Innenabstand ihn nicht verliert.
const cs = getComputedStyle(document.body);
urspruenglichesPolster = cs.paddingTop || "0px";
document.body.style.paddingTop =
"calc(" + urspruenglichesPolster + " + var(--honeybar-h))";
}
render();
applySettings(settings);
verdrahten();
clock();
setInterval(clock, 20000);
systemZiele();
}
function verdrahten() {
if (verdrahtet) return;
verdrahtet = true;
// 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.
document.addEventListener("click", close);
root.addEventListener("click", (e) => {
const drin = paare.some(([m, b]) => e.composedPath().includes(m) || e.target === b);
if (!drin) close();
});
root.getElementById("hide").addEventListener("click", () => {
try {
localStorage.setItem(LS_HIDDEN, "1");
} catch {
/* dann eben nur für diesen Aufruf */
}
unmount();
mountNub();
});
}
/**
* Der eine Weg ins System: ein Link auf den Schreibtisch, keine Aktion hier.
*
* Das ist keine Bequemlichkeit, sondern die Grenze: Einstellungen
* **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.
*
* Hier stand deshalb lange ein zweiter Punkt „Einstellungen …", der auf
* dieselbe Seite fuehrte, nur mit geoeffnetem Feld. **Weg damit** (Marcs
* Einwand, 01.09.2026): Zwei Tueren in denselben Raum sind keine zwei Wege.
* Wer eine Einstellung aendern will, ist ohnehin auf dem Schreibtisch, und
* dort steht sie einen Klick oder ein [S] entfernt.
*
* Abmelden steht hier bewusst gar nicht: Es gehoert dorthin, wo man sich
* angemeldet hat — unten in den Einstellungen.
*/
function systemZiele() {
root.getElementById("mHome").href = new URL(homeUrl, location.href).href;
}
/**
* 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
* ist der Weg zurück. Ganz verschwinden darf die HoneyBar nicht: wer sie einmal
* weggeklickt hat, käme sonst nur noch über die Entwicklerkonsole an sie heran.
*/
let nub = null;
function mountNub() {
if (nub) return;
nub = document.createElement("honey-bar-nub");
nub.style.cssText = [
"position:fixed", "left:0", "right:0", "height:3px",
"top:" + SAFE,
"z-index:2147483000", "cursor:pointer", "display:block",
].join(";");
nub.title = "HoneyBar einblenden";
const r = nub.attachShadow({ mode: "open" });
// Der Strich trug seine Farben fest verdrahtet und blieb deshalb bernstein,
// auch wenn in HoneyOS Grün eingestellt war. Er gehört zur HoneyBar, also
// trägt er dieselbe Röhre.
const t = ROEHREN[(settings && settings.phosphor) || "amber"] || ROEHREN.amber;
r.innerHTML = `
<style>
.strich {
height: 3px;
background: linear-gradient(90deg, ${t.dim}, ${t.p} 50%, ${t.dim});
opacity: .55;
transition: opacity .15s, height .15s;
}
:host(:hover) .strich { opacity: 1; height: 3px; }
</style>
<div class="strich"></div>`;
nub.addEventListener("click", () => {
forget();
nub.remove();
nub = null;
mount();
});
document.body.appendChild(nub);
}
function forget() {
try {
localStorage.removeItem(LS_HIDDEN);
} catch {
/* dann gilt es nur für diesen Aufruf */
}
}
function unmount() {
host.remove();
if (pushMode === "body" && urspruenglichesPolster !== null) {
document.body.style.paddingTop = urspruenglichesPolster;
urspruenglichesPolster = null;
}
document.documentElement.style.removeProperty("--honeybar-h");
}
async function start() {
let hidden = false;
try {
hidden = localStorage.getItem(LS_HIDDEN) === "1";
} catch {
/* kein Speicher, HoneyBar bleibt sichtbar */
}
// ?honeybar=1 holt sie zurück, falls der Strich einmal übersehen wird.
if (hidden && new URLSearchParams(location.search).get("honeybar") === "1") {
hidden = false;
forget();
}
if (hidden) {
if (document.body) mountNub();
else document.addEventListener("DOMContentLoaded", mountNub, { once: true });
return;
}
if (appsUrl) {
try {
// credentials: die Sitzung liegt auf der HoneyOS-Domain, und die ist
// dieselbe Site wie diese hier — sonst käme das Cookie nicht mit.
const res = await fetch(appsUrl, {
cache: "no-store",
credentials: "include",
headers: appsKey ? { "X-HoneyOS-Key": appsKey } : {},
});
if (res.ok) {
const data = await res.json();
if (Array.isArray(data.apps) && data.apps.length) {
apps = [{ id: "honeyos", name: "DESKTOP", url: homeUrl }].concat(data.apps);
}
settings = data.settings || null;
if (data.glyphs) glyphs = data.glyphs;
}
} catch {
/* nicht angemeldet, kein CORS, kein Netz → eingebaute Liste;
die HoneyBar bleibt ein Umschalter, nur ohne frische Namen */
}
}
if (document.body) mount();
else document.addEventListener("DOMContentLoaded", mount, { once: true });
wetter();
}
start();
})();