feat(honeybar): the HoneyOS bar, loaded at runtime

A strip along the top that leads to the other apps of the same system. It is
served by HoneyOS; InkNote only says where to find it, through
INKNOTE_HONEYBAR_ORIGIN. Empty means no bar and no change at all.

The origin is served by the API rather than built into the page, because an
image is pulled ready-made and a service worker keeps whole responses: anything
frozen into index.html would outlive the setting.

Layout follows the bar through --honeybar-h. The body has overflow:hidden, so
the surplus height would not have scrolled but been cut off at the bottom; and
#root padded itself around the notch that the bar now covers, which would have
counted that gap twice.

Unit 263/263, all fourteen browser suites green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-25 11:07:43 +02:00
1 parent 2ea448f091
commit 00b1d4469f
12 files changed
+144 -5

No files matched your search

+14 -1
View File
@@ -38,7 +38,20 @@
</script>
<style>
body { margin: 0; padding: 0; overflow: hidden; overscroll-behavior: none; }
#root { padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); height: 100vh; }
/* --honeybar-h is set by the HoneyOS bar while it is on the page, and is
0 when it is not. The bar sits above the app and pushes it down, so the
app has that much less room; body has overflow:hidden, which means the
difference would not scroll but simply be cut off at the bottom.
It also covers the notch itself, so the padding above must give way —
otherwise the gap is counted twice. max() keeps that from going
negative when there is no bar. */
#root {
box-sizing: border-box;
padding: max(0px, calc(env(safe-area-inset-top) - var(--honeybar-h, 0px)))
env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
height: calc(100vh - var(--honeybar-h, 0px));
}
#splash { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 16px; background: #0d1117; z-index: 9999; transition: opacity 0.3s; }
#splash.hide { opacity: 0; pointer-events: none; }
#splash .logo { width: 64px; height: 64px; border-radius: 18px; background: linear-gradient(135deg, #388bfd, #a855f7); display: flex; align-items: center; justify-content: center; font-size: 32px; animation: pulse 1.5s infinite; }
+2 -2
View File
@@ -933,7 +933,7 @@ export default function App() {
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100vh",
height: "calc(100vh - var(--honeybar-h, 0px))",
background: T.bg,
color: T.text,
fontFamily: FONT_UI,
@@ -1000,7 +1000,7 @@ export default function App() {
<div
style={{
display: "flex",
height: "100vh",
height: "calc(100vh - var(--honeybar-h, 0px))",
width: "100vw",
overflow: "hidden",
background: T.bg,
+37
View File
@@ -0,0 +1,37 @@
/**
* Load the HoneyOS bar, if this instance is configured to have one.
*
* The bar is a strip along the top of every app that belongs to the same
* system: it says where you are and takes you to the others. It lives on the
* HoneyOS server, so it stays in step everywhere without InkNote being rebuilt.
*
* Deliberately fire-and-forget. If HoneyOS is slow, down, or simply not
* configured, nothing here throws and nothing here waits — InkNote is not the
* kind of app that may fail to start because a decoration is missing. Offline
* there is no bar at all, which is the right trade for an offline-first app.
*/
export async function mountHoneybar() {
// The bar mounts itself on load; a second script tag would mean two bars.
if (document.querySelector("script[data-honeybar]")) return;
let origin, key;
try {
const r = await fetch("/api/public/config");
if (!r.ok) return;
const cfg = (await r.json()).honeybar;
if (!cfg?.origin) return;
({ origin, key } = cfg);
} catch {
return; // offline, or no server — either way, no bar
}
const el = document.createElement("script");
el.dataset.honeybar = "";
el.defer = true;
el.src = origin.replace(/\/+$/, "") + "/honeybar/honeybar.js";
// honeybar.js reads its settings from document.currentScript, which is set
// for a script inserted this way just as it is for one written in the HTML.
el.dataset.app = "inknote";
if (key) el.dataset.key = key;
document.head.appendChild(el);
}
+4
View File
@@ -1,6 +1,7 @@
import React from "react";
import ReactDOM from "react-dom/client";
import NoteApp from "./App.jsx";
import { mountHoneybar } from "./honeybar.js";
// Hide splash screen
const hideSplash = () => {
@@ -19,6 +20,9 @@ ReactDOM.createRoot(document.getElementById("root")).render(
setTimeout(hideSplash, 200);
// Not awaited: the bar is a decoration and must never hold up the app.
mountHoneybar();
// Register service worker
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
+1 -1
View File
@@ -81,7 +81,7 @@ export function AuthScreen({ onLogin, dark, initialError = "" }) {
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
minHeight: "calc(100vh - var(--honeybar-h, 0px))",
background: T.bg,
fontFamily: "'DM Sans',sans-serif",
}}
+1 -1
View File
@@ -157,7 +157,7 @@ export function Sidebar({
zIndex: isMobile ? 900 : 1,
width: isMobile ? (open ? "100vw" : 0) : open ? 260 : 0,
minWidth: isMobile ? 0 : open ? 260 : 0,
height: isMobile ? "100vh" : "auto",
height: isMobile ? "calc(100vh - var(--honeybar-h, 0px))" : "auto",
background: T.sidebar,
borderRight: isMobile ? "none" : `1px solid ${T.border}`,
display: "flex",