HA Widgets — Eigene Home-Assistant-Apps mit Vue 3 (Teil 6: Tablet und LCARS-Design)

- Veröffentlicht unter Makerspace von

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.

Gesamtansicht der Tablet-App


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.

LcarsNavFrame in Aktion


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.

Replikator-Ergebnis


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 via defineExpose
  • Touch-Events: touch-action: manipulation + .passive statt .prevent — kein Scroll-Blockieren, kein Double-Tap-Delay
  • Replikator: LED-Buttons als Zutatenauswahl, minimaler Lade-Text statt Spinner, Ergebnis direkt archivierbar