Files
HoneyOS/web/style.css
T
marcandClaude Opus 5 283f9a1850 feat: Backend mit OIDC-Anmeldung, Apps und Einstellungen im Server
Aus der statischen Seite wird ein Go-Binary mit eingebetteter Oberfläche.
Grund: Apps über .env zu pflegen wurde unübersichtlich, und kosmetische
Einstellungen sollen auf jedem Gerät gleich sein.

- Anmeldung über den eigenen OIDC-Anbieter (PocketID), mit PKCE. Ohne
  Anmeldung gibt es nichts zu sehen – auch keine App-Liste.
- Sitzung als signiertes Cookie ohne Gegenstück im Server; Schreibzugriffe
  zusätzlich über ein lesbares CSRF-Cookie abgesichert
- Apps werden in der Oberfläche angelegt und bearbeitet, nicht mehr in der
  .env. Der Server prüft jeden Eintrag und meldet zurück, was er behält.
- Einstellungen liegen je Nutzer im Server; localStorage ist nur noch der
  Vorschuss, damit die Röhre beim Laden nicht in der falschen Farbe blitzt
- die Leiste holt die App-Liste aus /api/apps – freigegeben sind allein die
  Ursprünge, die selbst als App eingetragen sind
- Tests fahren einen echten OIDC-Anbieter hoch und gehen den Anmeldeweg
  einmal ganz durch

Ohne HONEYOS_OIDC_ISSUER läuft alles offen – für die Entwicklung, mit
deutlicher Warnung beim Start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 15:20:55 +02:00

888 lines
21 KiB
CSS

/*
* 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; }
}
/* ── Anmeldung ─────────────────────────────────────────────────────────────
* Derselbe Bildschirm wie der Boot, nur endet er nicht von selbst. Wer nicht
* angemeldet ist, sieht vom Schreibtisch nichts — auch nicht die App-Liste.
*/
.login {
flex: 1;
display: flex;
flex-direction: column;
padding: clamp(16px, 5vw, 56px);
overflow: hidden;
}
.login-box {
margin-top: 26px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.login-btn {
background: none;
border: 1px solid var(--p);
color: var(--p);
font: inherit;
font-size: 15px;
letter-spacing: 0.14em;
padding: 10px 22px;
cursor: pointer;
animation: pulse 2.4s ease-in-out infinite;
}
.login-btn:hover, .login-btn:focus-visible {
background: var(--p);
color: var(--bg);
text-shadow: none;
outline: none;
animation: none;
}
@keyframes pulse {
50% { box-shadow: 0 0 14px color-mix(in srgb, var(--p) 45%, transparent); }
}
.login-note {
margin: 0;
color: var(--p-dim);
font-size: 12px;
max-width: 52ch;
line-height: 1.6;
}
.login-note b { color: var(--p-mid); font-weight: 400; }
/* Nutzer in der Leiste */
.mb-avatar {
width: 18px;
height: 18px;
object-fit: cover;
border: 1px solid var(--p-dim);
}
.mb-avatar[hidden] { display: none; }
/* ── App-Verwaltung ────────────────────────────────────────────────────── */
.apped { display: flex; flex-direction: column; gap: 10px; }
.approw {
border: 1px solid var(--p-dim);
background: var(--bg);
}
.approw > summary {
display: flex;
align-items: center;
gap: 9px;
padding: 7px 9px;
cursor: pointer;
list-style: none;
font-size: 13px;
}
.approw > summary::-webkit-details-marker { display: none; }
.approw[open] > summary { border-bottom: 1px solid var(--p-dim); background: var(--p-faint); }
.approw .ae-glyph { width: 18px; height: 18px; flex: none; }
.approw .ae-glyph svg { display: block; width: 100%; height: 100%; }
.approw .ae-url { margin-left: auto; color: var(--p-dim); font-size: 11px; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.approw .ae-body { padding: 10px; display: flex; flex-direction: column; gap: 8px; }
.ae-field { display: grid; grid-template-columns: 92px 1fr; gap: 8px; align-items: center; }
.ae-field > span { font-size: 11px; color: var(--p-dim); letter-spacing: 0.1em; }
.ae-field input, .ae-field select {
background: var(--bg-raised);
border: 1px solid var(--p-dim);
color: var(--p);
font: inherit;
font-size: 12.5px;
padding: 5px 7px;
min-width: 0;
}
.ae-field input:focus, .ae-field select:focus { outline: none; border-color: var(--p); }
/* Hinweise stehen unter ihrem Feld, bündig mit der Eingabe — nicht daneben. */
.ae-hint { margin: -4px 0 0 100px; color: var(--p-dim); font-size: 11px; }
@media (max-width: 520px) {
.ae-field { grid-template-columns: 1fr; gap: 3px; }
.ae-hint { margin-left: 0; }
}
.ae-actions { display: flex; gap: 6px; justify-content: flex-end; padding-top: 2px; }
.btn-danger { border-color: var(--p-dim); color: var(--p-mid); }
.btn-danger:hover { border-color: #d0563a; color: #ff7a5c; background: none; }
.apped-empty {
border: 1px dashed var(--p-dim);
padding: 18px;
text-align: center;
color: var(--p-dim);
font-size: 12.5px;
}
.saveline {
min-height: 16px;
font-size: 11.5px;
color: var(--p-mid);
}
.saveline[data-kind="err"] { color: #ff7a5c; }