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:
1 parent
1c3218939b
commit
5ed4e30179
5 files changed
+191
-41
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user