Kraftstoff-Rabatt
${CAR.smartdeal.aktiv ? `
Gültig bis
@@ -3897,7 +4018,10 @@ const ICONS = {
/* Wagen von vorn - dasselbe Symbol wie in der myAudi-App (Audi-Symbolsatz,
assets/icons/car.svg). Flaeche statt Kontur, deshalb die Klasse "voll". */
audi: '
',
- trips: '
',
+ /* CI-Symbol tour-s statt der frueheren handgezeichneten Route - dieselbe
+ Fuellflaeche-statt-Kontur-Behandlung wie "audi" und "stat" (siehe
+ ICONS_VOLL). */
+ trips: '
',
/* CI-Symbol polls-s statt der vorherigen handgezeichneten Balken - dieselbe
Fuellflaeche-statt-Kontur-Behandlung wie "audi" (siehe ICONS_VOLL). Anders
als die handgezeichneten Tab-Icons (die im 24er-Raster deutlich Rand
@@ -3920,7 +4044,7 @@ const TABS = [
{ id: "fuel", label: "Tanken", eyebrow: "", title: "Tanken" },
];
/* Symbole, die als Flaeche gezeichnet sind statt als Kontur */
-const ICONS_VOLL = new Set(["audi", "stat"]);
+const ICONS_VOLL = new Set(["audi", "stat", "trips"]);
let route = { name: "home" };
const ZURUECK = {
trip: "trips", fill: "fuel", service: "audi", reifen: "audi", ident: "audi",
@@ -4286,77 +4410,17 @@ const LUPE_SVG = `
0 ? a.slice(0, idxUnter + 1) : null;
-}
-
-/* Alle aktuell im Setup zugeordneten Entity-IDs (domain-frei), außer der
- übergebenen selbst - Vergleichsbasis für entitaetIdKurz() unten. */
-function setupAndereIdsOhneDomain(ausser) {
- const ids = [];
- for (const feld of setupKatalog()) {
- const wert = setupZuordnung[feld.key];
- if (feld.liste) { for (const w of (wert || [])) if (w && w !== ausser) ids.push(w); }
- else if (wert && wert !== ausser) ids.push(wert);
- }
- return ids.map((id) => id.includes(".") ? id.split(".").slice(1).join(".") : id);
-}
-
-/* Die Entity-ID ohne Domain und ohne das installationsspezifische Präfix -
- die tatsächliche, stabile Kennung des zugeordneten Sensors, unabhängig
- davon, wie Geräte-Slug und Integrationsinstanz beim jeweiligen Nutzer
- heißen (z. B. "testcar_b9_fmm003_testintegratoin_" oder
- "audi_rs_4_avant_").
-
- Das Präfix ist NICHT das mit dem einzelnen nächsten Verwandten geteilte
- (frühere Fassung) - das kürzte in der Praxis zu viel: zwei Felder
- derselben Sensorquelle können selbst wieder ein gemeinsames Namens-
- Teilstück haben, das nichts mit der Installation zu tun hat (z. B. teilen
- sich "oil_change_due" und "oil_change_distance" das Wortstück
- "oil_change_", "can_fuel_volume" teilt sich "can_" mit einem anderen
- CAN-Sensor) - genau das hat der Nutzer live beobachtet: "can_fuel_volume"
- wurde zu bloß "fuel_volume", "oil_change_due" zu bloß "due". Stattdessen
- zählt jedes Präfix, das eine Entität mit irgendeiner ANDEREN teilt, als
- eine Stimme; das Präfix mit den MEISTEN Stimmen gewinnt. Das echte
- Installationspräfix teilt sich die Entität mit fast allen anderen
- Sensoren ihrer Quelle (viele Stimmen), ein zufällig geteiltes
- Namens-Teilstück nur mit dem einen verwandten Feld (eine Stimme) - die
- Mehrheit entscheidet also richtig, ohne einen einzelnen "nächsten"
- Nachbarn zu bevorzugen. Bei Gleichstand gewinnt das kürzere Präfix
- (vorsichtiger - weniger Kürzung im Zweifel). Mehrere gleichzeitige
- Sensorquellen (z. B. FMM003 und eine zweite, "audi_rs_4_avant_"-
- präfixte Integration) bleiben dabei weiterhin unabhängig voneinander
- korrekt gekürzt, da die Stimmen pro Entität und nicht global gezählt
- werden. Findet sich kein Verwandter mit gemeinsamem Präfix, bleibt die
- volle domain-freie ID stehen statt einer geratenen, möglicherweise
- falschen Kürzung. */
-function entitaetIdKurz(entityId) {
- if (!entityId) return null;
- const ohneDomain = entityId.includes(".") ? entityId.split(".").slice(1).join(".") : entityId;
- const stimmen = new Map();
- for (const andere of setupAndereIdsOhneDomain(entityId)) {
- const kandidat = praefixPaar(ohneDomain, andere);
- if (!kandidat) continue;
- stimmen.set(kandidat, (stimmen.get(kandidat) || 0) + 1);
- }
- let bestesPraefix = null, besteStimmen = 0;
- for (const [kandidat, anzahl] of stimmen) {
- const besser = anzahl > besteStimmen || (anzahl === besteStimmen && bestesPraefix && kandidat.length < bestesPraefix.length);
- if (besser) { bestesPraefix = kandidat; besteStimmen = anzahl; }
- }
- return bestesPraefix ? ohneDomain.slice(bestesPraefix.length) : ohneDomain;
-}
+/* praefixPaar()/setupAndereIdsOhneDomain()/entitaetIdKurz() sind am 2026-08-31
+ entfallen: sie kürzten die Kennung des gerade zugeordneten Sensors für den
+ Beispieltext im Setup. Der kommt jetzt fest aus dem Feld-Katalog, damit er
+ sich nicht mehr mit jeder Auswahl ändert - siehe setupFeldzeile(). */
/* Die Art des erwarteten Werts, als kurzes Klammer-Etikett direkt neben der
Feld-Überschrift ("Tankfüllstand [%]", "12V-Batteriespannung [V]") - eine
feste Eigenschaft der FELD-ROLLE selbst (aus dem FELDER-Katalog,
einstellungen.py), unabhängig davon, ob und welcher Sensor gerade
zugeordnet ist. Bewusst NICHT der Live-Wert eines Sensors - der gehört
- nirgends mehr hin, siehe entitaetIdKurz()'s eigenen Kommentar zur
- Kopfzeile. binary_sensor liefert immer den Rohzustand "on"/"off"
+ nirgends mehr hin. binary_sensor liefert immer den Rohzustand "on"/"off"
(unabhängig von device_class - die beeinflusst nur, wie ANDERE
HA-Oberflächen ihn anzeigen, nicht den Rohwert, den diese App liest).
Bei mehreren möglichen Einheiten (z. B. "L"/"l") die klein geschriebene
@@ -4409,18 +4473,33 @@ function entitaetCombo(key, idx) {
}
function setupFeldzeile(feld) {
- // Bei einem Listenfeld (vier Positionen wie Tür-/Fenstersensoren) gibt es
- // keinen EINEN zugeordneten Sensor, dessen Kennung sich anzeigen ließe -
- // jede Position hat ihre eigene Zuordnung, siehe die Unterfelder unten.
- // Das Einheiten-Etikett neben dem Label gilt dagegen für die ganze Rolle
- // und wird auch dort gezeigt.
+ // Einheit und Beispiel gelten beide für die ganze Feld-Rolle und stehen
+ // deshalb auch bei Listenfeldern (vier Positionen wie Tür-/Fenstersensoren).
const einheit = feldEinheitAnzeige(feld);
- const erwartet = feld.liste ? "" : entitaetIdKurz(setupZuordnung[feld.key]);
- // Nutzerkorrektur 2026-08-28: kein erfundenes statisches Beispiel mehr (war
- // für Reichweite/Sofort-Aktualisierung schlicht falsch) - nur EINE Zeile,
- // der tatsächlich zugeordnete Sensor (erwartet), als "(z. B. ...)"
- // dargestellt. Der Klammertext selbst in Label-Größe/-Farbe, der Sensorname
- // darin in derselben kleinen/grauen Schrift wie die Einheit ("[on/off]").
+ /* Das Beispiel steht fest im Feld-Katalog (FELDER in einstellungen.py,
+ Schlüssel "beispiel") und gehört damit zur FELD-ROLLE - genau wie das
+ Einheiten-Etikett daneben.
+
+ Vorgeschichte in zwei Schritten:
+
+ Bis 2026-08-28 stand hier ein von Hand ausgedachtes Beispiel. Das war
+ für Reichweite und Sofort-Aktualisierung schlicht falsch, und der
+ Nutzer hat es zu Recht beanstandet. Der Ersatz - die gekürzte Kennung
+ des GERADE ZUGEORDNETEN Sensors - war zwar nie falsch, aber auch kein
+ Beispiel mehr: er änderte sich mit jeder Auswahl mit und sagte einem
+ damit nur zurück, was man eben selbst eingestellt hatte (Nutzerkorrektur
+ 2026-08-31: "Ich möchte, dass xxx permanent bleibt").
+
+ Jetzt wieder fest - aber nicht ausgedacht: die Namen sind die der
+ tatsächlich laufenden Integration, abgelesen aus der Zuordnung dieser
+ Installation. Damit ist das Beispiel stabil UND wahr, und beide
+ Beanstandungen sind zugleich erfüllt.
+
+ Listenfelder bekommen es ebenfalls: dort ist der Name der ersten
+ Position das Muster, nach dem sich die anderen drei richten. */
+ const erwartet = feld.beispiel || "";
+ // Der Klammertext in Label-Größe/-Farbe, der Sensorname darin in derselben
+ // kleinen/grauen Schrift wie die Einheit ("[on/off]").
const hinweis = erwartet
? `(z. B. ${esc(erwartet)} ) `
: "";
@@ -4565,6 +4644,7 @@ function render() {
Aenderungspruefung in datenLaden(). */
if (MAP) { MAP.remove(); MAP = null; TILE = null; }
if (route.name === "trip" || route.name === "fill") initMap().catch(() => {}); // ohne Internet bleibt die Kartenflaeche leer
+ standortVerfolgungBeenden();
if (SMAP) { SMAP.remove(); SMAP = null; SMAP_TILE = null; FAHRZEUG_MARKER = null; USER_MARKER = null; }
if (route.name === "standort") initStandortMap().catch(() => {});
if (TMAP) { TMAP.remove(); TMAP = null; TMAP_TILE = null; }
@@ -5953,6 +6033,7 @@ class AudiDashboardPanel extends HTMLElement {
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.
if (MAP) { MAP.remove(); MAP = null; TILE = null; }
+ standortVerfolgungBeenden();
if (SMAP) { SMAP.remove(); SMAP = null; SMAP_TILE = null; FAHRZEUG_MARKER = null; USER_MARKER = null; }
if (TMAP) { TMAP.remove(); TMAP = null; TMAP_TILE = null; }
}
@@ -5992,6 +6073,7 @@ class AudiDashboardPanel extends HTMLElement {
+
diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css b/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css
index 8274a9a..12fc092 100644
--- a/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css
+++ b/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css
@@ -105,6 +105,19 @@ html,body{height:100%}
.rings svg{height:17px}
.profilbtn{display:none}
/* Unterseite: kompakter, zentrierter Titel wie in einer iOS-Navigationsleiste */
+/* Gegengewicht zum Zurueck-Pfeil auf Unterseiten.
+
+ Der Titel wird dort mittig gesetzt (.head{align-items:center}), aber nur
+ innerhalb von .head - und .head beginnt erst hinter dem Zurueck-Pfeil.
+ Rechts steht nichts, weil die Ringe auf Unterseiten ausgeblendet werden.
+ Die Ueberschrift sass deshalb um die halbe Pfeilbreite zu weit rechts;
+ in der App an derselben Stelle live gemessen: 17px (2026-08-31).
+
+ Gleiche Masse wie .back (34px, margin -8px), damit links und rechts
+ exakt derselbe Platz belegt ist. Auf breiten Bildschirmen steht der
+ Titel linksbuendig - dort ist der Platzhalter abgeschaltet. */
+.kopfgegengewicht{display:none}
+.phone:has(.back.on) .kopfgegengewicht{display:block;order:10;flex:none;width:34px;height:34px;margin-right:-8px}
.phone:has(.back.on) .topbar{padding:54px 16px 6px;gap:8px}
.phone:has(.back.on) .head{align-items:center}
.phone:has(.back.on) .title{font-size:17px;font-weight:400;letter-spacing:-.01em;text-align:center}
@@ -217,11 +230,19 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
.legend span{font-size:12.5px}
/* ---------------- Menüs und Popups ---------------- */
-.bildmenu,.sdpopup{border:0;border-radius:14px;background:rgba(40,44,52,.92);
+/* Blaetter statt schwebender Karten (siehe audi-dashboard.css). Die Radien
+ folgen der neuen Lage: das Aktionsblatt steht frei ueber der unteren
+ Kante und ist rundum rund, das Formularblatt sitzt auf der Kante auf und
+ ist nur oben rund. */
+.bildmenu{border:0;border-radius:16px;background:rgba(40,44,52,.92);
-webkit-backdrop-filter:saturate(180%) blur(22px);backdrop-filter:saturate(180%) blur(22px);
box-shadow:0 12px 40px rgba(0,0,0,.5)}
+.sdpopup{border:0;border-radius:16px 16px 0 0;background:rgba(40,44,52,.92);
+ -webkit-backdrop-filter:saturate(180%) blur(22px);backdrop-filter:saturate(180%) blur(22px);
+ box-shadow:0 -8px 40px rgba(0,0,0,.5)}
:host([data-theme="tag"]) .bildmenu,:host([data-theme="tag"]) .sdpopup{background:rgba(250,250,252,.92)}
-.bildmenu button{padding:13px 16px;font-size:16px;border-bottom:1px solid var(--ios-sep)}
+.bildmenu button{padding:16px 18px;font-size:16px;min-height:52px;border-bottom:1px solid var(--ios-sep)}
+.bildmenu button:last-child{border-bottom:none}
/* ---------------- Tab-Leiste ---------------- */
.tabbar{border-top:1px solid var(--ios-sep);padding-bottom:26px;
@@ -319,6 +340,7 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
/* Die Kopf-Ringe setzt die App per Inline-Stil (display:inline-flex),
deshalb hier mit !important. */
.rings{display:none !important}
+ .kopfgegengewicht{display:none !important}
/* Nur die Box selbst hoeher setzen, nicht jedes Element mit .carfix: die
Klasse haengt auch am
und am Platzhalter-
(siehe
@@ -338,7 +360,7 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
ruhigen Karte. max-width + margin:auto zentriert sie innerhalb des
bestehenden left/right-Abstands, ohne die transform-basierte
Einblend-Animation dieser Elemente zu beruehren. */
- .sdpopup,.sheet,.beleg-popup{max-width:560px;margin-left:auto;margin-right:auto}
+ .sdpopup,.sheet,.beleg-popup,.bildmenu{max-width:560px;margin-left:auto;margin-right:auto}
/* .setup-popup breiter als die übrigen Popups: die Sensorkennung neben der
Feld-Überschrift (entitaetIdKurz()) kann ein echter Rohsensorname von
Herstellerseite sein, teils 40-60 Zeichen lang
diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard.css b/custom_components/audi_dashboard/frontend/audi-dashboard.css
index 30c0cb1..85e9d6d 100644
--- a/custom_components/audi_dashboard/frontend/audi-dashboard.css
+++ b/custom_components/audi_dashboard/frontend/audi-dashboard.css
@@ -599,8 +599,12 @@ button.leaf:active { background: var(--tile-2); }
.szene.bleed::before {
content: "";
position: absolute; left: 0; right: 0; bottom: 0;
- height: 42%;
- background: linear-gradient(to bottom, transparent, var(--canvas) 78%);
+ /* 26/96 statt 42/78: der Verlauf soll die harte Bildkante ausblenden,
+ nicht das Fahrzeug. Mit 42% Hoehe und Vollton ab 78% war das untere
+ Drittel des Fotos praktisch einfarbig - Raeder und Schweller lagen
+ darunter (gemeldet vom Eigentuemer, 2026-08-31). */
+ height: 26%;
+ background: linear-gradient(to bottom, transparent, var(--canvas) 96%);
pointer-events: none;
z-index: 1;
}
@@ -668,7 +672,9 @@ button.leaf:active { background: var(--tile-2); }
.marke { display: inline-flex; align-items: center; gap: 6px; }
.marke-logo { display: inline-flex; align-items: center; }
-.marke-logo img { height: 13px; width: auto; }
+/* Auf Zeilenhoehe (16px) statt 13px: das Markenlogo stand neben Text
+ derselben Zeile und wirkte daneben zu klein. */
+.marke-logo img { height: 16px; width: auto; }
.marke-logo .ph { display: none; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--fg3); }
.marke-logo.logo-fehlt img { display: none; }
.marke-logo.logo-fehlt .ph { display: inline; }
@@ -1011,19 +1017,23 @@ button.leaf:active { background: var(--tile-2); }
gezaehlt, das Blatt war auf dem Geraet rund 16px hoeher als noetig und
schob sich entsprechend weiter hoch. Am Rechner faellt das nicht auf,
dort ist der Einzug 0. */
- padding: 0 20px 18px;
+ padding: 0 20px 34px;
transform: translateY(calc(100% - var(--standort-peek)));
transition: transform .28s cubic-bezier(.22,.61,.36,1);
}
-/* Aufgezogen rastet das Blatt auf der mittleren Hoehe ein, nicht auf seiner
- Inhaltshoehe. Apple beschreibt genau das als "detents": Hoehen, auf denen
- ein Blatt natuerlich zur Ruhe kommt - large ist das voll ausgefahrene
- Blatt, medium etwa die Haelfte davon. Vorher war die Rasthoehe schlicht
- die Hoehe des Inhalts, dadurch schlossen die Knoepfe unten buendig mit der
- Menueleiste ab. Der Ueberschuss liegt unter den Knoepfen und haelt sie von
- der Leiste frei. Die eingeklappte Hoehe bleibt unberuehrt: die Verschiebung
- rechnet mit 100% der jeweiligen Blatthoehe minus dem Guckwert. */
-.standortmenu.offen { transform: translateY(0); min-height: 50%; }
+/* Aufgezogen bleibt unter den Knoepfen ein fester Abstand stehen, statt dass
+ sie buendig mit der Menueleiste abschliessen.
+
+ Vorher stand hier min-height:50% - Apples "medium detent". Gemessen ergab
+ das 89px (Panel) bzw. 76px (App) Luft unter dem letzten Knopf, und das war
+ sichtbar zu viel: das Blatt zog ueber die Haelfte des Bildschirms auf,
+ obwohl sein Inhalt dort laengst zu Ende war. Eine Raste ist nur dann
+ richtig, wenn zwischen ihr und der Inhaltshoehe kein grosses Loch klafft.
+
+ Jetzt bestimmt der Inhalt die Hoehe, und die 34px Fussabstand halten die
+ Knoepfe von der Leiste frei - hergeleitet aus der Beanstandung, nicht
+ geraten: 18px waren zu wenig, 76-89px zu viel. */
+.standortmenu.offen { transform: translateY(0); }
.standortmenu-griffzone { padding: 10px 0 14px; touch-action: none; cursor: grab; position: relative; }
/* Der Griff ist jetzt ein Knopf (Tastaturzugang zum Menue). Die Knopf-
Voreinstellungen des Browsers werden deshalb zurueckgesetzt; die
@@ -1090,38 +1100,62 @@ button.leaf:active { background: var(--tile-2); }
.standort-pille[disabled] { opacity: .4; pointer-events: none; }
/* ------------------------------------------------------- Bilderverwaltung */
-.bildmenu-catcher { position: fixed; inset: 0; z-index: 1000; }
+/* ---------------------------------------------------------- Blätter
+ Apples Vorgabe kennt für etwas Modales auf dem Telefon kein frei
+ schwebendes Fenster: es kommt von der unteren Kante. Das Aktionsblatt
+ (eine kurze Liste von Wegen) und das Formularblatt (eine Eingabe) sind
+ die beiden Formen; die Karte in der Bildschirmmitte war keine von
+ beiden und stand ausserdem ohne abgedunkelten Grund über der voll
+ sichtbaren Seite.
+
+ Der Schleier gehört dazu und ist nicht Geschmack: modal heisst, dass
+ der Inhalt dahinter nicht bedienbar ist - das soll man sehen. */
+.bildmenu-catcher {
+ position: absolute; inset: 0; z-index: 1000;
+ background: rgba(0,0,0,.45);
+ animation: sheet-fade .18s ease-out;
+}
.bildmenu {
position: absolute; z-index: 1001;
- background: var(--tile-2);
- border-radius: 14px;
- padding: var(--sp-2);
+ left: 10px; right: 10px;
+ bottom: max(10px, env(safe-area-inset-bottom));
+ top: auto;
+ transform: none;
+ background: var(--tile-deckend, var(--canvas));
+ border-radius: 16px;
+ overflow: hidden;
+ padding: 0;
display: flex; flex-direction: column;
- min-width: 160px;
+ animation: sheet-rein .24s cubic-bezier(.22,.61,.36,1);
}
-.bildmenu.rad { top: 90px; left: 22px; }
-/* "Bild der Übersicht": das Vorschaubild füllt die ganze Kachelbreite, seine
- Unterkante liegt daher immer nah an der Kachel-Unterkante - das Menü von
- dort aus verankern statt mit einem festen top-Wert, der bei jedem
- Bildseitenverhältnis neu nachgemessen werden müsste. */
-.bildmenu.ansicht { bottom: var(--sp-5); left: 50%; transform: translateX(-50%); }
+/* Die beiden Lage-Varianten sind gegenstandslos geworden - ein Blatt
+ kommt immer von unten. Die Klassen bleiben im Markup stehen, damit die
+ Aufrufstellen unberührt bleiben; hier tragen sie nichts mehr bei. */
+.bildmenu.rad, .bildmenu.ansicht { left: 10px; right: 10px; top: auto; transform: none; }
.bildmenu button {
display: block; width: 100%; text-align: left;
background: none; border: none;
- color: var(--fg); font-family: inherit; font-size: 15px;
- padding: 13px 10px; min-height: 46px; border-radius: 10px;
+ color: var(--fg); font-family: inherit; font-size: 16px;
+ padding: 16px 18px; min-height: 52px; border-radius: 0;
+ border-bottom: 1px solid var(--line);
cursor: pointer;
}
+.bildmenu button:last-child { border-bottom: none; }
.bildmenu button:active { background: var(--shade); }
.bildmenu button.loeschen { color: var(--bad); }
+/* Formularblatt (siehe Kasten oben): unten angeschlagen, nur oben rund,
+ blickdicht. Stand vorher als Karte 60px unter dem oberen Rand - ausser
+ Daumenreichweite und ohne sichtbaren Weg hinaus. */
.sdpopup {
position: absolute; z-index: 1001;
- left: var(--sp-5); right: var(--sp-5); top: 60px;
- background: var(--tile-2);
- border-radius: 14px;
- padding: var(--sp-4);
+ left: 0; right: 0; bottom: 0; top: auto;
+ background: var(--tile-deckend, var(--canvas));
+ border-radius: 16px 16px 0 0;
+ padding: var(--sp-4) 20px max(20px, env(safe-area-inset-bottom));
+ animation: blatt-hoch .28s cubic-bezier(.22,.61,.36,1);
}
+@keyframes blatt-hoch { from { transform: translateY(100%) } to { transform: none } }
/* Beleg-Fenster: PDF hineinziehen (Rechner) oder einfügen (Telefon). */
.beleg-catcher {
@@ -1129,15 +1163,20 @@ button.leaf:active { background: var(--tile-2); }
background: rgba(0,0,0,.45);
animation: sheet-fade .18s ease-out;
}
+/* Formularblatt (siehe Kasten oben). Traegt drei Fenster: Beleg
+ hochladen, Daten importieren, Datensatz sichern/laden. */
.beleg-popup {
position: absolute; z-index: 1001;
- left: var(--sp-5); right: var(--sp-5);
- top: 50%; transform: translateY(-50%);
- /* Wie .setup-popup: --tile-2 ist im Nacht-Theme transluzent, ein frei
- schwebendes Fenster braucht einen blickdichten Grund. */
+ left: 0; right: 0; bottom: 0;
+ top: auto; transform: none;
+ max-height: 88%;
+ overflow-y: auto;
+ /* --tile-2 ist im Nacht-Theme transluzent; ein Blatt ueber der Seite
+ braucht einen blickdichten Grund. */
background: var(--tile-deckend, var(--canvas));
- border-radius: var(--r-tile);
- padding: var(--sp-4);
+ border-radius: 16px 16px 0 0;
+ padding: var(--sp-4) 20px max(20px, env(safe-area-inset-bottom));
+ animation: blatt-hoch .28s cubic-bezier(.22,.61,.36,1);
}
.beleg-drop {
margin-top: var(--sp-3);
diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json
index 6897cdb..d766cee 100644
--- a/custom_components/audi_dashboard/manifest.json
+++ b/custom_components/audi_dashboard/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
- "version": "2026.8.30.20",
+ "version": "2026.8.31.1",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"],
diff --git a/design-system/src/components/ImagePlaceholder/ImagePlaceholder.tsx b/design-system/src/components/ImagePlaceholder/ImagePlaceholder.tsx
index b33ba15..b0e1a84 100644
--- a/design-system/src/components/ImagePlaceholder/ImagePlaceholder.tsx
+++ b/design-system/src/components/ImagePlaceholder/ImagePlaceholder.tsx
@@ -33,18 +33,34 @@ export function ImagePlaceholder({
size = "default",
className,
}: ImagePlaceholderProps) {
- const [failed, setFailed] = React.useState(false);
+ /*
+ * Gemerkt wird die Adresse, die fehlgeschlagen ist - nicht ein blosses
+ * "ist fehlgeschlagen".
+ *
+ * Der Unterschied ist der Fehler, den das behebt: In der Fotogalerie unter
+ * "Mein Audi" bleibt dieselbe Komponente stehen und bekommt nur eine neue
+ * src. Mit einem blossen Wahrheitswert blieb ein einmal gesetztes true fuer
+ * immer stehen - sobald ein noch leerer Platz durchlaufen war, zeigten auch
+ * alle folgenden Plaetze mit echtem Foto nur noch den Platzhalter. Erst ein
+ * Seitenwechsel setzte den Zustand zurueck, weil die Komponente dabei neu
+ * entsteht (gemeldet vom Eigentuemer, 2026-08-31).
+ *
+ * Bewusst ohne useEffect: ein Vergleich beim Rendern kommt ohne den
+ * zusaetzlichen Durchlauf aus, den ein Zuruecksetzen im Effekt braucht -
+ * und in dem waere fuer einen Wimpernschlag das falsche Bild zu sehen.
+ */
+ const [fehlgeschlagen, setzeFehlgeschlagen] = React.useState
(null);
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;
+ const showPlaceholder = !src || fehlgeschlagen === src;
return (
{!showPlaceholder && (
-
setFailed(true)} />
+
setzeFehlgeschlagen(src)} />
)}
{showPlaceholder && (
diff --git a/design-system/src/components/Pill/Pill.css b/design-system/src/components/Pill/Pill.css
index 7a56314..b7636a6 100644
--- a/design-system/src/components/Pill/Pill.css
+++ b/design-system/src/components/Pill/Pill.css
@@ -18,3 +18,15 @@
color: var(--red);
border-color: var(--red);
}
+
+/* Als Knopf: die Optik bleibt exakt die der Anzeige-Pille, nur die
+ Browser-Vorgaben fuer
werden zurueckgenommen. Deckungsgleich mit
+ `.pill` im Panel, wo dieselbe Pille mit style="cursor:pointer;
+ font-family:inherit" als Knopf gerendert wird. */
+.ads-pill--knopf {
+ font: inherit;
+ font-size: 12.5px;
+ cursor: pointer;
+ -webkit-appearance: none;
+ appearance: none;
+}
diff --git a/design-system/src/components/Pill/Pill.tsx b/design-system/src/components/Pill/Pill.tsx
index e66151d..8d1458b 100644
--- a/design-system/src/components/Pill/Pill.tsx
+++ b/design-system/src/components/Pill/Pill.tsx
@@ -5,11 +5,34 @@ export interface PillProps {
/** "work" recolors border+text to the accent red - use for a highlighted tag. */
variant?: "default" | "work";
className?: string;
+ /**
+ * Macht aus der Pille einen Knopf.
+ *
+ * Nicht kosmetisch: im Panel ist die Art-Pille einer Fahrt seit jeher ein
+ * ``, das zwischen Privat und Arbeitsweg umschaltet. Ohne
+ * diesen Zweig war sie in der App ein totes `` - dieselbe Optik, aber
+ * ohne die Bedienung, was von aussen wie ein Fehler aussieht und es de facto
+ * auch war.
+ *
+ * Fehlt onClick, bleibt es beim ``: eine Pille, die nur etwas anzeigt,
+ * soll auch nicht per Tabulator erreichbar sein.
+ */
+ onClick?: () => void;
+ /** Nur mit onClick sinnvoll - beschreibt die Wirkung, nicht den Zustand. */
+ ariaLabel?: string;
}
-export function Pill({ children, variant = "default", className }: PillProps) {
+export function Pill({ children, variant = "default", className, onClick, ariaLabel }: PillProps) {
const classes = ["ads-pill"];
if (variant === "work") classes.push("ads-pill--work");
+ if (onClick) classes.push("ads-pill--knopf");
if (className) classes.push(className);
+ if (onClick) {
+ return (
+
+ {children}
+
+ );
+ }
return {children} ;
}
diff --git a/design-system/src/components/Popup/Popup.tsx b/design-system/src/components/Popup/Popup.tsx
index 5093c97..d1e1a1b 100644
--- a/design-system/src/components/Popup/Popup.tsx
+++ b/design-system/src/components/Popup/Popup.tsx
@@ -48,7 +48,27 @@ export function Popup({
>
);
- const target = portalTarget ?? (typeof document !== "undefined" ? document.body : null);
+ /*
+ * Ziel des Portals ist die App-Wurzel, NICHT document.body.
+ *
+ * Die Farbwerte haengen an [data-theme] auf dem .ads-root-Element (siehe
+ * tokens/tokens.css: :root traegt den Nachtmodus, [data-theme="tag"]
+ * ueberschreibt ihn). Ein nach document.body portaliertes Fenster steht
+ * ausserhalb dieses Elements und erbt damit immer den Nachtmodus - im
+ * Tagmodus gemessen am 2026-08-31: innerhalb --tile:#FFFFFF, auf body
+ * --tile:rgba(255,255,255,.055). Das Fenster war dadurch eine fast
+ * durchsichtige Flaeche auf weissem Grund, ohne erkennbare Kante.
+ *
+ * Das betraf jedes Popup der App gleichermassen und war kein Fehler der
+ * einzelnen Aufrufe - deshalb sitzt die Korrektur hier und nicht dort.
+ *
+ * position:fixed bleibt davon unberuehrt: .ads-root setzt weder transform
+ * noch filter, der umschliessende Block bleibt also das Sichtfenster, und
+ * ein overflow:hidden weiter oben schneidet ein solches Element nicht ab.
+ */
+ const target =
+ portalTarget ??
+ (typeof document !== "undefined" ? (document.querySelector(".ads-root") ?? document.body) : null);
return target ? createPortal(content, target) : content;
}
diff --git a/design-system/src/components/Sheet/ActionSheet.tsx b/design-system/src/components/Sheet/ActionSheet.tsx
new file mode 100644
index 0000000..0cf2050
--- /dev/null
+++ b/design-system/src/components/Sheet/ActionSheet.tsx
@@ -0,0 +1,98 @@
+import * as React from "react";
+import { createPortal } from "react-dom";
+
+import { sheetPortalTarget, useEscape } from "./Sheet";
+
+/**
+ * Action sheet - the iOS pattern for a short set of choices the user asked for.
+ *
+ * Two groups, exactly as Apple describes them: the choices in one rounded
+ * card, "Cancel" alone in a second one below it with a gap between. The gap is
+ * not decoration - it is what makes Cancel unmistakably the way out rather
+ * than one more option in the list.
+ *
+ * Deliberately identical in geometry to `.sheet` in the panel's
+ * audi-dashboard.css, which already implemented this pattern: 10px side
+ * margins, 16px corners, 52px minimum row height, one hairline between rows,
+ * opaque surface. Two codebases, one look.
+ *
+ * Use `Sheet` instead when the content is a form rather than a list of
+ * choices.
+ */
+
+export interface ActionSheetAktion {
+ label: React.ReactNode;
+ onClick: () => void;
+ /** Red, for anything that removes or overwrites something. */
+ destructive?: boolean;
+ disabled?: boolean;
+}
+
+export interface ActionSheetProps {
+ open: boolean;
+ onClose: () => void;
+ /** Optional heading above the choices. */
+ title?: React.ReactNode;
+ /** One explanatory line under the title. */
+ description?: React.ReactNode;
+ actions: ActionSheetAktion[];
+ /** Label of the separated bottom button. Pass null to leave it out. */
+ cancelLabel?: string | null;
+ /** Rendered between the heading and the choices - an error line, a hint. */
+ children?: React.ReactNode;
+ className?: string;
+ portalTarget?: Element;
+}
+
+export function ActionSheet({
+ open,
+ onClose,
+ title,
+ description,
+ actions,
+ cancelLabel = "Abbrechen",
+ children,
+ className,
+ portalTarget,
+}: ActionSheetProps) {
+ useEscape(open, onClose);
+ const target = sheetPortalTarget(portalTarget);
+ if (!open || !target) return null;
+
+ const classes = ["ads-aktionsblatt"];
+ if (className) classes.push(className);
+
+ return createPortal(
+ <>
+
+
+
+ {(title || description) && (
+
+ {title && {title} }
+ {description && {description} }
+
+ )}
+ {children &&
{children}
}
+ {actions.map((aktion, i) => (
+
+ {aktion.label}
+
+ ))}
+
+ {cancelLabel !== null && (
+
+ {cancelLabel}
+
+ )}
+
+ >,
+ target,
+ );
+}
diff --git a/design-system/src/components/Sheet/Sheet.css b/design-system/src/components/Sheet/Sheet.css
new file mode 100644
index 0000000..ffa3229
--- /dev/null
+++ b/design-system/src/components/Sheet/Sheet.css
@@ -0,0 +1,214 @@
+/* -------------------------------------------------------------- Blätter
+ Zwei Bausteine, ein Muster: etwas Modales kommt von der unteren Kante,
+ nicht aus der Bildschirmmitte. Apples Vorgabe reserviert die schwebende
+ Karte (Popover) für große Bildschirme; auf dem Telefon soll daraus ein
+ Blatt werden.
+
+ Der abdunkelnde Schleier gehört dazu und ist nicht Geschmack: modal heißt,
+ dass der Inhalt dahinter nicht bedienbar ist - das soll man sehen. Die alte
+ .ads-popup-Fassung hatte gar keinen (background:none), die Karte schwebte
+ ohne Trennung über der noch voll sichtbaren Seite. */
+
+.ads-sheet-scrim {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ background: rgba(0, 0, 0, .45);
+ border: none;
+ animation: ads-blatt-blende .18s ease-out;
+}
+
+@keyframes ads-blatt-blende {
+ from { opacity: 0 }
+ to { opacity: 1 }
+}
+
+@keyframes ads-blatt-hoch {
+ from { transform: translateY(100%) }
+ to { transform: none }
+}
+
+@keyframes ads-blatt-rein {
+ from { transform: translateY(18px); opacity: 0 }
+ to { transform: none; opacity: 1 }
+}
+
+/* ------------------------------------------------------------- Formular */
+
+.ads-sheet {
+ position: fixed;
+ z-index: 1001;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ /* Blickdicht, nie durchscheinend: --tile-2 ist im Nachtthema ein rgba-Weiß
+ für Kachel-auf-Kachel und ließ die Seite durch den Dialog schimmern.
+ Dieselbe Korrektur trägt das Panel längst in .sheet-gruppe und
+ .setup-popup. */
+ background: var(--tile-deckend, var(--canvas));
+ /* Nur oben rund - das Blatt sitzt auf der Kante auf. */
+ border-radius: 16px 16px 0 0;
+ padding: 0 20px max(20px, env(safe-area-inset-bottom));
+ max-height: 88%;
+ display: flex;
+ flex-direction: column;
+ animation: ads-blatt-hoch .28s cubic-bezier(.22, .61, .36, 1);
+}
+
+/* Breite Bildschirme: ein Blatt über die volle Breite eines Monitors wäre
+ ein Band, kein Dialog. Dieselbe Obergrenze wie die Popups des Panels. */
+@media (min-width: 700px) {
+ .ads-sheet {
+ max-width: 560px;
+ margin-left: auto;
+ margin-right: auto;
+ border-radius: 16px;
+ bottom: 20px;
+ left: 20px;
+ right: 20px;
+ }
+}
+
+/* Greifer: das sichtbare Versprechen, dass sich das Blatt wegschieben lässt.
+ Maße wie der Griff des Standort-Blattes (36 x 4). */
+.ads-sheet__griff {
+ display: block;
+ width: 36px;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--line-strong);
+ margin: 8px auto 4px;
+ flex: none;
+}
+
+.ads-sheet__kopf {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 8px 0 12px;
+ flex: none;
+}
+
+.ads-sheet__kopf strong {
+ font-size: 17px;
+ font-weight: 400;
+ color: var(--fg);
+}
+
+.ads-sheet__kopf span {
+ font-size: 13px;
+ line-height: 1.45;
+ color: var(--fg2);
+}
+
+/* Der Inhalt scrollt, nicht das Blatt: der Greifer und die Überschrift sollen
+ stehen bleiben, wenn ein Formular länger wird als der Bildschirm. */
+.ads-sheet__inhalt {
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ overscroll-behavior: contain;
+}
+
+/* ------------------------------------------------------------- Auswahl */
+
+.ads-aktionsblatt {
+ position: fixed;
+ z-index: 1001;
+ left: 10px;
+ right: 10px;
+ bottom: max(10px, env(safe-area-inset-bottom));
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ animation: ads-blatt-rein .24s cubic-bezier(.22, .61, .36, 1);
+}
+
+@media (min-width: 700px) {
+ .ads-aktionsblatt {
+ max-width: 560px;
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+.ads-aktionsblatt__gruppe {
+ background: var(--tile-deckend, var(--canvas));
+ border-radius: 16px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.ads-aktionsblatt__kopf {
+ padding: 15px 18px 14px;
+ border-bottom: 1px solid var(--line);
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.ads-aktionsblatt__kopf strong {
+ font-size: 15px;
+ font-weight: 400;
+ color: var(--fg);
+}
+
+.ads-aktionsblatt__kopf span {
+ font-size: 13px;
+ color: var(--fg2);
+ line-height: 1.45;
+}
+
+.ads-aktionsblatt__zusatz {
+ padding: 12px 18px;
+ border-bottom: 1px solid var(--line);
+}
+
+.ads-aktionsblatt__gruppe button {
+ background: none;
+ border: none;
+ font-family: inherit;
+ font-size: 16px;
+ color: var(--fg);
+ padding: 16px 18px;
+ min-height: 52px;
+ text-align: left;
+ cursor: pointer;
+ border-bottom: 1px solid var(--line);
+}
+
+.ads-aktionsblatt__gruppe button:last-child {
+ border-bottom: none;
+}
+
+.ads-aktionsblatt__gruppe button:active {
+ background: var(--shade);
+}
+
+.ads-aktionsblatt__gruppe button:disabled {
+ color: var(--fg3);
+ cursor: default;
+}
+
+.ads-aktionsblatt__loeschen {
+ color: var(--bad);
+}
+
+/* Eigene Karte mit Abstand darüber - das ist es, was "Abbrechen" vom Rest der
+ Liste trennt und es unmissverständlich zum Ausweg macht, statt zu einer
+ weiteren Wahlmöglichkeit. */
+.ads-aktionsblatt__abbrechen {
+ background: var(--tile-deckend, var(--canvas));
+ border: none;
+ border-radius: 16px;
+ font-family: inherit;
+ font-size: 16px;
+ color: var(--fg);
+ padding: 16px;
+ min-height: 52px;
+ cursor: pointer;
+}
+
+.ads-aktionsblatt__abbrechen:active {
+ background: var(--shade);
+}
diff --git a/design-system/src/components/Sheet/Sheet.tsx b/design-system/src/components/Sheet/Sheet.tsx
new file mode 100644
index 0000000..8ccf45b
--- /dev/null
+++ b/design-system/src/components/Sheet/Sheet.tsx
@@ -0,0 +1,103 @@
+import * as React from "react";
+import { createPortal } from "react-dom";
+
+/**
+ * Modal sheet - the iOS pattern for a task that needs input.
+ *
+ * WHY THIS REPLACES `Popup`
+ * -------------------------
+ * `Popup` renders a card floating in the middle of the screen. On iPhone that
+ * is a popover, and Apple's Human Interface Guidelines reserve popovers for
+ * larger screens: on a phone they are expected to adapt into a sheet that
+ * slides up from the bottom edge. A centred card is also harder to reach with
+ * a thumb, and it gave no hint of how to get rid of it.
+ *
+ * The card was translucent on top of that (`--tile-2` is an rgba white in the
+ * night theme, meant for tile-on-tile layering) so page content showed through
+ * the dialog. The panel had already hit this and fixed it for its own sheet
+ * and setup dialog by switching to the opaque surface token; this component
+ * starts there.
+ *
+ * WHAT MAKES IT A SHEET
+ * ---------------------
+ * - anchored to the bottom edge, rounded top corners only
+ * - a grabber, so it reads as draggable and dismissible
+ * - a dimmed scrim: modal means the content behind is out of reach, and it
+ * should look that way
+ * - opaque surface, never see-through
+ * - the bottom safe area stays clear of the home indicator
+ * - Escape closes it, and so does a tap on the scrim
+ *
+ * `ActionSheet` is the sibling for a set of choices; use that one when the
+ * sheet is a list of buttons rather than a form.
+ */
+
+/**
+ * Portal target for both sheet components: the themed app root, never
+ * `document.body`. The colour tokens hang off `[data-theme]` on `.ads-root`;
+ * anything portalled outside it inherits the night palette even while the app
+ * shows the day one (measured 2026-08-31 - see Popup.tsx).
+ */
+export function sheetPortalTarget(portalTarget?: Element): Element | null {
+ if (portalTarget) return portalTarget;
+ if (typeof document === "undefined") return null;
+ return document.querySelector(".ads-root") ?? document.body;
+}
+
+/** Escape closes the topmost modal - expected wherever there is a keyboard. */
+export function useEscape(open: boolean, onClose: () => void): void {
+ React.useEffect(() => {
+ if (!open) return;
+ const beiTaste = (e: KeyboardEvent) => {
+ if (e.key === "Escape") onClose();
+ };
+ document.addEventListener("keydown", beiTaste);
+ return () => document.removeEventListener("keydown", beiTaste);
+ }, [open, onClose]);
+}
+
+export interface SheetProps {
+ open: boolean;
+ onClose: () => void;
+ /** Shown at the top of the sheet, above the content. */
+ title?: React.ReactNode;
+ /** One explanatory line under the title. */
+ description?: React.ReactNode;
+ children: React.ReactNode;
+ className?: string;
+ portalTarget?: Element;
+}
+
+export function Sheet({
+ open,
+ onClose,
+ title,
+ description,
+ children,
+ className,
+ portalTarget,
+}: SheetProps) {
+ useEscape(open, onClose);
+ const target = sheetPortalTarget(portalTarget);
+ if (!open || !target) return null;
+
+ const classes = ["ads-sheet"];
+ if (className) classes.push(className);
+
+ return createPortal(
+ <>
+
+
+
+ {(title || description) && (
+
+ {title && {title} }
+ {description && {description} }
+
+ )}
+
{children}
+
+ >,
+ target,
+ );
+}
diff --git a/design-system/src/components/Sheet/index.ts b/design-system/src/components/Sheet/index.ts
new file mode 100644
index 0000000..4aa5dbb
--- /dev/null
+++ b/design-system/src/components/Sheet/index.ts
@@ -0,0 +1,4 @@
+export { Sheet, sheetPortalTarget, useEscape } from "./Sheet";
+export type { SheetProps } from "./Sheet";
+export { ActionSheet } from "./ActionSheet";
+export type { ActionSheetProps, ActionSheetAktion } from "./ActionSheet";
diff --git a/design-system/src/index.ts b/design-system/src/index.ts
index d7f7924..54e41a6 100644
--- a/design-system/src/index.ts
+++ b/design-system/src/index.ts
@@ -46,6 +46,9 @@ export type { SwipeRowProps } from "./components/SwipeRow";
export { TabBar } from "./components/TabBar";
export type { TabBarProps, TabBarItem } from "./components/TabBar";
+export { Sheet, ActionSheet } from "./components/Sheet";
+export type { SheetProps, ActionSheetProps, ActionSheetAktion } from "./components/Sheet";
+
export { Popup, PopupMenuItem } from "./components/Popup";
export type { PopupProps, PopupMenuItemProps, PopupAnchor } from "./components/Popup";
diff --git a/design-system/src/styles.css b/design-system/src/styles.css
index 22a55c1..d1f10bf 100644
--- a/design-system/src/styles.css
+++ b/design-system/src/styles.css
@@ -18,5 +18,6 @@
@import "./components/Accordion/Accordion.css";
@import "./components/SwipeRow/SwipeRow.css";
@import "./components/TabBar/TabBar.css";
+@import "./components/Sheet/Sheet.css";
@import "./components/Popup/Popup.css";
@import "./components/ImagePlaceholder/ImagePlaceholder.css";
diff --git a/package-lock.json b/package-lock.json
index 90bd351..72c80f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,8 +19,12 @@
"@aparajita/capacitor-secure-storage": "^8.0.0",
"@audi-dash/ui": "*",
"@capacitor/android": "^8.5.0",
+ "@capacitor/app": "^8.1.1",
"@capacitor/core": "^8.5.0",
+ "@capacitor/filesystem": "^8.1.3",
"@capacitor/ios": "^8.5.0",
+ "@capacitor/preferences": "^8.0.1",
+ "@capacitor/share": "^8.0.1",
"@capgo/capacitor-updater": "^8.51.14",
"leaflet": "^1.9.4",
"react": "^18.3.1",
@@ -37,7 +41,8 @@
"jsdom": "^29.1.1",
"typescript": "^5.7.2",
"vite": "^6.0.7",
- "vitest": "^2.1.8"
+ "vitest": "^2.1.8",
+ "xcode": "^3.0.1"
}
},
"design-system": {
@@ -516,6 +521,18 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/@capacitor/filesystem": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-8.1.3.tgz",
+ "integrity": "sha512-GNBlRkaXEEb+6WP/YsDXr2zygyOHoieuye6viyd2ZkEdBwfseG41jxfarVxLWH0r0owhyNUbwJ/Qi8fubqr6wA==",
+ "license": "MIT",
+ "dependencies": {
+ "@capacitor/synapse": "^1.0.4"
+ },
+ "peerDependencies": {
+ "@capacitor/core": ">=8.0.0"
+ }
+ },
"node_modules/@capacitor/ios": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.5.0.tgz",
@@ -534,6 +551,30 @@
"@capacitor/core": ">=8.0.0"
}
},
+ "node_modules/@capacitor/preferences": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.1.tgz",
+ "integrity": "sha512-T6no3ebi79XJCk91U3Jp/liJUwgBdvHR+s6vhvPkPxSuch7z3zx5Rv1bdWM6sWruNx+pViuEGqZvbfCdyBvcHQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": ">=8.0.0"
+ }
+ },
+ "node_modules/@capacitor/share": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-8.0.1.tgz",
+ "integrity": "sha512-3cSBKBCJVon54rKDROP2rqGyeGks4pBh9TbaEk9S375Kbek/ZHe72N50zIa0Vn9Eac/SuhwgehO/mmA4CsUOiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@capacitor/core": ">=8.0.0"
+ }
+ },
+ "node_modules/@capacitor/synapse": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz",
+ "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
+ "license": "ISC"
+ },
"node_modules/@capgo/capacitor-updater": {
"version": "8.51.14",
"resolved": "https://registry.npmjs.org/@capgo/capacitor-updater/-/capacitor-updater-8.51.14.tgz",