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:
commit
84cc5d619e
17 files changed
+3126
No files matched your search
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
*.tmp
|
||||
shots/
|
||||
+20
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
@@ -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
|
||||
@@ -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:-}
|
||||
Executable
+79
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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">■ 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">▾</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();
|
||||
})();
|
||||
@@ -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">■</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)">⚙</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">●●●</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">■ 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 · [M] modus · [S] einstellungen · [?] 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">×</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>
|
||||
@@ -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">◆</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 ▾</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>◆ HoneyOS</b>
|
||||
<span>Aether</span>
|
||||
<span class="cur">InkNote</span>
|
||||
<span class="right">marc 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> · <a href="../index.html">Amber-CRT</a> · <a href="./win95.html">Windows 95</a></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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">□</span><span class="tbtn raised">×</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">□</span><span class="tbtn raised">×</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 & 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">×</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> · <a href="../index.html">Amber-CRT</a> · <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>
|
||||
+764
@@ -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; }
|
||||
}
|
||||
Reference in new issue
Block a user