Hinweis: Der Beitrag wurde mit KI-Unterstützung (Claude Code) verfasst und manuell nachbearbeitet.
HA Widgets — alle Teile: Teil 1: Überblick · Teil 2: Wie fängt man an? · Teil 3: Architektur · Teil 4: Architektur-Checks · Teil 5: HA-Infrastruktur · Teil 6: LCARS-Design · Teil 7: Phone und Desktop · Teil 8: Claude Code richtig briefen
Teil 5 hat beschrieben wie Daten zwischen Apps und HA-Server fließen. Jetzt kommt das was der Nutzer sieht: die Tablet-App. Sie hängt an der Wand, läuft durch, und soll dabei eine bestimmte Ästhetik transportieren — LCARS, das Interface aus Star Trek.
LCARS: Eine Entscheidung, keine Spielerei (naja😅)
Das Tablet läuft als Always-on-Display. Es sollte nicht wie ein generisches Material-Design-Dashboard aussehen — es sollte etwas sein. LCARS (Library Computer Access/Retrieval System) aus Star Trek hat genau diese Eigenschaft: unverwechselbar, technisch, ohne aufdringlich zu wirken. Farbige Balken, geschwungene Bögen, Großbuchstaben, viel Schwarz.
Das Schöne daran: LCARS ist im Kern simples CSS. Keine Canvas-Tricks, kein SVG-Monstrum. Bögen über border-radius, Balken über flex, der charakteristische "Knick" aus zwei Elementen. Dazu eine Schrift die passt.

Das Farbsystem
Fünf Tabs, fünf Farben — jeweils in aktiv und gedimmt:
--lc-item1: #6688BB; /* BRÜCKE */
--lc-item1-dim: #3E5E88;
--lc-item2: #5577AA; /* TECHNIK */
--lc-item2-dim: #28405E;
--lc-item3: #8899CC; /* VERSORGUNG */
--lc-item3-dim: #324E72;
--lc-item4: #4466AA; /* KANTINE */
--lc-item4-dim: #223858;
--lc-item5: #4499BB; /* REPLIKATOR */
--lc-item5-dim: #244460;
Der aktive Tab bekommt seine Vollfarbe, alle anderen die gedimmte Variante. Die Farbe pflanzt sich durch: der Elbow oben links in der Sidebar nimmt die Farbe des aktiven Tabs, der Header rechts daneben ebenso. Wechselt der Tab, wechselt die Akzentfarbe der gesamten Shell.
Dazu kommen Funktionsfarben: --lc-alert (#ffaa22) für Timer-Alarme und Warnungen, --lc-success (#33BB88) für Bestätigungen, --lc-critical (#FF3300) für kritische Zustände. Orange statt Rot für Warnungen — Rot ist dem echten Fehlerfall vorbehalten.
Typografie
Eine Schrift: Antonio (Google Fonts). Serifenlos, kondensiert, alle Varianten in Großbuchstaben. Das ist (zumindest für mich) LCARS: kein Fließtext, keine Mischung, nur Uppercase mit letter-spacing.
@import url('https://fonts.googleapis.com/css2?family=Antonio:wght@400;700&display=swap');
--lc-font: 'Antonio', 'Helvetica Neue', Arial, sans-serif;
Der Import und alle CSS-Variablen stehen in lcars.css, die in App.vue eingebunden wird und damit global für alle Views gilt. Kein Scoped-Scope, kein CSS-in-JS — eine zentrale Datei für das gesamte Design-System.
Der Elbow: der LCARS-Knick
Das prägnanteste Element: der geschwungene Knick oben links der Sidebar, der Sidebar und Content-Bereich verbindet. In CSS ist das ein Element mit großem border-radius oben links:
.elbow-top {
height: calc(var(--header-h) + var(--notch-r));
border-radius: var(--elbow-r) 0 0 0;
margin-bottom: -1px;
}
--elbow-r ist 45px. Rechts und unten kein Radius — das Element schließt bündig an die Sidebar an. Das margin-bottom: -1px sieht nach Hack aus, ist aber bewusst: Subpixel-Rendering im Browser kann zwischen zwei Elementen eine 1px-Lücke entstehen lassen, die die Illusion des nahtlosen Bogens zerstört. Ein negativer Margin von einem Pixel schließt sie.
Das gleiche Problem taucht in LcarsNavFrame auf — dem inneren Rahmen der in Sub-Views verwendet wird. Dort ist es die andere Richtung:
.frame-elbow {
border-radius: var(--elbow-r) 0 0 0;
margin-top: -1px;
height: calc(100% + 1px);
}
height: calc(100% + 1px) statt negativem margin-bottom — je nach Kontext schließt die Lücke in die eine oder andere Richtung. Beide Lösungen sind minimal invasiv: ein Pixel Korrektur, kein Layout-Umbau.
Das Notch-SVG
Direkt unterhalb des Headers gibt es eine weitere LCARS-typische Form: einen Viertelkreis-Ausschnitt ("Notch") oben links im Content-Bereich. Für diese konkave Form reicht border-radius nicht — man braucht ein SVG:
<svg class="lcars-notch-svg" viewBox="0 0 100 100" preserveAspectRatio="none">
<path d="M 100 0 L 0 0 L 0 100 A 100 100 0 0 1 100 0 Z" :fill="activeColor" />
</svg>
Das SVG ist position: absolute und überlappt den Content-Bereich oben links. Der Pfad zeichnet ein Dreieck mit abgerundetem Arc entlang der Hypotenuse — das ergibt den charakteristischen konkaven Einschnitt. :fill="activeColor" ist ein Vue-Binding auf die Tab-Farbe: wenn der Tab wechselt, ändert sich auch die Notch-Farbe.
preserveAspectRatio="none" stellt sicher dass das SVG exakt in sein Container-Rechteck gestreckt wird. Das ist hier gewünscht — Notch-Breite und -Höhe werden über --notch-r in CSS gesteuert, nicht über SVG-Attribute.
Nav-Buttons: Flex von oben nach unten
Die Sidebar-Buttons sind keine <button>-Elemente — sie sind <div>s mit einer bestimmten Ausrichtung: Text unten rechts, der letzte Button streckt sich nach unten und hat seinen Text oben.
.lc-nav-btn {
display: flex;
align-items: flex-end;
justify-content: flex-end;
min-height: 60px;
padding-right: 14px;
padding-bottom: 6px;
font-family: var(--lc-font);
font-size: 1.3rem;
font-weight: 800;
text-transform: uppercase;
}
.lc-nav-btn:last-child {
flex: 1;
align-items: flex-start;
}
flex: 1 auf dem letzten Button füllt den verbleibenden Raum — die Sidebar hat keine sichtbare untere Kante. In LCARS ist dieser Bereich oft ein breiter andersfarbiger Block. Hier ist es derselbe Button, nur gestreckt — weniger DOM-Elemente, gleiches Ergebnis.
LcarsPanel
Der einfachste Baustein: ein Panel mit Titel. Jede Sektion auf dem Dashboard ist ein LcarsPanel.
<LcarsPanel title="METEOROLOGIE" color="var(--lc-accent)">
</LcarsPanel>
Die Komponente rendert links neben dem Titel einen farbigen Balken (16×24px, Kapselform), dann den Titel in Antonio-Uppercase, dann eine Trennlinie bis zum Rand. Über einen #header-Slot können Elemente rechts neben dem Titel eingehängt werden — in der Technik-View zum Beispiel ein LED-Indikator der zeigt ob irgendein Licht an ist.
LcarsDialog
Für Vollbild-Dialoge gibt es LcarsDialog: Timer-Alarm, Bestätigungsabfragen, Warte-Zustand beim Verlassen. Das Layout: oben eine horizontale Balkenreihe (Pill + Bar + Pips), zentriert der Titel, darunter der Slot-Inhalt, unten dieselbe Reihe gespiegelt.
<LcarsDialog
v-if="showAlarm && !dismissed"
title="TIMER ABGELAUFEN"
sub="KÜCHE — COUNTDOWN"
:alert="true"
>
<div class="lc-btn-row">
<button class="lc-btn" style="--c: var(--lc-text-bright)" @click="dismissed = true">IGNORIEREN</button>
<button class="lc-btn" style="--c: var(--lc-alert)" @click="stopAlarm">ALARM STOPPEN</button>
</div>
</LcarsDialog>
Der alert-Modus färbt den Titel in --lc-alert und versetzt ihn in eine Blink-Animation. Die Timer-Alarm-Logik steckt im shared useTimerAlarm-Composable und ist damit in allen drei Apps nutzbar — auch wenn der Alarm nur auf dem Tablet sichtbar ist.
LcarsNavFrame
Innerhalb einiger Views gibt es ein weiteres Layout-Muster: eine Sub-Sidebar links (Kategorien), Content rechts, mit eigenem Elbow und Notch. Das taucht in der Versorgung-View (Vorratskategorien links, Artikel rechts) und im Replikator (Zutaten-Kategorien links, Auswahl rechts) auf.
jscpd aus Teil 4 hat genau dieses Muster als Klon aufgedeckt — identisches Template-Gerüst in zwei Views. Das Ergebnis des Refactorings ist LcarsNavFrame:
<LcarsNavFrame :color="elbowColor">
<template #nav>
</template>
</LcarsNavFrame>
Die Komponente nimmt color als Prop und rendert Elbow, Topbar und Notch in dieser Farbe. Jede View gibt nur Farbe und Inhalt — das Layout-Gerüst kommt aus der Komponente.

App.vue: die Shell
Die oberste Ebene ist zweiteilig: Sidebar links, Main-Bereich rechts. Sidebar enthält Elbow und Nav-Buttons. Main enthält Header-Balken, Notch-SVG und <router-view>.
<keep-alive include="ViewKI,ViewRezeptbuch,ViewEinkauf"> erhält die Komponenten-Instanzen beim Tab-Wechsel. Das ist für diese Views wichtig: der Replikator hat ggf. ein laufendes oder gerade fertiggestelltes KI-Ergebnis, das Rezeptbuch eine offene Detailansicht, die Einkaufsliste Offline-State — alles soll beim Zurückkehren noch da sein.
Brücke und Technik (ViewMain, ViewTechnik) sind nicht in keep-alive — sie lesen aus HA-Entities, die sowieso reaktiv aktualisiert werden. Da kostet das frische Rendern nichts.
Klick auf den aktiven Tab resettet die View:
function onNavClick(tab: Tab): void {
if (isActive(tab.path) && ['/ki', '/rezepte'].includes(tab.path)) {
activeView.value?.reset?.()
} else {
router.push(tab.path)
}
}
activeView ist ein Ref auf die aktive Komponente. Views die das unterstützen, exponieren reset() über defineExpose. Beim Replikator zum Beispiel: wenn ein KI-Ergebnis angezeigt wird und man auf "REPLIKATOR" klickt, kommt man wieder zur Zutatenauswahl.
IPC: navigate_rezept
In App.vue wird beim Start ein IPC-Listener registriert — der Deep-Link aus Teil 3:
useAppIPC(async (on) => [
await on('navigate_rezept', async ({ slug, portionen }) => {
const query = portionen ? { portionen: String(portionen) } : {}
await router.push({ path: '/rezepte/' + (slug as string), query })
}),
])
Der Listener lebt in App.vue und ist damit immer aktiv, egal welche View gerade offen ist. Wenn die Desktop-App ein navigate_rezept-Event sendet, navigiert das Tablet sofort zu /rezepte/<slug> — ViewRezeptbuch öffnet den passenden Eintrag. Das passiert auch wenn gerade die Technik-View offen ist.
Touch-Events
Das Tablet ist ein Touchscreen. Ein Fallstrick bei Touch-Handling: Vue's .prevent-Modifier (@touchstart.prevent) macht Listener non-passive, was neuere Browser mit Konsolen-Warnungen quittieren und auf manchen Geräten Scroll-Verhalten beeinträchtigt.
Die Lösung: touch-action: manipulation im CSS — der Browser weiß dann dass kein Pinch-Zoom stattfindet und entfernt den Double-Tap-Delay. Dazu .passive am Listener:
style="touch-action: manipulation"
@touchstart.passive="startHold"
@touchend="cancelHold"
Kein .prevent nötig. Ein konkretes Beispiel ist der PC-Button in der Brücke-View: kurz antippen = an/aus toggeln, lang halten = Wake-on-LAN-Signal senden. Der Hold-Timeout läuft in usePCVater, der Button bekommt Mouse- und Touch-Events parallel. touch-action: manipulation verhindert dass der Browser den Touch für Gesten reserviert.
Der Replikator
ViewKI: Links eine Sub-Sidebar mit Vorratskategorien über LcarsNavFrame, rechts ein 3-Spalten-Grid mit den Zutaten der gewählten Kategorie. Oben eine Button-Reihe für die Mahlzeitkategorie (Hauptgericht, Dessert, ...). Unten: "MAHLZEIT PLANEN".
Die Zutat-Buttons nutzen das LED-Button-Muster aus lcars.css: ein ::after-Pseudo-Element simuliert eine kleine LED rechts im Button.
.lc-led-btn.on::after {
background: radial-gradient(
circle at 38% 32%,
var(--led-start) 0%,
var(--led-mid) 55%,
var(--led-end) 100%
);
box-shadow: 0 0 7px 2px var(--led-mid);
}
Nicht ausgewählt: LED dunkel. Ausgewählt: LED leuchtet mit Glow-Effekt. Kein extra DOM-Element nötig — alles über CSS.
Während die KI-Anfrage läuft, erscheint nur der Text "REPLIKATOR AKTIV…" mit Blink-Animation. Kein Spinner, kein Fortschrittsbalken. Das passt zur LCARS-Ästhetik — und ist ehrlich: Claude braucht ein paar Sekunden, da hilft kein animierter Ring.
Das Ergebnis wird über RezeptDetail.vue gerendert: Zutaten links, Schritte rechts, alles im LCARS-Stil. "IN DATENBANK ARCHIVIEREN" speichert das Rezept per wsCommand('ha_widgets/rezepte/create') — dann ist es sofort im Rezeptbuch auf allen drei Apps verfügbar.

Wie geht es weiter?
Teil 7 schaut auf die anderen beiden Apps: die Handy-App mit Material-3-Dark-Theme und Touch-Optimierungen, und die Desktop-App mit dem Verwaltungs-Interface — Rezept-Editor, Lagerverwaltung, Drag&Drop.
Fazit
- LCARS in CSS: Bögen über
border-radius, Notch als SVG, Typografie über Antonio-Font, alles in Uppercase mit letter-spacing - Farbsystem: fünf Tab-Farben in aktiv/gedimmt als CSS-Variablen — Elbow, Header und Notch folgen der aktiven Tab-Farbe mit
- Subpixel-Tricks:
margin: -1px/height: calc(100% + 1px)schließen Rendering-Lücken an Bogenkanten - LcarsPanel: einfaches Panel mit farbigem Balken und Titelzeile;
#header-Slot für Zusatz-Elemente - LcarsDialog: Vollbild-Dialog für Alarm, Bestätigung, Warten — mit optionalem Alert-Modus und Blink-Animation
- LcarsNavFrame: wiederverwendbarer Sub-Frame mit Sidebar, Elbow und Notch — eine Prop, ein Slot
- App.vue: Sidebar-Shell mit
<keep-alive>für State-erhaltende Views, Tab-Reset viadefineExpose - Touch-Events:
touch-action: manipulation+.passivestatt.prevent— kein Scroll-Blockieren, kein Double-Tap-Delay - Replikator: LED-Buttons als Zutatenauswahl, minimaler Lade-Text statt Spinner, Ergebnis direkt archivierbar
