feat: Leiste trägt die Röhre vom Server, Einklappen ist umkehrbar

Zwei Sachen, die beim Benutzen aufgefallen sind.

- "leiste ausblenden" war eine Einbahnstraße: die Leiste verschwand ganz und
  liess sich nur über die Entwicklerkonsole zurückholen. Jetzt bleibt ein
  3px-Strich stehen, ein Klick darauf holt sie wieder. ?honeybar=1 an der
  Adresse ist der Notausgang, falls der Strich übersehen wird.
- Die Leiste hatte ihre Farben fest eingetragen und blieb bernsteinfarben,
  auch wenn der Schreibtisch auf grün stand. Genau dieser Bruch liess sie wie
  etwas Angeklebtes wirken. /api/apps liefert deshalb jetzt {apps, settings};
  die Leiste setzt Röhre, Glühen und Scanlines danach.

Dass das auch über Site-Grenzen geht, war so nicht erwartet: der Schlüssel
für die App-Liste trägt das Aussehen einfach mit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-24 22:37:06 +02:00
1 parent 1c3218939b
commit 5ed4e30179
5 files changed
+191 -41

No files matched your search

+22 -10
View File
@@ -14,19 +14,21 @@ wirklich hinschaut.
Ohne diese Stücke ist alles danach nicht möglich. Reihenfolge nach dem
Verhältnis von Aufwand zu spürbarer Wirkung.
### 1.1 Die Leiste trägt deine Röhre
### 1.1 Die Leiste trägt deine Röhre — ✅ erledigt (24.08.2026)
**Das größte Gefühl für den kleinsten Eingriff.** Heute ist `honeybar.js` fest
bernsteinfarben — auch wenn dein Schreibtisch auf grün steht. Genau dieser Bruch
lässt die Leiste wie etwas Angeklebtes wirken statt wie Teil eines Systems.
Die Leiste liest ihre Farben nicht mehr fest, sondern aus derselben Antwort, mit
der sie die App-Liste holt. Röhre in HoneyOS auf grün → die Leiste in Aether ist
grün, ohne dass Aether angefasst wird. `glow` und `scanlines` gelten dort ebenso.
Die Einstellungen liegen seit 24.08. serverseitig. Liest die Leiste sie mit, gilt:
Röhre in InkNote auf grün stellen → Schreibtisch, Aether und jede weitere App
ziehen mit, weil es dieselbe Einstellung an derselben Stelle ist.
**Ging entgegen der ersten Einschätzung auch über Site-Grenzen hinweg.** Ich
hatte notiert, das scheitere daran, dass der Browser die Sitzung nicht mitschickt
— stimmt, aber der Schlüssel für die App-Liste löst es: dieselbe Anfrage bringt
das Aussehen gleich mit. Deshalb liefert `/api/apps` jetzt `{apps, settings}`
statt nur einer Liste.
Zu tun: die fest eingetragenen Farbwerte im Shadow DOM durch dieselben
`--p*`-Variablen ersetzen, die `style.css` schon benutzt, und beim Start
`/api/me` mitnehmen (holt die Leiste ohnehin schon für den Namen).
Offen bleibt: Bei mehreren Nutzern trägt die Leiste die Farbe des
Standardnutzers, weil ihr über die Site-Grenze niemand sagen kann, wer da
gerade schaut.
### 1.2 Systemmenü im Logo
@@ -120,6 +122,16 @@ wirklich draufschaust, lohnt der Rest. Wenn nicht, war es ein Abend.
### Wie sichtbar darf die Leiste sein?
**Teilweise beantwortet:** „ausblenden" heißt seit dem 24.08. „einklappen" — es
bleibt ein 3 px hoher Strich am oberen Rand stehen, ein Klick holt die Leiste
zurück. Vorher war das eine Einbahnstraße: wer einmal ausgeblendet hatte, kam
nur über die Entwicklerkonsole wieder heran. `?honeybar=1` an der Adresse
funktioniert als Notausgang.
Damit ist der schmale Streifen technisch schon da. Offen bleibt, ob er am Handy
der **Standard** sein soll.
**Der Zwiespalt:** Die Zeile steht im HTML der App, also ist die Leiste bei
*jedem* Aufruf da — auch wenn man die Cloud einfach direkt öffnet und mit
HoneyOS gerade nichts zu tun hat. 32 px, dauerhaft, in einer App, die man auch
+30
View File
@@ -540,3 +540,33 @@ func TestSchlüsselGiltAuchHinterDemProxy(t *testing.T) {
t.Errorf("richtiger Schlüssel: %d, erwartet 200", code)
}
}
// Die Leiste stellt genau eine Anfrage — sie muss Liste und Aussehen bringen.
func TestAppListeBringtDasAussehenMit(t *testing.T) {
s := proxyServer(t, Config{ProxyAuth: true, AppsKey: "geheim123"})
if err := s.store.SetSettings(localSub, map[string]string{"phosphor": "green", "glow": "off"}); err != nil {
t.Fatal(err)
}
if err := s.store.SetApps([]App{{ID: "aether", Name: "AETHER", URL: "https://cloud.example.test"}}); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest("GET", "/api/apps", nil)
req.Header.Set("X-HoneyOS-Key", "geheim123")
rec := httptest.NewRecorder()
s.routes().ServeHTTP(rec, req)
var got struct {
Apps []App
Settings map[string]string
}
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("Antwort nicht lesbar: %v — %s", err, rec.Body)
}
if len(got.Apps) != 1 || got.Apps[0].ID != "aether" {
t.Errorf("Liste fehlt: %+v", got.Apps)
}
if got.Settings["phosphor"] != "green" || got.Settings["glow"] != "off" {
t.Errorf("Aussehen fehlt: %+v", got.Settings)
}
}
+15 -4
View File
@@ -317,11 +317,22 @@ func (s *Server) handleSetSettings(w http.ResponseWriter, r *http.Request, sub s
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) handleApps(w http.ResponseWriter, r *http.Request, _ string) {
writeJSON(w, http.StatusOK, s.store.Apps())
// handleApps liefert die Liste **und** das Aussehen. Beides zusammen, weil die
// Leiste in einer fremden App genau eine Anfrage stellen kann: Sie liegt oft auf
// einer anderen Site, kommt also nur mit dem Schlüssel an Daten und hat keine
// Sitzung, aus der sich der Rest ergäbe.
//
// Welche Einstellungen? Die des Angemeldeten, wenn es einen gibt — sonst die des
// Standardnutzers. Bei mehreren Nutzern trägt die Leiste damit die Farbe des
// Standardnutzers; für eine Person stimmt es immer.
func (s *Server) handleApps(w http.ResponseWriter, r *http.Request, sub string) {
writeJSON(w, http.StatusOK, map[string]any{
"apps": s.store.Apps(),
"settings": s.store.User(sub).Settings,
})
}
func (s *Server) handleSetApps(w http.ResponseWriter, r *http.Request, _ string) {
func (s *Server) handleSetApps(w http.ResponseWriter, r *http.Request, sub string) {
var in []App
if err := decode(r, &in); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
@@ -331,7 +342,7 @@ func (s *Server) handleSetApps(w http.ResponseWriter, r *http.Request, _ string)
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, s.store.Apps())
s.handleApps(w, r, sub)
}
/* ── Kleinkram ───────────────────────────────────────────────────────────── */
+122 -25
View File
@@ -53,6 +53,7 @@
const appsKey = d("key", "");
let apps = DEFAULT_APPS;
let settings = null;
/* ── Aufbau ───────────────────────────────────────────────────────────── */
@@ -69,55 +70,66 @@
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: #140d07;
border-bottom: 1px solid #6b4718;
color: #ffb641;
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: 0 0 5px rgba(255,182,65,.4);
text-shadow: var(--glow);
user-select: none;
}
.bar::after {
/* Scanlines, damit die Leiste zum Desktop gehört */
content: ""; position: absolute; inset: 0; pointer-events: none;
background: repeating-linear-gradient(to bottom, rgba(0,0,0,.13) 0 1px, transparent 1px 3px);
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: #ffb641; padding-left: 2px; }
.home b { color: #c8842a; font-weight: 400; }
.home:hover { border-color: #6b4718; }
.home { letter-spacing: .14em; color: var(--p); padding-left: 2px; }
.home b { color: var(--p-mid); font-weight: 400; }
.home:hover { border-color: var(--p-dim); }
.apps { display: flex; gap: 4px; flex: 1; overflow-x: auto; scrollbar-width: none; }
.apps::-webkit-scrollbar { display: none; }
.app { color: #c8842a; letter-spacing: .08em; }
.app:hover { color: #ffb641; border-color: #6b4718; }
.app { color: var(--p-mid); letter-spacing: .08em; }
.app:hover { color: var(--p); border-color: var(--p-dim); }
.app[aria-current="page"] {
background: #ffb641; border-color: #ffb641; color: #140d07; text-shadow: none;
background: var(--p); border-color: var(--p); color: var(--bg); text-shadow: none;
}
.right { display: flex; align-items: center; gap: 8px; }
.user { color: #ffb641; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user { color: var(--p); max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user[hidden] { display: none; }
.clock { color: #c8842a; font-variant-numeric: tabular-nums; }
.menu-btn { border-color: #6b4718; line-height: 1; padding: 3px 7px; }
.menu-btn:hover { background: #2a1c0c; }
.clock { color: var(--p-mid); font-variant-numeric: tabular-nums; }
.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: #140d07; border: 1px solid #6b4718; padding: 4px;
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);
}
.menu[data-open="1"] { display: flex; }
.menu a, .menu button { text-align: left; padding: 6px 8px; color: #c8842a; }
.menu a:hover, .menu button:hover { background: #2a1c0c; color: #ffb641; }
.menu hr { border: 0; border-top: 1px solid #2a1c0c; margin: 3px 0; }
.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); }
.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: #6b4718;
text-transform: uppercase; color: var(--p-dim);
}
@media (max-width: 560px) { .clock { display: none; } }
</style>
@@ -136,9 +148,35 @@
<div class="head">wechseln zu</div>
<div id="menuApps"></div>
<hr />
<button id="hide" role="menuitem">leiste ausblenden</button>
<button id="hide" role="menuitem">leiste einklappen</button>
</div>`;
/* ── Aussehen ─────────────────────────────────────────────────────────────
* Dieselben drei Röhren wie in style.css. Sie hier zu wiederholen ist die
* bewusste Entscheidung: die Leiste 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() {
@@ -219,6 +257,7 @@
}
render();
applySettings(settings);
clock();
setInterval(clock, 20000);
@@ -250,9 +289,57 @@
/* dann eben nur für diesen Aufruf */
}
unmount();
mountNub();
});
}
/**
* 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 Leiste 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", "top:0", "left:0", "right:0", "height:3px",
"z-index:2147483000", "cursor:pointer", "display:block",
].join(";");
nub.title = "HoneyOS-Leiste einblenden";
const r = nub.attachShadow({ mode: "open" });
r.innerHTML = `
<style>
.strich {
height: 3px;
background: linear-gradient(90deg, #6b4718, #ffb641 50%, #6b4718);
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();
showUser();
});
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") {
@@ -269,7 +356,16 @@
} catch {
/* kein Speicher, Leiste bleibt sichtbar */
}
if (hidden) return;
// ?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 {
@@ -281,10 +377,11 @@
headers: appsKey ? { "X-HoneyOS-Key": appsKey } : {},
});
if (res.ok) {
const list = await res.json();
if (Array.isArray(list) && list.length) {
apps = [{ id: "honeyos", name: "DESKTOP", url: homeUrl }].concat(list);
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;
}
} catch {
/* nicht angemeldet, kein CORS, kein Netz → eingebaute Liste;
+2 -2
View File
@@ -222,7 +222,7 @@ async function loadFromServer() {
// Der Server hat das letzte Wort; localStorage war nur der Vorschuss.
state.settings = { ...DEFAULT_SETTINGS, ...(readJson(LS_SETTINGS) || {}), ...(me.settings || {}) };
state.cfg.apps = await api("/api/apps");
state.cfg.apps = (await api("/api/apps")).apps;
}
/** Einstellungen vorab aus localStorage, damit die Röhre nicht erst weiß
@@ -1000,7 +1000,7 @@ function openApps() {
msg.textContent = "speichere …";
try {
const clean = draft.map(({ __neu, ...rest }) => rest);
state.cfg.apps = await api("/api/apps", { method: "PUT", body: JSON.stringify(clean) });
state.cfg.apps = (await api("/api/apps", { method: "PUT", body: JSON.stringify(clean) })).apps;
draft = state.cfg.apps.map((a) => ({ ...a }));
msg.textContent = `${state.cfg.apps.length} gespeichert.`;
render(root);