Initialer Import: HA-Panel, Design-System, Companion-App

Drei zusammengehörige Teile in einem Repository:

- homeassistant/  Das fertige, im Einsatz befindliche Home-Assistant-Panel
  (panel_custom Custom Element + pyscript-Backend). Echte Fahrzeug- und
  Personendaten (fahrzeugprofil.json, fahrten.jsonl, tankvorgaenge.jsonl,
  Tankbelege) bleiben per .gitignore außen vor; die anonymisierte Vorlage
  fahrzeugprofil.example.json ist mit dabei.

- design-system/  Eigenständige React-Komponentenbibliothek (@audi-dash/ui),
  die die visuelle Sprache des Panels nachbildet - ohne Audi-Markenzeichen
  und ohne die lizenzierte Hausschrift. Dient als Grundlage für Claude
  Design. War bis hierher ein eigenes Repository und ist in dieses
  eingeschmolzen worden.

- companion-app/  Datenschicht der neuen App DataMetric360 (iOS/Android via
  Capacitor, zusätzlich als Iframe im HA-Dashboard). Noch ohne Oberfläche:
  REST- und WebSocket-Zugriff auf Home Assistant plus Warteschlange für
  Änderungen ohne Netz. Ersetzt das eingespritzte hass-Objekt, das nur
  innerhalb des HA-Frontends existiert.

Dazu die Projektdokumentation: SPECIFICATION.md (Ist-Stand des Panels),
COMPANION_APP_ARCHITECTURE.md (Architekturentscheidungen der neuen App),
AUDIT_2026-08-10.md, DESIGN_BRIEF_DATAMETRIC360.md und der ursprüngliche
Bauauftrag.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-11 00:20:05 +02:00
co-authored by Claude Opus 5
commit e1d570992e
151 changed files with 18162 additions and 0 deletions
@@ -0,0 +1,58 @@
import * as React from "react";
export interface ImagePlaceholderProps {
src?: string;
alt: string;
/** Caption shown on the load-error placeholder. */
label: React.ReactNode;
/** Small monospace hint under the label, e.g. an expected filename. */
hint?: React.ReactNode;
/** Placeholder icon override - defaults to a simple image glyph. */
icon?: React.ReactNode;
size?: "default" | "mini" | "round";
className?: string;
}
const DEFAULT_ICON = (
<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden>
<path
fill="currentColor"
fillRule="evenodd"
d="M4 5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5Zm2 0v14h12V5H6Zm3 3a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm-1 8 3-3 2 2 3-4 3 5H8Z"
clipRule="evenodd"
/>
</svg>
);
export function ImagePlaceholder({
src,
alt,
label,
hint,
icon,
size = "default",
className,
}: ImagePlaceholderProps) {
const [failed, setFailed] = React.useState(false);
const classes = ["ads-bildbox"];
if (size === "mini") classes.push("ads-bildbox--mini");
if (size === "round") classes.push("ads-bildbox--round");
if (className) classes.push(className);
const showPlaceholder = !src || failed;
return (
<div className={classes.join(" ")}>
{!showPlaceholder && (
<img src={src} alt={alt} onError={() => setFailed(true)} />
)}
{showPlaceholder && (
<div className="ads-platzhalter">
<span className="ads-platzhalter__icon">{icon ?? DEFAULT_ICON}</span>
<span className="ads-platzhalter__label">{label}</span>
{hint !== undefined && <span className="ads-platzhalter__hint">{hint}</span>}
</div>
)}
</div>
);
}