feat: HoneyOS – Retro-Schreibtisch und Leiste für die eigenen Apps

Statische Oberfläche vor Aether und InkNote: kein Backend, kein Build,
nginx liefert web/ aus. Jede App bleibt ihre eigene PWA auf ihrer eigenen
Domain – HoneyOS ist Startpunkt und Umschalter, kein iframe-Rahmen.

- zwei Betriebsarten aus einem Datenstand: Terminal (Kacheln, Tastatur)
  und Desktop (Symbole, verschiebbare Fenster, Taskleiste)
- honeybar.js als Drop-in für die Apps, rendert im Shadow DOM, damit
  Tailwind und die InkNote-Palette einander nicht in die Quere kommen
- Konfiguration vollständig aus der .env: das Skript in
  /docker-entrypoint.d/ schreibt config.json beim Containerstart
- optionaler _ME-Pfad pro App zeigt an, wer angemeldet ist; ohne CORS
  bleibt die Anzeige leer, ohne dass etwas kaputtgeht

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-24 14:45:55 +02:00
commit 84cc5d619e
17 files changed
+3126

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
# ─────────────────────────────────────────────────────────────────────────────
# HoneyOS — Konfiguration
#
# cp .env.example .env && anpassen
# docker compose pull && docker compose up -d
#
# Alles hier landet beim Containerstart in config.json und wird vom Browser
# gelesen. Das heißt: **es ist öffentlich.** Adressen und Namen gehören hier
# hinein, Geheimnisse niemals — HoneyOS hat kein Backend, das eines hüten
# könnte. Nach jeder Änderung: docker compose up -d (Neustart genügt).
# ─────────────────────────────────────────────────────────────────────────────
HONEYOS_PORT=8088
# Anzeigename, solange keine App einen besseren nennt (siehe _ME weiter unten).
HONEYOS_USER=marc
HONEYOS_HOSTNAME=honeyos
HONEYOS_VERSION=0.1
# ── Apps ─────────────────────────────────────────────────────────────────────
# Durchnummeriert ab 1. Eine App entsteht durch ihre _URL — fehlt die, wird die
# Nummer übersprungen. Bis zu 12 Stück.
#
# _URL Pflicht. Wohin der Start springt.
# _ID Kurzname, klein. Muss zu data-app der Leiste passen. Standard: appN
# _NAME Beschriftung. Standard: die ID in Großbuchstaben
# _SUBTITLE Zeile darunter auf der Kachel
# _GLYPH Symbol: cloud | note | box | chip | folder | gear
# _KEY Zifferntaste zum Starten im Terminal-Modus
# _ME optional, siehe „Anmeldung" unten
# _TOKEN_KEY optional, siehe „Anmeldung" unten
HONEYOS_APP_1_ID=aether
HONEYOS_APP_1_NAME=AETHER
HONEYOS_APP_1_SUBTITLE=Cloud & Dateien
HONEYOS_APP_1_URL=https://cloud.honeyy.duckdns.org
HONEYOS_APP_1_GLYPH=cloud
HONEYOS_APP_1_KEY=1
HONEYOS_APP_1_ME=/api/v1/auth/me
HONEYOS_APP_2_ID=inknote
HONEYOS_APP_2_NAME=INKNOTE
HONEYOS_APP_2_SUBTITLE=Notizen & Zeichnen
HONEYOS_APP_2_URL=https://notes.honeyy.duckdns.org
HONEYOS_APP_2_GLYPH=note
HONEYOS_APP_2_KEY=2
HONEYOS_APP_2_ME=/api/auth/me
HONEYOS_APP_2_TOKEN_KEY=inknote_token
# HONEYOS_APP_3_ID=paperwall
# HONEYOS_APP_3_URL=https://…
# HONEYOS_APP_3_GLYPH=box
# HONEYOS_APP_3_KEY=3
# ─────────────────────────────────────────────────────────────────────────────
# Anmeldung — was HoneyOS kann und was nicht
# ─────────────────────────────────────────────────────────────────────────────
#
# HoneyOS meldet NIEMANDEN an. Es ist eine statische Seite; ein OIDC-Client
# bräuchte ein Backend und ein Client-Geheimnis, und beides gibt es hier nicht
# — absichtlich. Die Anmeldung bleibt Sache von Aether und InkNote, die beide
# an PocketID hängen.
#
# Was HoneyOS kann: **anzeigen**, wer angemeldet ist. Dafür ist _ME da — der
# Pfad, unter dem die App „wer bin ich" beantwortet. Relativ zu _URL oder
# absolut. Aus der Antwort wird der Anzeigename gefischt (display_name,
# displayName, name, username), verschachtelt unter `user` wird auch gefunden.
#
# Zwei Wege, wie das gelesen wird — und sie verhalten sich unterschiedlich:
#
# 1. Die Leiste IN der App (honeybar.js) fragt ihre eigene App.
# Gleicher Ursprung, also **funktioniert das sofort**, ohne dass an Aether
# oder InkNote etwas geändert werden muss.
#
# 2. Der Desktop auf os.… fragt über die Domaingrenze.
# Dafür muss die App diesen Ursprung erlauben, sonst blockt der Browser:
# InkNote: ALLOWED_ORIGINS=https://os.honeyy.duckdns.org
# Aether: CORS mit Access-Control-Allow-Credentials — noch nicht drin
# Ohne das bleibt die Anzeige einfach leer. Nichts geht kaputt, die
# Lampe „online" funktioniert weiter.
#
# _TOKEN_KEY brauchen nur Apps, die einen Bearer-Token im localStorage halten
# statt eines Cookies (InkNote tut das). Der Token ist pro Domain sichtbar —
# von os.… aus also NICHT lesbar. Für die Leiste in der App reicht es.
#
# Und wichtig: alle Adressen müssen unter derselben Domain liegen
# (…​.honeyy.duckdns.org). Sonst behandelt der Browser sie als fremde Site und
# schickt die Sitzung gar nicht erst mit.
+3
View File
@@ -0,0 +1,3 @@
.env
*.tmp
shots/
+20
View File
@@ -0,0 +1,20 @@
# HoneyOS — statisch. Kein Build, kein Backend: nginx liefert web/ aus.
#
# Die Konfiguration entsteht beim Start aus der .env — das Skript in
# /docker-entrypoint.d/ schreibt config.json, bevor nginx hochkommt.
FROM nginx:1.27-alpine
LABEL org.opencontainers.image.title="HoneyOS" \
org.opencontainers.image.description="Retro-Schreibtisch und Umschalter für die eigenen Apps" \
org.opencontainers.image.source="https://gitea.honeyy.duckdns.org/Docker/HoneyOS"
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY docker-entrypoint.d/20-honeyos-config.sh /docker-entrypoint.d/20-honeyos-config.sh
COPY web/ /usr/share/nginx/html/
RUN chmod +x /docker-entrypoint.d/20-honeyos-config.sh
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -qO- http://127.0.0.1/healthz || exit 1
+164
View File
@@ -0,0 +1,164 @@
# HoneyOS
Ein Retro-Schreibtisch vor den eigenen Apps. Amber-CRT, Boot-Log, zwei Betriebsarten —
und dieselbe Leiste zum Einhängen in Aether und InkNote.
**Was es ist:** eine Oberfläche davor. Jede App bleibt ihre eigene PWA auf ihrer eigenen
Domain mit ihrer eigenen Arbeitsumgebung und ihrer eigenen Anmeldung. HoneyOS ist der
Startpunkt, der Umschalter und die gemeinsame Leiste — **kein Rahmen, der die Apps
einsperrt** (kein iframe).
**Was es nicht ist:** eine Anmeldung. HoneyOS ist eine statische Seite ohne Backend; ein
OIDC-Client bräuchte ein Client-Geheimnis und eine Sitzung, und beides gehört nicht in
etwas, das der Browser vollständig lesen kann. Angemeldet wird bei Aether und InkNote,
die beide an PocketID hängen. HoneyOS kann den Stand nur **anzeigen** — siehe unten.
## Betrieb
```bash
cp .env.example .env # Adressen und Namen eintragen
docker login gitea.honeyy.duckdns.org
docker compose pull && docker compose up -d
```
Es gibt nichts zu sichern: keine Datenbank, kein Volume. Der ganze Zustand ist die `.env` —
und was sich der Browser pro Gerät merkt (Betriebsart, Röhrenfarbe).
Hinter deinem Reverse Proxy erwartet HoneyOS nichts Besonderes: `proxy_pass` auf den Port,
fertig. `/healthz` antwortet mit `ok`.
## Konfiguration
Alles kommt aus der `.env`. Beim Containerstart schreibt
[`docker-entrypoint.d/20-honeyos-config.sh`](docker-entrypoint.d/20-honeyos-config.sh)
daraus `config.json`, die der Browser liest. Nach jeder Änderung `docker compose up -d`.
**Alles darin ist öffentlich.** Adressen und Namen gehören hinein, Geheimnisse niemals.
Eine App entsteht durch ihre `_URL`; fehlt die, wird die Nummer übersprungen (bis 12):
```dotenv
HONEYOS_APP_1_ID=aether
HONEYOS_APP_1_NAME=AETHER
HONEYOS_APP_1_SUBTITLE=Cloud & Dateien
HONEYOS_APP_1_URL=https://cloud.honeyy.duckdns.org
HONEYOS_APP_1_GLYPH=cloud # cloud | note | box | chip | folder | gear
HONEYOS_APP_1_KEY=1 # Zifferntaste im Terminal-Modus
HONEYOS_APP_1_ME=/api/v1/auth/me # optional, siehe „Wer ist angemeldet"
```
Ohne `HONEYOS_APP_1_URL` bleibt die eingebackene `web/config.json` gültig — ein
`docker run` ohne `.env` zeigt also die Standard-Apps. Adressen lassen sich zusätzlich im
Browser unter *Einstellungen* überschreiben; das gilt dann nur für dieses Gerät.
## Zwei Betriebsarten, ein Datenstand
| | Terminal-Modus | Desktop-Modus |
|---|---|---|
| Apps | Kacheln nebeneinander | Symbole auf der Fläche, Doppelklick öffnet |
| Fenster | Dialog in der Mitte | verschiebbar, minimierbar |
| unten | Statuszeile mit Tastenhinweisen | Taskleiste mit Startmenü |
Umschalten mit **`M`**, über *Einstellungen → Betriebsart* oder im Startmenü.
Gleich bleiben in beiden: die Menüleiste oben, die Palette, die Einstellungen, `config.json`.
`renderTiles()` und `renderDeskIcons()` in `os.js` zeichnen aus denselben Daten — eine neue
App erscheint ohne Zutun in beiden.
**Tasten:** `1` `2` App starten · `M` Betriebsart · `S` Einstellungen · `?` Hilfe ·
`Esc` schließt Startmenü, oberstes Fenster oder Dialog — in dieser Reihenfolge.
## Die Leiste in Aether / InkNote
Eine Zeile ins `frontend/index.html` der App:
```html
<script src="https://os.honeyy.duckdns.org/honeybar/honeybar.js"
data-app="aether"></script>
```
`data-app` ist die `id` aus der Konfiguration — danach markiert sich die Leiste selbst als
aktiv. Die App-Liste holt sie von allein neben ihrem eigenen Skript.
Sie rendert in einem **Shadow DOM**: Tailwind (Aether) und die InkNote-Palette kommen nicht
hinein, ihre Stile nicht hinaus. Sie schiebt den Seiteninhalt um 32 px nach unten
(`data-push="none"` legt sie stattdessen darüber). Rechnet die App mit voller Höhe:
```css
.app-shell { height: calc(100vh - var(--honeybar-h, 0px)); }
```
Vorschau ohne Einbau: `/honeybar/demo.html`.
## Wer ist angemeldet
`_ME` ist der Pfad, unter dem eine App „wer bin ich" beantwortet (relativ zur `_URL` oder
absolut). Aus der Antwort wird der Anzeigename gefischt — `display_name`, `displayName`,
`name`, `username`, auch verschachtelt unter `user`.
Gelesen wird das an zwei Stellen, und die verhalten sich **unterschiedlich**:
1. **Die Leiste in der App** fragt ihre eigene App. Gleicher Ursprung → funktioniert
sofort, ohne Änderung an Aether oder InkNote.
2. **Der Desktop auf `os.…`** fragt über die Domaingrenze. Dafür muss die App diesen
Ursprung erlauben:
- InkNote: `ALLOWED_ORIGINS=https://os.honeyy.duckdns.org`
- Aether: CORS mit `Access-Control-Allow-Credentials` — noch nicht vorhanden
Fehlt das, bleibt die Anzeige leer und sonst passiert nichts. Die Lampe „online"
funktioniert unabhängig davon weiter.
`_TOKEN_KEY` brauchen nur Apps, die einen Bearer-Token im localStorage halten statt eines
Cookies (InkNote: `inknote_token`). Der ist pro Domain sichtbar — von `os.…` aus also nicht
lesbar, für die Leiste in der App dagegen schon.
**Voraussetzung für all das:** alle Adressen liegen unter derselben Domain
(`*.honeyy.duckdns.org`). Sonst behandelt der Browser sie als fremde Site und schickt die
Sitzung gar nicht erst mit.
## Entwickeln
```bash
docker compose -f docker-compose.dev.yml up -d --build # → http://localhost:8088
```
`web/` ist gemountet: Datei ändern, Seite neu laden. Weil der Mount schreibgeschützt ist,
greift dort die `.env`-Konfiguration **nicht** — es gilt die eingebackene `web/config.json`.
Zum Prüfen der `.env` das echte Image nehmen (`docker compose up -d --build`).
```
web/
index.html Desktop
style.css alle Farben in den --p*-Variablen ganz oben
os.js Boot, beide Ansichten, Fenster, Einstellungen
config.json Standardkonfiguration (wird zur Laufzeit aus der .env ersetzt)
honeybar/ die einbettbare Leiste + Vorschauseite
looks/ zwei verworfene Entwürfe (System 7, Win95) — nur Ansicht
```
Neue Symbole sind 12×12-Pixelraster in `os.js` (`GLYPHS`).
## Bauen und schieben
```bash
./build.sh # → gitea.honeyy.duckdns.org/docker/honeyos:latest
./build.sh 0.2 # mit Tag
```
## Grenzen, die man kennen sollte
- **localStorage ist pro Domain.** Betriebsart und Röhrenfarbe auf `os.…` kennt die Leiste
auf `notes.…` nicht. Der Umschalter ist ein Schalter, kein geteilter Zustand.
- **Die Lampe ist kein Health-Check.** Bei einer fremden Domain darf der Browser nur sehen,
*dass* geantwortet wurde, nicht *was*. Ein 500er sähe aus wie „online".
- **`config.json` und `honeybar.js` gehen mit `Access-Control-Allow-Origin: *` raus**, weil
die anderen Domains sie holen. Öffentliche Konfiguration — nichts Vertrauliches hinein.
## Was als Nächstes ansteht
1. **CORS in Aether** für `os.…`, damit der Desktop den Anmeldestand sehen kann. InkNote
hat mit `ALLOWED_ORIGINS` schon den Haken dafür.
2. **Überall abmelden** in der Leiste. Braucht pro App den CSRF-Weg zum Logout — bei Aether
ein `POST` mit Token, das geht nicht nebenbei über die Domaingrenze.
3. **Fenster mit Inhalt** statt nur Verweisen: „letzte Dateien", „letzte Notizen". Fenster
zeigen dann Daten aus den APIs — die Apps selbst bleiben in ihrem eigenen Tab.
Executable
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
set -e
REGISTRY="gitea.honeyy.duckdns.org"
OWNER="${GITEA_OWNER:-docker}"
TAG="${1:-latest}"
IMAGE="${REGISTRY}/${OWNER}/honeyos:${TAG}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " HoneyOS — Build & Push to Gitea"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " Image: ${IMAGE}"
echo ""
echo "→ Logging in to ${REGISTRY}..."
docker login "${REGISTRY}"
echo ""
echo "→ Building..."
docker build -t "${IMAGE}" .
echo ""
echo "→ Pushing..."
docker push "${IMAGE}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " ✅ Done!"
echo " ${IMAGE}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+11
View File
@@ -0,0 +1,11 @@
# Entwicklung: web/ wird hineingemountet, ein Reload im Browser reicht.
services:
honeyos:
build: .
image: honeyos:dev
container_name: honeyos-dev
ports:
- "${HONEYOS_PORT:-8088}:80"
volumes:
- ./web:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
+59
View File
@@ -0,0 +1,59 @@
# ─────────────────────────────────────────────
# HoneyOS — Betrieb aus der Gitea-Registry
#
# cp .env.example .env && anpassen
# docker login gitea.honeyy.duckdns.org
# docker compose pull && docker compose up -d
#
# Es gibt nichts zu sichern: HoneyOS hat keine Datenbank und kein Volume.
# Der ganze Zustand ist die .env — und was der Browser sich pro Gerät in
# localStorage merkt (Betriebsart, Röhre).
# ─────────────────────────────────────────────
services:
honeyos:
image: gitea.honeyy.duckdns.org/docker/honeyos:latest
container_name: honeyos
restart: unless-stopped
ports:
- "${HONEYOS_PORT:-8088}:80"
environment:
- HONEYOS_USER=${HONEYOS_USER:-}
- HONEYOS_HOSTNAME=${HONEYOS_HOSTNAME:-honeyos}
- HONEYOS_VERSION=${HONEYOS_VERSION:-0.1}
- HONEYOS_APP_1_ID=${HONEYOS_APP_1_ID:-}
- HONEYOS_APP_1_NAME=${HONEYOS_APP_1_NAME:-}
- HONEYOS_APP_1_SUBTITLE=${HONEYOS_APP_1_SUBTITLE:-}
- HONEYOS_APP_1_URL=${HONEYOS_APP_1_URL:-}
- HONEYOS_APP_1_GLYPH=${HONEYOS_APP_1_GLYPH:-}
- HONEYOS_APP_1_KEY=${HONEYOS_APP_1_KEY:-}
- HONEYOS_APP_1_ME=${HONEYOS_APP_1_ME:-}
- HONEYOS_APP_1_TOKEN_KEY=${HONEYOS_APP_1_TOKEN_KEY:-}
- HONEYOS_APP_2_ID=${HONEYOS_APP_2_ID:-}
- HONEYOS_APP_2_NAME=${HONEYOS_APP_2_NAME:-}
- HONEYOS_APP_2_SUBTITLE=${HONEYOS_APP_2_SUBTITLE:-}
- HONEYOS_APP_2_URL=${HONEYOS_APP_2_URL:-}
- HONEYOS_APP_2_GLYPH=${HONEYOS_APP_2_GLYPH:-}
- HONEYOS_APP_2_KEY=${HONEYOS_APP_2_KEY:-}
- HONEYOS_APP_2_ME=${HONEYOS_APP_2_ME:-}
- HONEYOS_APP_2_TOKEN_KEY=${HONEYOS_APP_2_TOKEN_KEY:-}
- HONEYOS_APP_3_ID=${HONEYOS_APP_3_ID:-}
- HONEYOS_APP_3_NAME=${HONEYOS_APP_3_NAME:-}
- HONEYOS_APP_3_SUBTITLE=${HONEYOS_APP_3_SUBTITLE:-}
- HONEYOS_APP_3_URL=${HONEYOS_APP_3_URL:-}
- HONEYOS_APP_3_GLYPH=${HONEYOS_APP_3_GLYPH:-}
- HONEYOS_APP_3_KEY=${HONEYOS_APP_3_KEY:-}
- HONEYOS_APP_3_ME=${HONEYOS_APP_3_ME:-}
- HONEYOS_APP_3_TOKEN_KEY=${HONEYOS_APP_3_TOKEN_KEY:-}
- HONEYOS_APP_4_ID=${HONEYOS_APP_4_ID:-}
- HONEYOS_APP_4_NAME=${HONEYOS_APP_4_NAME:-}
- HONEYOS_APP_4_SUBTITLE=${HONEYOS_APP_4_SUBTITLE:-}
- HONEYOS_APP_4_URL=${HONEYOS_APP_4_URL:-}
- HONEYOS_APP_4_GLYPH=${HONEYOS_APP_4_GLYPH:-}
- HONEYOS_APP_4_KEY=${HONEYOS_APP_4_KEY:-}
- HONEYOS_APP_4_ME=${HONEYOS_APP_4_ME:-}
- HONEYOS_APP_4_TOKEN_KEY=${HONEYOS_APP_4_TOKEN_KEY:-}
+79
View File
@@ -0,0 +1,79 @@
#!/bin/sh
# Schreibt config.json aus den Umgebungsvariablen — läuft vor nginx.
#
# Das nginx-Image führt alles in /docker-entrypoint.d/ beim Start aus. Damit
# braucht HoneyOS keinen eigenen Entrypoint und kein Backend: die Konfiguration
# entsteht einmal beim Hochfahren, danach ist es wieder reines Ausliefern.
#
# Ohne HONEYOS_APP_1_URL bleibt die eingebackene config.json unangetastet —
# ein `docker run` ohne .env zeigt also weiter die Standard-Apps.
set -eu
TARGET=/usr/share/nginx/html/config.json
MAX_APPS=12
# Wert einer Variablen holen, deren Name erst zur Laufzeit feststeht.
get() { eval "printf '%s' \"\${$1:-}\""; }
# Für JSON entschärfen: Backslash und Anführungszeichen. Zeilenumbrüche haben
# in diesen Feldern nichts zu suchen und fliegen raus.
esc() { printf '%s' "$1" | tr -d '\n\r' | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
# Ein Feld nur schreiben, wenn es gesetzt ist — leere Schlüssel wären Lärm.
field() {
[ -n "$2" ] || return 0
printf ',"%s":"%s"' "$1" "$(esc "$2")"
}
if [ -z "$(get HONEYOS_APP_1_URL)" ]; then
echo "honeyos: keine HONEYOS_APP_*-Variablen gesetzt — eingebackene config.json bleibt"
exit 0
fi
if [ ! -w "$(dirname "$TARGET")" ]; then
echo "honeyos: $(dirname "$TARGET") ist nicht beschreibbar (read-only mount?) — config.json bleibt wie sie ist"
exit 0
fi
tmp="${TARGET}.tmp"
{
printf '{'
printf '"user":"%s"' "$(esc "$(get HONEYOS_USER)")"
printf ',"hostname":"%s"' "$(esc "${HONEYOS_HOSTNAME:-honeyos}")"
printf ',"version":"%s"' "$(esc "${HONEYOS_VERSION:-0.1}")"
printf ',"apps":['
n=1
first=1
while [ "$n" -le "$MAX_APPS" ]; do
url=$(get "HONEYOS_APP_${n}_URL")
if [ -z "$url" ]; then
n=$((n + 1))
continue
fi
id=$(get "HONEYOS_APP_${n}_ID")
[ -n "$id" ] || id="app${n}"
name=$(get "HONEYOS_APP_${n}_NAME")
[ -n "$name" ] || name=$(printf '%s' "$id" | tr '[:lower:]' '[:upper:]')
[ "$first" -eq 1 ] || printf ','
first=0
printf '{"id":"%s","name":"%s","url":"%s"' "$(esc "$id")" "$(esc "$name")" "$(esc "$url")"
field subtitle "$(get "HONEYOS_APP_${n}_SUBTITLE")"
field glyph "$(get "HONEYOS_APP_${n}_GLYPH")"
field key "$(get "HONEYOS_APP_${n}_KEY")"
field me "$(get "HONEYOS_APP_${n}_ME")"
field tokenKey "$(get "HONEYOS_APP_${n}_TOKEN_KEY")"
printf '}'
n=$((n + 1))
done
printf ']}'
} > "$tmp"
mv "$tmp" "$TARGET"
count=$(tr -cd '{' < "$TARGET" | wc -c)
echo "honeyos: config.json geschrieben ($((count - 1)) App(s))"
+30
View File
@@ -0,0 +1,30 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Für den Healthcheck im Dockerfile und für den Reverse Proxy.
location = /healthz {
access_log off;
add_header Content-Type text/plain;
return 200 "ok\n";
}
# Das statische Blech: alles kurz cachen, nichts endlos.
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
# config.json und honeybar.js werden von den anderen Domains geholt.
# Beides ist öffentliche Konfiguration ohne Geheimnisse — CORS offen ist
# hier die richtige Antwort und keine Bequemlichkeit.
location ~* \.(json|js)$ {
add_header Access-Control-Allow-Origin "*";
add_header Cache-Control "no-cache";
}
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
}
+26
View File
@@ -0,0 +1,26 @@
{
"user": "marc",
"hostname": "honeyos",
"version": "0.1",
"apps": [
{
"id": "aether",
"name": "AETHER",
"subtitle": "Cloud & Dateien",
"url": "https://cloud.honeyy.duckdns.org",
"glyph": "cloud",
"key": "1",
"me": "/api/v1/auth/me"
},
{
"id": "inknote",
"name": "INKNOTE",
"subtitle": "Notizen & Zeichnen",
"url": "https://notes.honeyy.duckdns.org",
"glyph": "note",
"key": "2",
"me": "/api/auth/me",
"tokenKey": "inknote_token"
}
]
}
+41
View File
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>HoneyBar — Vorschau in einer fremden App</title>
<style>
/* Bewusst NICHT im HoneyOS-Stil: hier wird geprüft, dass die Leiste
eine ganz andere App nicht anfasst und von ihr nicht angefasst wird. */
body {
margin: 0;
background: #0d1117;
color: #e6edf3;
font: 15px/1.6 system-ui, sans-serif;
}
header.fake {
padding: 14px 20px;
background: #161b22;
border-bottom: 1px solid #30363d;
font-weight: 600;
}
main { padding: 24px 20px; max-width: 620px; }
code { background: #161b22; padding: 2px 5px; border-radius: 4px; font-size: 13px; }
/* So rechnet eine App mit Vollhöhe die Leiste heraus: */
.full { height: calc(100vh - var(--honeybar-h, 0px)); }
</style>
</head>
<body>
<header class="fake">Irgendeine App (dunkles Theme, eigene Schrift)</header>
<main>
<p>Die Leiste oben kommt aus <code>honeybar.js</code> und gehört nicht zu dieser Seite.</p>
<p>
Sie liegt in einem Shadow DOM: nichts von hier färbt auf sie ab, nichts von ihr auf
diese Seite. <code>data-app="inknote"</code> markiert, wo man gerade ist.
</p>
<p>Eine App, die mit <code>100vh</code> rechnet, nimmt <code>--honeybar-h</code> ab.</p>
</main>
<script src="./honeybar.js" data-app="inknote" data-home="../index.html"></script>
</body>
</html>
+290
View File
@@ -0,0 +1,290 @@
/*
* HoneyBar — die HoneyOS-Leiste 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 Leiste 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 Leiste
* nichts merken außer den 32 Pixeln oben.
*
* data-Attribute am <script>:
* data-app (Pflicht) id der App, in der die Leiste steckt
* data-apps URL zu einer config.json; ohne das gilt die Liste unten
* data-home URL des HoneyOS-Desktops
* data-push "body" (Standard) schiebt den Seiteninhalt nach unten,
* "none" legt die Leiste darüber
*
* Wichtig zu wissen: localStorage ist pro Domain. Was auf os.honeyy… steht,
* sieht diese Leiste auf notes.honeyy… nicht — die Leiste 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";
const DEFAULT_APPS = [
{ id: "honeyos", name: "DESKTOP", url: "https://os.honeyy.duckdns.org" },
{ id: "aether", name: "AETHER", url: "https://cloud.honeyy.duckdns.org" },
{ id: "inknote", name: "INKNOTE", url: "https://notes.honeyy.duckdns.org" },
];
const current = d("app", "");
const homeUrl = d("home", "https://os.honeyy.duckdns.org");
const pushMode = d("push", "body");
// Ohne data-apps holt die Leiste die Konfiguration neben ihrem eigenen
// Skript — dieselbe Datei, die der Desktop liest.
const appsUrl = d("apps", script ? new URL("../config.json", script.src).href : "");
let apps = DEFAULT_APPS;
/* ── Aufbau ───────────────────────────────────────────────────────────── */
const host = document.createElement("honey-bar");
host.style.cssText = [
"position:fixed", "top:0", "left:0", "right:0",
"height:" + BAR_H + "px",
"z-index:2147483000",
"display:block",
"color-scheme:dark",
].join(";");
const root = host.attachShadow({ mode: "open" });
root.innerHTML = `
<style>
:host, * { box-sizing: border-box; }
.bar {
display: flex; align-items: center; gap: 10px;
height: ${BAR_H}px; padding: 0 8px;
background: #140d07;
border-bottom: 1px solid #6b4718;
color: #ffb641;
font: 12px/1 ui-monospace, "SF Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
text-shadow: 0 0 5px rgba(255,182,65,.4);
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);
}
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; }
.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[aria-current="page"] {
background: #ffb641; border-color: #ffb641; color: #140d07; 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[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; }
.menu {
position: absolute; top: ${BAR_H + 2}px; right: 6px; min-width: 190px;
background: #140d07; border: 1px solid #6b4718; 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 .head {
padding: 4px 8px; font-size: 10px; letter-spacing: .18em;
text-transform: uppercase; color: #6b4718;
}
@media (max-width: 560px) { .clock { display: none; } }
</style>
<div class="bar">
<a class="home" id="home" title="HoneyOS">&#9632; HONEY<b>OS</b></a>
<nav class="apps" id="apps"></nav>
<div class="right">
<span class="user" id="user" 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" id="menu" role="menu">
<div class="head">wechseln zu</div>
<div id="menuApps"></div>
<hr />
<button id="hide" role="menuitem">leiste ausblenden</button>
</div>`;
/* ── Verhalten ────────────────────────────────────────────────────────── */
function render() {
const nav = root.getElementById("apps");
const menuApps = root.getElementById("menuApps");
nav.innerHTML = "";
menuApps.innerHTML = "";
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;
a.textContent = app.name;
if (app.id === current) a.setAttribute("aria-current", "page");
nav.appendChild(a);
const m = document.createElement("a");
m.href = app.url;
m.setAttribute("role", "menuitem");
m.textContent = (app.id === current ? "▸ " : "  ") + app.name;
menuApps.appendChild(m);
});
}
/**
* Zeigt an, wer angemeldet ist. Die Leiste steckt in der App selbst, der
* Aufruf geht also an denselben Ursprung — kein CORS, keine Freigabe nötig.
* Antwortet die App nicht wie erwartet, bleibt das Feld leer und niemand
* merkt etwas: die Anzeige ist eine Zugabe, keine Zusicherung.
*/
async function showUser() {
const me = apps.find((a) => a.id === current);
if (!me || !me.me) return;
try {
const headers = {};
if (me.tokenKey) {
const tok = localStorage.getItem(me.tokenKey);
if (tok) headers.Authorization = "Bearer " + tok;
}
const res = await fetch(url(me, me.me), { credentials: "same-origin", cache: "no-store", headers });
if (!res.ok) return;
const data = await res.json();
const u = (data && (data.user || data.profile)) || data || {};
const name = u.display_name || u.displayName || u.name || u.username || u.preferred_username;
if (!name) return;
const el = root.getElementById("user");
el.textContent = name;
el.hidden = false;
} catch {
/* nicht angemeldet, kein Endpunkt, kein Netz — alles kein Grund zu lärmen */
}
}
function url(app, path) {
if (!path) return app.url;
if (/^https?:\/\//i.test(path)) return path;
return app.url.replace(/\/+$/, "") + (path.startsWith("/") ? path : "/" + path);
}
function clock() {
root.getElementById("clock").textContent = new Date().toLocaleTimeString("de-DE", {
hour: "2-digit",
minute: "2-digit",
});
}
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", BAR_H + "px");
if (pushMode === "body") {
const cs = getComputedStyle(document.body);
document.body.style.paddingTop = parseFloat(cs.paddingTop || 0) + BAR_H + "px";
}
render();
clock();
setInterval(clock, 20000);
root.getElementById("home").href = homeUrl;
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));
});
// 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();
});
root.getElementById("hide").addEventListener("click", () => {
try {
localStorage.setItem(LS_HIDDEN, "1");
} catch {
/* dann eben nur für diesen Aufruf */
}
unmount();
});
}
function unmount() {
host.remove();
if (pushMode === "body") {
const cs = getComputedStyle(document.body);
document.body.style.paddingTop = Math.max(0, parseFloat(cs.paddingTop || 0) - BAR_H) + "px";
}
document.documentElement.style.removeProperty("--honeybar-h");
}
async function start() {
let hidden = false;
try {
hidden = localStorage.getItem(LS_HIDDEN) === "1";
} catch {
/* kein Speicher, Leiste bleibt sichtbar */
}
if (hidden) return;
if (appsUrl) {
try {
const res = await fetch(appsUrl, { cache: "no-store" });
if (res.ok) {
const cfg = await res.json();
if (Array.isArray(cfg.apps) && cfg.apps.length) {
apps = [{ id: "honeyos", name: "DESKTOP", url: homeUrl }].concat(cfg.apps);
}
}
} catch {
/* Liste nicht erreichbar → eingebaute Liste, Leiste funktioniert trotzdem */
}
}
if (document.body) mount();
else document.addEventListener("DOMContentLoaded", mount, { once: true });
showUser();
}
start();
})();
+86
View File
@@ -0,0 +1,86 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0a0705" />
<title>HoneyOS</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%230a0705'/%3E%3Cpath d='M4 4h2v3h4V4h2v8h-2V9H6v3H4z' fill='%23ffb641'/%3E%3C/svg%3E" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<!-- Bildschirm: alles liegt darin, damit Scanlines/Vignette darüber passen -->
<div class="crt" id="screen">
<!-- ── Boot ───────────────────────────────────────────────────────── -->
<div class="boot" id="boot" hidden>
<pre class="boot-log" id="bootLog" aria-live="polite"></pre>
<p class="boot-hint">[ beliebige Taste = überspringen ]</p>
</div>
<!-- ── Desktop ────────────────────────────────────────────────────── -->
<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">
<span class="hex">&#9632;</span> HONEY<b>OS</b>
</button>
<nav class="mb-apps" id="mbApps" aria-label="Anwendungen"></nav>
<div class="mb-right">
<span class="mb-user" id="mbUser">marc</span>
<span class="mb-clock" id="clock">--:--</span>
<button class="mb-btn" id="btnSettings" title="Einstellungen (S)">&#9881;</button>
</div>
</header>
<main class="stage" id="stage">
<!-- Statusfenster -->
<section class="win win-status">
<div class="win-bar"><span class="win-title">system</span><span class="win-dots">&#9679;&#9679;&#9679;</span></div>
<div class="win-body">
<p class="greet" id="greet">Willkommen zurück.</p>
<dl class="statlist" id="statList"></dl>
</div>
</section>
<!-- App-Kacheln -->
<section class="tiles" id="tiles" aria-label="Apps starten"></section>
</main>
<!-- Desktop-Modus: Symbolfläche, Fenster, Taskleiste -->
<div class="deskspace" id="deskspace" hidden>
<div class="desk-icons" id="deskIcons"></div>
<div class="windows" id="windows"></div>
</div>
<footer class="taskbar" id="taskbar" hidden>
<button class="task-start" id="btnStart">&#9632; START</button>
<div class="task-list" id="taskList"></div>
<span class="task-date" id="taskDate"></span>
</footer>
<footer class="statusbar" id="statusbar">
<span id="hint">[1] [2] app starten &nbsp;·&nbsp; [M] modus &nbsp;·&nbsp; [S] einstellungen &nbsp;·&nbsp; [?] hilfe</span>
<span class="sb-right" id="sbRight"></span>
</footer>
</div>
<!-- ── Dialoge ────────────────────────────────────────────────────── -->
<div class="modal" id="modal" hidden>
<div class="win win-modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="win-bar">
<span class="win-title" id="modalTitle">einstellungen</span>
<button class="win-close" id="btnClose" aria-label="Schließen">&times;</button>
</div>
<div class="win-body" id="modalBody"></div>
</div>
</div>
<!-- CRT-Effekte: rein dekorativ, liegen über allem -->
<div class="scanlines" aria-hidden="true"></div>
<div class="vignette" aria-hidden="true"></div>
<div class="flicker" aria-hidden="true"></div>
</div>
<script src="./os.js"></script>
</body>
</html>
+184
View File
@@ -0,0 +1,184 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>HoneyOS — Look: System 7</title>
<style>
/* ── Mac System 7 ────────────────────────────────────────────────────
Hellgrau, 1 Bit, alles mit 1px schwarzer Kante. Keine Farbe außer
der Auswahl. Schrift klein und eng — Chicago gibt es nicht mehr,
Geneva/Verdana kommt am nächsten. */
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
font: 12px/1.35 Geneva, Verdana, "DejaVu Sans", sans-serif;
color: #000;
/* der geditherte Schreibtisch: 2px-Schachbrett */
background: repeating-conic-gradient(#8a8a8a 0 25%, #d0d0d0 0 50%) 0 0 / 3px 3px;
overflow: hidden;
-webkit-font-smoothing: none;
}
/* Menüleiste */
.menubar {
display: flex; align-items: center; gap: 18px;
height: 21px; padding: 0 10px;
background: #fff; border-bottom: 1px solid #000;
position: relative; z-index: 10;
}
.menubar .apple { font-weight: 700; letter-spacing: -0.5px; }
.menubar .item { cursor: default; }
.menubar .item:hover { background: #000; color: #fff; }
.menubar .right { margin-left: auto; display: flex; gap: 14px; align-items: center; }
/* Fenster */
.win {
position: absolute;
background: #fff;
border: 1px solid #000;
box-shadow: 2px 2px 0 rgba(0,0,0,.45);
}
.titlebar {
display: flex; align-items: center; gap: 6px;
height: 19px; padding: 0 5px;
border-bottom: 1px solid #000;
/* die sechs Streifen links und rechts des Titels */
background: repeating-linear-gradient(to bottom, #000 0 1px, #fff 1px 2px);
}
.titlebar .box {
width: 11px; height: 11px; background: #fff; border: 1px solid #000; flex: none;
}
.titlebar .name {
background: #fff; padding: 0 7px; font-weight: 700; white-space: nowrap;
margin: 0 auto; letter-spacing: -0.2px;
}
.win-body { padding: 10px 12px; }
/* Statusfenster */
#status { left: 26px; top: 44px; width: 268px; }
#status h1 { font-size: 12px; font-weight: 700; margin: 0 0 8px; }
.rows { display: grid; grid-template-columns: 1fr auto; gap: 2px 10px; }
.rows span:nth-child(odd) { color: #444; }
.rows span:nth-child(even) { font-weight: 700; }
.sep { border: 0; border-top: 1px solid #000; margin: 9px 0; }
.note { color: #555; font-size: 11px; }
/* Fenster mit den Programmen — ein Finder-Fenster, kein Kachelraster */
#apps { left: 330px; top: 44px; width: 372px; }
.shelf { display: flex; gap: 6px; padding: 14px 10px 18px; }
.icon {
width: 104px; padding: 6px 4px 4px; text-align: center; cursor: default;
border: 1px solid transparent;
}
.icon svg { display: block; margin: 0 auto 5px; image-rendering: pixelated; }
.icon .label { display: inline-block; padding: 0 3px; }
.icon:hover .label { background: #000; color: #fff; }
.icon.sel svg { filter: invert(1); }
.icon.sel .label { background: #000; color: #fff; }
.shelf-foot {
border-top: 1px solid #000; padding: 2px 8px; font-size: 11px; color: #333;
display: flex; justify-content: space-between;
}
/* Leiste, wie sie in Aether/InkNote aussähe */
#bar { left: 26px; top: 296px; width: 676px; }
.bardemo {
display: flex; align-items: center; gap: 14px;
height: 21px; padding: 0 8px; background: #fff; border-bottom: 1px solid #000;
font-size: 11px;
}
.bardemo .cur { background: #000; color: #fff; padding: 0 5px; }
.bardemo .right { margin-left: auto; }
.barhost { padding: 12px; color: #333; font-size: 11px; background: #efefef; }
/* Papierkorb unten rechts, weil ohne den ist es kein Mac */
.trash { position: absolute; right: 30px; bottom: 26px; width: 90px; text-align: center; }
.trash .label { background: #d0d0d0; padding: 0 3px; }
.switch {
position: absolute; left: 26px; bottom: 20px;
font-size: 11px; background: #d0d0d0; padding: 2px 6px; border: 1px solid #000;
}
.switch a { color: #000; }
</style>
</head>
<body>
<div class="menubar">
<span class="apple">&#9670;</span>
<span class="item">Ablage</span>
<span class="item">Bearbeiten</span>
<span class="item">Ansicht</span>
<span class="item">Spezial</span>
<span class="right"><span class="item">Di. 02:47</span><span class="item">HoneyOS &#9662;</span></span>
</div>
<!-- Statusfenster -->
<div class="win" id="status">
<div class="titlebar"><span class="box"></span><span class="name">HoneyOS</span></div>
<div class="win-body">
<h1>Willkommen zurück, marc.</h1>
<div class="rows">
<span>Programme</span><span>2</span>
<span>erreichbar</span><span>2 von 2</span>
<span>Version</span><span>0.1</span>
</div>
<hr class="sep" />
<p class="note">Jedes Programm läuft auf seiner eigenen Domain. HoneyOS öffnet sie nur.</p>
</div>
</div>
<!-- Programme -->
<div class="win" id="apps">
<div class="titlebar"><span class="box"></span><span class="name">Programme</span><span class="box"></span></div>
<div class="shelf">
<div class="icon sel">
<svg width="40" height="40" viewBox="0 0 20 20" shape-rendering="crispEdges">
<path fill="#000" d="M6 5h8v1h1v1h1v1h1v6h-1v1H3v-1H2V9h1V8h1V7h1V6h1z"/>
<path fill="#fff" d="M6 6h8v1h1v1h1v5H3V9h1V8h1V7h1z"/>
</svg>
<span class="label">Aether</span>
</div>
<div class="icon">
<svg width="40" height="40" viewBox="0 0 20 20" shape-rendering="crispEdges">
<path fill="#000" d="M4 2h12v16H4z"/>
<path fill="#fff" d="M5 3h10v14H5z"/>
<path fill="#000" d="M7 5h6v1H7zm0 3h6v1H7zm0 3h6v1H7zm0 3h4v1H7z"/>
</svg>
<span class="label">InkNote</span>
</div>
<div class="icon">
<svg width="40" height="40" viewBox="0 0 20 20" shape-rendering="crispEdges">
<path fill="#000" d="M9 4h2v5h5v2h-5v5H9v-5H4V9h5z"/>
</svg>
<span class="label">hinzufügen</span>
</div>
</div>
<div class="shelf-foot"><span>3 Objekte</span><span>alles bereit</span></div>
</div>
<!-- Leiste -->
<div class="win" id="bar">
<div class="titlebar"><span class="box"></span><span class="name">so sähe die Leiste in InkNote aus</span></div>
<div class="bardemo">
<b>&#9670; HoneyOS</b>
<span>Aether</span>
<span class="cur">InkNote</span>
<span class="right">marc &nbsp; 02:47</span>
</div>
<div class="barhost">… hier beginnt die App selbst, mit ihrem eigenen Aussehen.</div>
</div>
<div class="trash">
<svg width="40" height="40" viewBox="0 0 20 20" shape-rendering="crispEdges">
<path fill="#000" d="M7 3h6v1h4v1H3V4h4zm-2 3h10v12H5z"/>
<path fill="#d0d0d0" d="M6 7h8v10H6z"/>
<path fill="#000" d="M8 9h1v6H8zm3 0h1v6h-1z"/>
</svg>
<span class="label">Papierkorb</span>
</div>
<div class="switch">Look: <b>System 7</b> &nbsp;·&nbsp; <a href="../index.html">Amber-CRT</a> &nbsp;·&nbsp; <a href="./win95.html">Windows 95</a></div>
</body>
</html>
+225
View File
@@ -0,0 +1,225 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>HoneyOS — Look: Windows 95</title>
<style>
/* ── Windows 95 ──────────────────────────────────────────────────────
Alles lebt von zwei Kanten: hell oben/links, dunkel unten/rechts.
Das ist der gesamte 3D-Effekt, mehr steckt nicht dahinter. */
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
font: 11px/1.4 "MS Sans Serif", Tahoma, Verdana, "DejaVu Sans", sans-serif;
color: #000;
background: #008080;
overflow: hidden;
-webkit-font-smoothing: none;
}
.raised {
border: 2px outset;
border-color: #fff #404040 #404040 #fff;
background: #c0c0c0;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
}
.sunken {
border: 2px solid;
border-color: #808080 #fff #fff #808080;
background: #c0c0c0;
box-shadow: inset 1px 1px 0 #404040, inset -1px -1px 0 #dfdfdf;
}
/* Schreibtisch-Symbole */
.desk { position: absolute; top: 12px; left: 12px; width: 84px; }
.deskicon {
width: 84px; padding: 6px 2px; text-align: center; color: #fff; cursor: default;
margin-bottom: 6px;
}
.deskicon svg { display: block; margin: 0 auto 4px; image-rendering: pixelated; }
.deskicon .label { display: inline-block; padding: 1px 3px; text-shadow: 1px 1px 0 rgba(0,0,0,.6); }
.deskicon.sel .label { background: #000080; text-shadow: none; outline: 1px dotted #fff; }
.deskicon.sel svg { filter: hue-rotate(0deg) brightness(.85); }
/* Fenster */
.win { position: absolute; padding: 3px; }
.titlebar {
display: flex; align-items: center; gap: 4px;
height: 18px; padding: 0 2px 0 3px;
background: linear-gradient(90deg, #000080, #1084d0);
color: #fff; font-weight: 700;
}
.titlebar.inactive { background: linear-gradient(90deg, #808080, #b5b5b5); }
.titlebar .t { flex: 1; }
.tbtn {
width: 16px; height: 14px; font: 700 10px/10px "MS Sans Serif", Tahoma, sans-serif;
display: grid; place-items: center; padding-bottom: 2px;
}
.menustrip { display: flex; gap: 12px; padding: 2px 5px; }
.menustrip span { cursor: default; }
.menustrip span:hover { background: #000080; color: #fff; }
.win-body { padding: 10px; }
#status { left: 120px; top: 30px; width: 292px; }
#status h1 { font-size: 11px; margin: 0 0 8px; font-weight: 700; }
.rows { display: grid; grid-template-columns: 1fr auto; gap: 3px 10px; margin-bottom: 10px; }
.rows span:nth-child(even) { font-weight: 700; }
.field { padding: 6px 8px; color: #000; }
.note { color: #404040; }
.buttons { display: flex; gap: 6px; justify-content: flex-end; margin-top: 12px; }
.btn { padding: 3px 14px; min-width: 72px; text-align: center; cursor: default; }
.btn.default { outline: 1px solid #000; outline-offset: 1px; }
#progs { left: 436px; top: 30px; width: 300px; }
.listview { padding: 4px; height: 148px; background: #fff; }
.row { display: flex; align-items: center; gap: 8px; padding: 3px 4px; }
.row.sel { background: #000080; color: #fff; }
.row .sub { margin-left: auto; color: #606060; }
.row.sel .sub { color: #c0c0c0; }
.statusfields { display: flex; gap: 3px; margin-top: 4px; }
.statusfields div { padding: 2px 6px; }
.statusfields div:first-child { flex: 1; }
/* Leiste */
#bar { left: 120px; top: 236px; width: 616px; }
.bardemo {
display: flex; align-items: center; gap: 4px; padding: 2px 3px;
}
.bardemo .pill { padding: 2px 10px; cursor: default; }
.bardemo .right { margin-left: auto; display: flex; gap: 4px; align-items: center; }
.barhost { margin-top: 3px; padding: 14px 10px; background: #fff; color: #404040; }
/* Taskleiste */
.taskbar {
position: absolute; left: 0; right: 0; bottom: 0; height: 28px;
display: flex; align-items: center; gap: 4px; padding: 2px 3px;
}
.start { display: flex; align-items: center; gap: 4px; padding: 2px 8px 2px 4px; font-weight: 700; cursor: default; }
.divider { width: 2px; height: 20px; border-left: 1px solid #808080; border-right: 1px solid #fff; margin: 0 2px; }
.task { padding: 3px 10px; width: 154px; text-align: left; cursor: default; }
.task.pressed {
border-color: #808080 #fff #fff #808080;
box-shadow: inset 1px 1px 0 #404040;
background: repeating-conic-gradient(#c0c0c0 0 25%, #dfdfdf 0 50%) 0 0/2px 2px;
font-weight: 700;
}
.tray { margin-left: auto; display: flex; align-items: center; gap: 6px; padding: 2px 8px; }
.switch { position: absolute; right: 12px; top: 12px; padding: 4px 8px; }
.switch a { color: #000080; }
</style>
</head>
<body>
<!-- Schreibtisch -->
<div class="desk">
<div class="deskicon sel">
<svg width="32" height="32" viewBox="0 0 16 16" shape-rendering="crispEdges">
<path fill="#000" d="M5 4h6v1h1v1h1v1h1v5H2V8h1V7h1V6h1z"/>
<path fill="#dfe8f5" d="M5 5h6v1h1v1h1v4H3V8h1V7h1z"/>
<path fill="#7fa8d8" d="M4 9h9v2H4z"/>
</svg>
<span class="label">Aether</span>
</div>
<div class="deskicon">
<svg width="32" height="32" viewBox="0 0 16 16" shape-rendering="crispEdges">
<path fill="#000" d="M3 1h10v14H3z"/>
<path fill="#fffbe6" d="M4 2h8v12H4z"/>
<path fill="#c07000" d="M5 4h6v1H5zm0 3h6v1H5zm0 3h4v1H5z"/>
<path fill="#e0b040" d="M10 9h3v1h-3z"/>
</svg>
<span class="label">InkNote</span>
</div>
</div>
<!-- Statusfenster -->
<div class="win raised" id="status">
<div class="titlebar">
<span class="t">HoneyOS</span>
<span class="tbtn raised">_</span><span class="tbtn raised">&#9633;</span><span class="tbtn raised">&times;</span>
</div>
<div class="menustrip"><span><u>D</u>atei</span><span><u>A</u>nsicht</span><span><u>?</u></span></div>
<div class="win-body">
<h1>Willkommen zurück, marc.</h1>
<div class="rows">
<span>Programme</span><span>2</span>
<span>erreichbar</span><span>2 von 2</span>
<span>Version</span><span>0.1</span>
</div>
<div class="field sunken note">Jedes Programm läuft auf seiner eigenen Domain.<br />HoneyOS öffnet sie nur.</div>
<div class="buttons">
<span class="btn raised default">OK</span>
<span class="btn raised">Einstellungen</span>
</div>
</div>
</div>
<!-- Programme -->
<div class="win raised" id="progs">
<div class="titlebar inactive">
<span class="t">Programme</span>
<span class="tbtn raised">_</span><span class="tbtn raised">&#9633;</span><span class="tbtn raised">&times;</span>
</div>
<div class="win-body" style="padding: 4px">
<div class="listview sunken">
<div class="row sel">
<svg width="16" height="16" viewBox="0 0 16 16" shape-rendering="crispEdges">
<path fill="#000" d="M5 4h6v1h1v1h1v1h1v5H2V8h1V7h1V6h1z"/><path fill="#dfe8f5" d="M5 5h6v1h1v1h1v4H3V8h1V7h1z"/>
</svg>
Aether <span class="sub">Cloud &amp; Dateien</span>
</div>
<div class="row">
<svg width="16" height="16" viewBox="0 0 16 16" shape-rendering="crispEdges">
<path fill="#000" d="M3 1h10v14H3z"/><path fill="#fffbe6" d="M4 2h8v12H4z"/><path fill="#c07000" d="M5 4h6v1H5zm0 3h6v1H5z"/>
</svg>
InkNote <span class="sub">Notizen</span>
</div>
<div class="row">
<svg width="16" height="16" viewBox="0 0 16 16" shape-rendering="crispEdges">
<path fill="#404040" d="M7 3h2v4h4v2H9v4H7V9H3V7h4z"/>
</svg>
Programm hinzufügen…
</div>
</div>
<div class="statusfields">
<div class="sunken">3 Objekte</div>
<div class="sunken">alles bereit</div>
</div>
</div>
</div>
<!-- Leiste -->
<div class="win raised" id="bar">
<div class="titlebar inactive"><span class="t">so sähe die Leiste in InkNote aus</span><span class="tbtn raised">&times;</span></div>
<div class="win-body" style="padding: 4px">
<div class="bardemo sunken" style="background:#c0c0c0">
<span class="pill raised"><b>HoneyOS</b></span>
<span class="divider"></span>
<span class="pill raised">Aether</span>
<span class="pill raised task pressed" style="width:auto">InkNote</span>
<span class="right"><span class="pill sunken">marc</span><span class="pill sunken">02:47</span></span>
</div>
<div class="barhost sunken">… hier beginnt die App selbst, mit ihrem eigenen Aussehen.</div>
</div>
</div>
<div class="switch raised">
Look: <b>Windows 95</b> &nbsp;·&nbsp; <a href="../index.html">Amber-CRT</a> &nbsp;·&nbsp; <a href="./system7.html">System 7</a>
</div>
<!-- Taskleiste -->
<div class="taskbar raised">
<span class="start raised">
<svg width="16" height="14" viewBox="0 0 8 7" shape-rendering="crispEdges">
<path fill="#ff2f2f" d="M0 1h3v2H0z"/><path fill="#2fbf2f" d="M4 0h4v3H4z"/>
<path fill="#2f6fff" d="M0 4h3v2H0z"/><path fill="#ffd02f" d="M4 4h4v3H4z"/>
</svg>
Start
</span>
<span class="divider"></span>
<span class="task raised pressed">HoneyOS</span>
<span class="task raised">Programme</span>
<span class="tray sunken">02:47</span>
</div>
</body>
</html>
+1024
View File
File diff suppressed because it is too large. Load diff
+764
View File
@@ -0,0 +1,764 @@
/*
* HoneyOS — Amber-CRT-Desktop.
*
* Farben laufen ausschließlich über die Phosphor-Variablen unten. Wer eine
* neue Röhre will, ergänzt einen [data-phosphor]-Block und fasst sonst nichts an.
*/
/* ── Phosphor ──────────────────────────────────────────────────────────── */
:root,
[data-phosphor="amber"] {
--p: #ffb641; /* Vollton — Text, Rahmen im Fokus */
--p-mid: #c8842a; /* halb — Sekundärtext */
--p-dim: #6b4718; /* schwach — Rahmen, Trennlinien */
--p-faint: #2a1c0c; /* Andeutung — Füllungen */
--bg: #0a0705;
--bg-raised: #140d07;
--glow: 0 0 6px rgba(255, 182, 65, 0.45);
}
[data-phosphor="green"] {
--p: #5cff8f;
--p-mid: #2fa85c;
--p-dim: #17512c;
--p-faint: #08200f;
--bg: #040803;
--bg-raised: #071206;
--glow: 0 0 6px rgba(92, 255, 143, 0.4);
}
[data-phosphor="ice"] {
--p: #cfe8ff;
--p-mid: #7fa6c4;
--p-dim: #395366;
--p-faint: #12202a;
--bg: #04070a;
--bg-raised: #0a1118;
--glow: 0 0 6px rgba(207, 232, 255, 0.35);
}
* {
box-sizing: border-box;
}
/* Boot- und Desktop-Ebene tragen `display:flex` — das schlägt das
hidden-Attribut des Browsers, beide lägen sonst gleichzeitig da. */
[hidden] {
display: none !important;
}
html,
body {
height: 100%;
margin: 0;
}
body {
background: var(--bg);
color: var(--p);
font-family: ui-monospace, "SF Mono", "Cascadia Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
/* ── Bildschirm + Effekte ──────────────────────────────────────────────── */
.crt {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
overflow: hidden;
/* Sanftes Glimmen aus der Mitte, wie eine aufgewärmte Röhre */
background:
radial-gradient(120% 90% at 50% 45%, color-mix(in srgb, var(--p) 7%, transparent), transparent 70%),
var(--bg);
text-shadow: var(--glow);
}
.scanlines,
.vignette,
.flicker {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 90;
}
.scanlines {
background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0.28) 0 1px, transparent 1px 3px);
opacity: 0.7;
}
.vignette {
background: radial-gradient(115% 105% at 50% 50%, transparent 55%, rgba(0, 0, 0, 0.75) 100%);
}
.flicker {
background: var(--p);
opacity: 0;
mix-blend-mode: overlay;
animation: flicker 7s infinite steps(1);
}
@keyframes flicker {
0%, 96%, 100% { opacity: 0; }
97% { opacity: 0.035; }
98% { opacity: 0; }
99% { opacity: 0.02; }
}
/* Wer die Effekte abschaltet, schaltet sie wirklich ab. */
[data-scanlines="off"] .scanlines,
[data-flicker="off"] .flicker {
display: none;
}
[data-glow="off"] .crt {
text-shadow: none;
}
@media (prefers-reduced-motion: reduce) {
.flicker { animation: none; }
}
/* ── Boot ──────────────────────────────────────────────────────────────── */
.boot {
flex: 1;
padding: clamp(16px, 5vw, 56px);
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow: hidden;
}
.boot-log {
margin: 0;
font: inherit;
white-space: pre-wrap;
color: var(--p);
}
.boot-log .ok { color: var(--p); }
.boot-log .warn { color: var(--p-mid); }
.boot-log b { font-weight: 700; }
.boot-hint {
margin-top: auto;
color: var(--p-dim);
font-size: 12px;
}
.caret::after {
content: "\2588";
animation: blink 1s steps(1) infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
/* ── Desktop ───────────────────────────────────────────────────────────── */
.desktop {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
animation: warmup 0.5s ease-out;
}
@keyframes warmup {
from { opacity: 0; transform: scaleY(0.9); filter: brightness(2.5); }
to { opacity: 1; transform: none; filter: none; }
}
/* Menüleiste — Zwilling von honeybar.js */
.menubar {
display: flex;
align-items: center;
gap: 10px;
padding: 0 10px;
height: 34px;
flex: 0 0 34px;
border-bottom: 1px solid var(--p-dim);
background: var(--bg-raised);
padding-top: env(safe-area-inset-top);
height: calc(34px + env(safe-area-inset-top));
flex-basis: calc(34px + env(safe-area-inset-top));
}
.mb-logo {
background: none;
border: 0;
color: var(--p);
font: inherit;
letter-spacing: 0.14em;
cursor: pointer;
padding: 2px 4px;
white-space: nowrap;
}
.mb-logo b { font-weight: 400; color: var(--p-mid); }
.mb-logo .hex { font-size: 10px; vertical-align: 2px; }
.mb-apps {
display: flex;
gap: 4px;
overflow-x: auto;
scrollbar-width: none;
flex: 1;
}
.mb-apps::-webkit-scrollbar { display: none; }
.mb-app {
background: none;
border: 1px solid transparent;
color: var(--p-mid);
font: inherit;
font-size: 12px;
letter-spacing: 0.08em;
padding: 2px 9px;
cursor: pointer;
white-space: nowrap;
}
.mb-app:hover { color: var(--p); border-color: var(--p-dim); }
.mb-app[aria-current="true"] {
color: var(--bg);
background: var(--p);
border-color: var(--p);
text-shadow: none;
}
.mb-right {
display: flex;
align-items: center;
gap: 10px;
color: var(--p-mid);
font-size: 12px;
}
.mb-user { opacity: 0.9; }
.mb-clock { font-variant-numeric: tabular-nums; color: var(--p); }
.mb-btn {
background: none;
border: 1px solid var(--p-dim);
color: var(--p);
font: inherit;
line-height: 1;
padding: 3px 7px;
cursor: pointer;
}
.mb-btn:hover { background: var(--p-faint); }
/* Bühne */
.stage {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: clamp(14px, 3vw, 32px);
display: grid;
gap: clamp(14px, 3vw, 28px);
grid-template-columns: minmax(0, 1fr);
align-content: start;
max-width: 1100px;
width: 100%;
margin: 0 auto;
}
@media (min-width: 860px) {
.stage {
grid-template-columns: 320px minmax(0, 1fr);
align-items: start;
}
}
/* Fenster */
.win {
border: 1px solid var(--p-dim);
background: var(--bg-raised);
}
.win-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 3px 8px;
border-bottom: 1px solid var(--p-dim);
background: var(--p-faint);
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--p-mid);
}
.win-dots { letter-spacing: 2px; opacity: 0.5; }
.win-close {
background: none;
border: 0;
color: var(--p);
font: inherit;
font-size: 16px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
}
.win-body { padding: 14px; }
.greet {
margin: 0 0 12px;
font-size: 15px;
}
.statlist {
margin: 0;
display: grid;
grid-template-columns: auto 1fr;
gap: 3px 12px;
font-size: 12.5px;
}
.statlist dt { color: var(--p-dim); }
.statlist dd { margin: 0; color: var(--p-mid); text-align: right; font-variant-numeric: tabular-nums; }
.statlist dd.up { color: var(--p); }
.statlist dd.down { color: var(--p-mid); opacity: 0.6; }
/* App-Kacheln */
.tiles {
display: grid;
gap: clamp(12px, 2.5vw, 20px);
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
align-content: start;
}
.tile {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px;
min-height: 168px;
border: 1px solid var(--p-dim);
background: var(--bg-raised);
color: var(--p);
font: inherit;
text-align: left;
text-decoration: none;
cursor: pointer;
position: relative;
transition: background 0.12s, border-color 0.12s, transform 0.12s;
}
.tile:hover,
.tile:focus-visible {
background: var(--p-faint);
border-color: var(--p);
transform: translateY(-2px);
outline: none;
}
.tile:active { transform: none; }
.tile-key {
position: absolute;
top: 8px;
right: 10px;
font-size: 11px;
color: var(--p-dim);
}
.tile-icon {
width: 46px;
height: 46px;
color: var(--p);
}
.tile-icon svg { display: block; width: 100%; height: 100%; }
.tile-name {
font-size: 16px;
letter-spacing: 0.16em;
}
.tile-sub {
font-size: 12px;
color: var(--p-mid);
margin-top: -6px;
}
.tile-state {
margin-top: auto;
font-size: 11px;
letter-spacing: 0.1em;
color: var(--p-dim);
display: flex;
align-items: center;
gap: 6px;
}
.tile-state .led {
width: 7px;
height: 7px;
background: var(--p-dim);
border-radius: 50%;
}
.tile-state[data-state="up"] { color: var(--p); }
.tile-state[data-state="up"] .led { background: var(--p); box-shadow: var(--glow); }
.tile-state[data-state="down"] { color: var(--p-mid); }
.tile-state[data-state="down"] .led { background: transparent; border: 1px solid var(--p-mid); }
/* leerer Zustand */
.tile-add {
align-items: center;
justify-content: center;
color: var(--p-dim);
border-style: dashed;
min-height: 168px;
}
/* Statuszeile */
.statusbar {
flex: 0 0 auto;
display: flex;
justify-content: space-between;
gap: 12px;
padding: 4px 10px;
padding-bottom: calc(4px + env(safe-area-inset-bottom));
border-top: 1px solid var(--p-dim);
background: var(--bg-raised);
color: var(--p-dim);
font-size: 11px;
letter-spacing: 0.06em;
}
.sb-right { color: var(--p-mid); white-space: nowrap; }
@media (max-width: 620px) {
.statusbar { display: none; }
}
/* ── Modal ─────────────────────────────────────────────────────────────── */
.modal {
position: absolute;
inset: 0;
z-index: 80;
display: grid;
place-items: center;
padding: 16px;
background: rgba(0, 0, 0, 0.65);
}
.modal[hidden] { display: none; }
.win-modal {
width: min(520px, 100%);
max-height: min(78vh, 640px);
display: flex;
flex-direction: column;
box-shadow: 0 0 0 1px var(--p-faint), 0 18px 60px rgba(0, 0, 0, 0.7);
}
.win-modal .win-body { overflow-y: auto; }
.field {
display: block;
margin-bottom: 16px;
}
.field > .label {
display: block;
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--p-dim);
margin-bottom: 6px;
}
.seg { display: flex; flex-wrap: wrap; gap: 6px; }
.seg button {
background: none;
border: 1px solid var(--p-dim);
color: var(--p-mid);
font: inherit;
font-size: 12px;
padding: 4px 12px;
cursor: pointer;
}
.seg button:hover { border-color: var(--p); color: var(--p); }
.seg button[aria-pressed="true"] {
background: var(--p);
border-color: var(--p);
color: var(--bg);
text-shadow: none;
}
.field input[type="text"],
.field input[type="url"] {
width: 100%;
background: var(--bg);
border: 1px solid var(--p-dim);
color: var(--p);
font: inherit;
font-size: 13px;
padding: 6px 8px;
}
.field input:focus {
outline: none;
border-color: var(--p);
}
.url-row {
display: grid;
grid-template-columns: 88px 1fr;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.url-row span { font-size: 12px; color: var(--p-mid); letter-spacing: 0.1em; }
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 18px;
padding-top: 12px;
border-top: 1px solid var(--p-faint);
}
.btn {
background: none;
border: 1px solid var(--p-dim);
color: var(--p);
font: inherit;
font-size: 12px;
letter-spacing: 0.1em;
padding: 5px 14px;
cursor: pointer;
}
.btn:hover { background: var(--p-faint); border-color: var(--p); }
.btn-primary { background: var(--p); border-color: var(--p); color: var(--bg); text-shadow: none; }
.btn-primary:hover { background: var(--p); filter: brightness(1.1); }
.about p { margin: 0 0 10px; color: var(--p-mid); font-size: 13px; }
.about code {
color: var(--p);
background: var(--p-faint);
padding: 1px 4px;
}
.keys {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 14px;
font-size: 12.5px;
margin-top: 12px;
}
.keys dt { color: var(--p); }
.keys dd { margin: 0; color: var(--p-mid); }
/* ── Desktop-Modus ─────────────────────────────────────────────────────────
* Dieselbe Röhre, andere Betriebsart: Symbole auf der Fläche, echte Fenster,
* Taskleiste unten. Die Menüleiste oben bleibt in beiden Modi dieselbe — sie
* ist die Klammer, die HoneyOS mit den Apps teilt.
*/
.deskspace {
flex: 1;
min-height: 0;
position: relative;
overflow: hidden;
}
.desk-icons {
position: absolute;
inset: 0;
padding: 12px 8px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-content: flex-start;
gap: 2px;
}
.deskicon {
width: 94px;
padding: 8px 4px 6px;
text-align: center;
background: none;
border: 1px solid transparent;
color: var(--p);
font: inherit;
cursor: pointer;
}
.deskicon:hover { border-color: var(--p-dim); }
.deskicon:focus-visible { outline: 1px dotted var(--p); outline-offset: -3px; }
.di-glyph {
/* inline-Elemente ignorieren Breite und Höhe — ohne block wächst das SVG
auf seine Eigengröße und sprengt das Symbol. */
display: block;
width: 34px;
height: 34px;
margin: 0 auto 6px;
color: var(--p);
}
.di-glyph svg { display: block; width: 100%; height: 100%; }
.di-label {
display: inline-block;
font-size: 11.5px;
padding: 1px 4px;
letter-spacing: 0.06em;
}
.deskicon[data-sel="1"] { background: var(--p-faint); border-color: var(--p-dim); }
.deskicon[data-sel="1"] .di-label { background: var(--p); color: var(--bg); text-shadow: none; }
/* Erreichbarkeitslampe am Symbol */
.deskicon[data-app] .di-label::after { content: ""; }
.deskicon[data-state="down"] .di-glyph { opacity: 0.45; }
/* Fenster im Desktop-Modus */
.windows {
position: absolute;
inset: 0;
pointer-events: none;
}
.dwin {
position: absolute;
pointer-events: auto;
border: 1px solid var(--p-dim);
background: var(--bg-raised);
box-shadow: 0 12px 34px rgba(0, 0, 0, 0.55);
display: flex;
flex-direction: column;
max-height: calc(100% - 16px);
}
.dwin[data-focus="1"] { border-color: var(--p); }
.dwin[data-min="1"] { display: none; }
.dwin .win-bar {
cursor: grab;
touch-action: none;
user-select: none;
}
.dwin[data-drag="1"] .win-bar { cursor: grabbing; }
.dwin[data-focus="0"] .win-bar { color: var(--p-dim); }
.dwin .win-body {
overflow: auto;
min-height: 0;
}
.win-btns { display: flex; gap: 2px; }
.win-btn {
background: none;
border: 1px solid var(--p-dim);
color: var(--p);
font: inherit;
font-size: 10px;
line-height: 1;
padding: 1px 5px;
cursor: pointer;
}
.win-btn:hover { background: var(--p-faint); border-color: var(--p); }
/* Programme-Fenster */
.applist { display: flex; flex-direction: column; gap: 2px; }
.applist button {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
background: none;
border: 1px solid transparent;
color: var(--p);
font: inherit;
font-size: 13px;
text-align: left;
padding: 5px 6px;
cursor: pointer;
}
.applist button:hover { background: var(--p-faint); border-color: var(--p-dim); }
.applist .al-glyph { width: 18px; height: 18px; flex: none; }
.applist .al-glyph svg { display: block; width: 100%; height: 100%; }
.applist .al-sub { margin-left: auto; color: var(--p-dim); font-size: 11px; }
/* Taskleiste */
.taskbar {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 6px;
padding: 3px 6px;
padding-bottom: calc(3px + env(safe-area-inset-bottom));
border-top: 1px solid var(--p-dim);
background: var(--bg-raised);
}
.task-start {
background: var(--p-faint);
border: 1px solid var(--p-dim);
color: var(--p);
font: inherit;
font-size: 11px;
letter-spacing: 0.16em;
padding: 3px 10px;
cursor: pointer;
flex: none;
}
.task-start:hover, .task-start[aria-expanded="true"] { background: var(--p); color: var(--bg); text-shadow: none; }
.task-list {
display: flex;
gap: 4px;
flex: 1;
overflow-x: auto;
scrollbar-width: none;
}
.task-list::-webkit-scrollbar { display: none; }
.task-btn {
background: none;
border: 1px solid var(--p-dim);
color: var(--p-mid);
font: inherit;
font-size: 11px;
padding: 3px 10px;
min-width: 96px;
max-width: 160px;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.task-btn:hover { color: var(--p); }
.task-btn[data-active="1"] { background: var(--p-faint); color: var(--p); border-color: var(--p); }
.task-date { color: var(--p-dim); font-size: 11px; white-space: nowrap; }
/* Startmenü */
.startmenu {
position: absolute;
left: 6px;
bottom: 4px;
min-width: 208px;
z-index: 70;
border: 1px solid var(--p);
background: var(--bg-raised);
padding: 4px;
display: none;
flex-direction: column;
gap: 1px;
box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.6);
}
.startmenu[data-open="1"] { display: flex; }
.startmenu .sm-head {
padding: 4px 8px;
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--p-dim);
}
.startmenu button {
background: none;
border: 0;
color: var(--p-mid);
font: inherit;
font-size: 12.5px;
text-align: left;
padding: 6px 9px;
cursor: pointer;
}
.startmenu button:hover { background: var(--p); color: var(--bg); text-shadow: none; }
.startmenu hr { border: 0; border-top: 1px solid var(--p-faint); margin: 3px 0; }
@media (max-width: 620px) {
.task-btn { min-width: 0; }
.task-date { display: none; }
}