Teilen-Blatt, HIG-Blaetter, geteilte Ansichtseinstellungen (2026.8.31.1)
Share-Erweiterung: ein PDF aus Mail landet ueber Teilen -> DataMetric360 als Tankbeleg. Alles Native liegt versioniert unter companion-app/native/, weil ios/ gitignored ist und npx cap add ios es sonst wieder frisst; scripts/ios-teilen-einrichten.mjs haengt es bei jedem Bau ins Xcode-Projekt und ist in ios-signieren.sh eingehaengt. Der pbxproj-Teil ist auf keinem Mac erprobt - er bricht vor dem Schreiben ab, und docs/SHARE_EXTENSION.md beschreibt dieselben Handgriffe fuer Xcode. "In den Kalender uebernehmen" ging in der App nie: WKWebView ignoriert das download-Attribut, der Klick lief ins Leere. Auf dem Geraet jetzt @capacitor/filesystem plus @capacitor/share. useTheme/useBoden legten je Aufrufer eigenen Zustand an - fuenf Kopien. Der Schalter aenderte nur seine eigene, data-theme in App.tsx blieb stehen, die Tag/Nacht-Umschaltung wirkte erst nach einem Neustart. Jetzt ein Speicher je Einstellung ueber useSyncExternalStore. Zwei Regressionstests, gegen den alten Stand als fehlschlagend nachgewiesen. Alle Popups nach Apple HIG: neue Bausteine Sheet und ActionSheet, blickdicht und mit abgedunkeltem Schleier, in beiden Codebasen. Popup portalierte nach document.body - ausserhalb von [data-theme] - und war deshalb im Tagmodus fast unsichtbar; Portal zielt jetzt auf .ads-root. ImagePlaceholder merkte sich "fehlgeschlagen" ohne die Adresse: nach einem leeren Galerieplatz zeigten auch die mit Foto nur noch den Platzhalter. Vier Regressionstests, ebenfalls gegen den alten Stand geprueft. Bildadressen tragen jetzt einen Cache-Brecher - HA liefert /local/ mit 31 Tagen Cache-Vorgabe aus, im Panel eingefuegte Radfotos erschienen in der App deshalb nicht. uebersichtsbild: Panel speichert den Namen, die App verglich gegen den Dateinamen - der Vergleich traf nie zu. Kanonisch ist der Name. Weiter: Navigationspfeil statt gleichschenkligem Dreieck auf der Streckenlinie (die Kerbe unterscheidet Kopf und Ende), CI-Symbol tour-s als Fahrten-Icon, Datumsfelder zeigen ihren Wert ohne erstes Antippen, feste Beispielnamen im Setup, Kopf-Gegengewicht fuer mittige Titel, Standort-Blatt mit festem Fussabstand, NSLocationWhenInUseUsageDescription, watchPosition fuer die Live-Ortung, Art-Pille wieder als Knopf, Zwischenablage fuer Tankbelege. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
{"version":"2026.8.30.20","sha256":"28839ee2ba8d1d703bb688a0124a2930ffb45b0d92acfc05d9c9504b7141c144","bytes":257018,"gebaut":"2026-08-30T22:05:13Z"}
|
||||
{"version":"2026.8.31.1","sha256":"bd990f600b5bf81ce66105af8d49a77d31a2a4e3b4e5070858c38e1e8d23fcc9","bytes":266016,"gebaut":"2026-08-31T12:23:41Z"}
|
||||
Binary file not shown.
@@ -573,6 +573,82 @@ function tiles() {
|
||||
if (TILE) MAP.removeLayer(TILE);
|
||||
TILE = window.L.tileLayer(url, { maxZoom: 19, attribution: ATTR }).addTo(MAP);
|
||||
}
|
||||
/* Fahrtrichtung auf der Streckenlinie.
|
||||
|
||||
Bis 2026-08-31 lagen auf der Linie kleine runde Punkte - je einer pro
|
||||
GPS-Stuetzpunkt. Sie sagten nichts ueber die Richtung, und bei einer Fahrt
|
||||
hin und zurueck auf derselben Strasse verschmolzen Hin- und Rueckweg zu
|
||||
einem einzigen roten Band: aus der Karte war nicht mehr abzulesen, dass es
|
||||
ueberhaupt zwei Wege waren.
|
||||
|
||||
Die Form ist der Navigationspfeil, kein gleichschenkliges Dreieck: das
|
||||
Dreieck sieht von beiden Seiten gleich aus und laesst auf Abstand offen,
|
||||
wo vorn ist - besonders dort, wo die Strecke die Richtung wechselt. Die
|
||||
Kerbe im Fuss macht Kopf und Ende unterscheidbar (Nutzervorgabe mit
|
||||
Vorlage, 2026-08-31).
|
||||
|
||||
Drei Entscheidungen, jede an Mustern nachgemessen (siehe unten):
|
||||
|
||||
1. Abstand nach STRECKE, nicht nach Stuetzpunkten. Die Punktdichte des
|
||||
Rohtracks haengt an der Geschwindigkeit - im Stau entsteht ein Haufen,
|
||||
auf der Autobahn eine Luecke. Pfeile daran haetten genau dort geklumpt,
|
||||
wo am wenigsten passiert ist.
|
||||
|
||||
2. Mittig auf der Linie, ohne Versatz quer zur Fahrtrichtung. Ein solcher
|
||||
Versatz war erprobt und trennt eine Hin-und-Rueck-Strecke sauber auf
|
||||
zwei Spuren - aber nur, wenn Hin- und Rueckweg wirklich Punkt fuer Punkt
|
||||
uebereinanderliegen. Das tun echte GPS-Aufzeichnungen nicht (Eigentum
|
||||
des Fahrzeughalters, 2026-08-31). Fuer einen Fall, der in den Daten
|
||||
nicht vorkommt, waere die schiefe Lage aller Dreiecke ein taeglicher
|
||||
Preis ohne Gegenwert.
|
||||
|
||||
3. Weisser Rand. Das Dreieck liegt in derselben Farbe auf derselben Linie;
|
||||
ohne die helle Kontur verschwimmt beides zu einer dicken Wurst. */
|
||||
function spurMeter(a, b) {
|
||||
const R = 6371000, r = Math.PI / 180;
|
||||
const dLat = (b[0] - a[0]) * r, dLon = (b[1] - a[1]) * r;
|
||||
const h = Math.sin(dLat / 2) ** 2 + Math.cos(a[0] * r) * Math.cos(b[0] * r) * Math.sin(dLon / 2) ** 2;
|
||||
return R * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
||||
}
|
||||
/* Winkel im Uhrzeigersinn ab Norden - genau das, was CSS rotate() erwartet.
|
||||
Die Laengendifferenz wird mit cos(Breite) gestaucht, sonst zeigten die
|
||||
Pfeile in unseren Breiten rund 30 Grad daneben. */
|
||||
function spurWinkel(a, b) {
|
||||
const dx = (b[1] - a[1]) * Math.cos(((a[0] + b[0]) / 2) * Math.PI / 180);
|
||||
return Math.atan2(dx, b[0] - a[0]) * 180 / Math.PI;
|
||||
}
|
||||
function spurPfeile(karte, spur, farbe) {
|
||||
if (!Array.isArray(spur) || spur.length < 2 || !window.L) return;
|
||||
const laengen = [];
|
||||
let gesamt = 0;
|
||||
for (let i = 1; i < spur.length; i++) { const d = spurMeter(spur[i - 1], spur[i]); laengen.push(d); gesamt += d; }
|
||||
if (!gesamt) return;
|
||||
// Rund alle 350 m, aber nie weniger als 4 und nie mehr als 14: auf einer
|
||||
// 200-km-Fahrt waeren 570 Pfeile eine zweite Linie, auf 300 m gar keiner.
|
||||
const anzahl = Math.max(4, Math.min(14, Math.round(gesamt / 350)));
|
||||
const schritt = gesamt / (anzahl + 1);
|
||||
let ziel = schritt, gelaufen = 0;
|
||||
for (let i = 1; i < spur.length && ziel < gesamt; i++) {
|
||||
const d = laengen[i - 1];
|
||||
while (ziel <= gelaufen + d && ziel < gesamt) {
|
||||
const f = d ? (ziel - gelaufen) / d : 0;
|
||||
const lat = spur[i - 1][0] + (spur[i][0] - spur[i - 1][0]) * f;
|
||||
const lon = spur[i - 1][1] + (spur[i][1] - spur[i - 1][1]) * f;
|
||||
const w = spurWinkel(spur[i - 1], spur[i]).toFixed(1);
|
||||
const html = '<div style="transform:rotate(' + w + 'deg)">'
|
||||
+ '<svg width="13" height="13" viewBox="0 0 10 10" style="overflow:visible">'
|
||||
+ '<path d="M5 0.4 L8.8 9.6 L5 6.4 L1.2 9.6 Z" fill="none" stroke="#fff" stroke-width="1.8" stroke-linejoin="round" opacity=".95"/>'
|
||||
+ '<path d="M5 0.4 L8.8 9.6 L5 6.4 L1.2 9.6 Z" fill="' + farbe + '"/></svg></div>';
|
||||
window.L.marker([lat, lon], {
|
||||
icon: window.L.divIcon({ className: "spur-pfeil", html, iconSize: [13, 13], iconAnchor: [6.5, 6.5] }),
|
||||
interactive: false,
|
||||
}).addTo(karte);
|
||||
ziel += schritt;
|
||||
}
|
||||
gelaufen += d;
|
||||
}
|
||||
}
|
||||
|
||||
async function initMap() {
|
||||
await leafletLaden();
|
||||
const el = ROOT.getElementById("map");
|
||||
@@ -591,7 +667,10 @@ async function initMap() {
|
||||
? t.route
|
||||
: [[t.start_lat, t.start_lon], [t.end_lat, t.end_lon]];
|
||||
const line = window.L.polyline(track, { color: rot, weight: 2.5, opacity: 0.9, dashArray: Array.isArray(t.route) && t.route.length >= 2 ? null : "4 6" }).addTo(MAP);
|
||||
track.slice(1, -1).forEach((pt) => window.L.circleMarker(pt, { radius: 2.5, color: rot, weight: 0, fillColor: rot, fillOpacity: 0.85 }).addTo(MAP));
|
||||
// Nur auf einer echten Aufzeichnung: die gestrichelte Ersatzlinie
|
||||
// zwischen Start und Ziel ist keine gefahrene Strecke, und ein Pfeil
|
||||
// darauf behauptete eine Fahrtrichtung, die niemand gemessen hat.
|
||||
if (Array.isArray(t.route) && t.route.length >= 2) spurPfeile(MAP, track, rot);
|
||||
window.L.circleMarker([t.start_lat, t.start_lon], { radius: 6, color: "#fff", weight: 2, fillColor: "#000", fillOpacity: 1 }).addTo(MAP).bindPopup("Start · " + (t.start_address || ""));
|
||||
window.L.circleMarker([t.end_lat, t.end_lon], { radius: 6, color: rot, weight: 2, fillColor: rot, fillOpacity: 1 }).addTo(MAP).bindPopup("Ziel · " + (t.end_address || ""));
|
||||
MAP.fitBounds(line.getBounds(), { padding: [26, 26] });
|
||||
@@ -637,6 +716,7 @@ async function initMap() {
|
||||
let USER_POS = null; // {lat, lon, genauigkeit} oder null
|
||||
let USER_POS_TS = 0;
|
||||
let USER_POS_FEHLER = null;
|
||||
let USER_WATCH = null; // watchPosition-Kennung, solange die Standortseite offen ist
|
||||
let STANDORT_ADRESSE = null; // zuletzt aufgelöste Adresse (Cache über Koordinaten-Rundung)
|
||||
let STANDORT_ADRESSE_KEY = null;
|
||||
let STANDORTMENU_OFFEN = false;
|
||||
@@ -779,6 +859,43 @@ function standortErfassenFallsNoetig() {
|
||||
standortErfassen();
|
||||
}
|
||||
|
||||
/* Fortlaufende Ortung, solange die Standortseite offen ist - "live bei
|
||||
Verwendung". Die einmalige Abfrage oben bleibt daneben bestehen und ist
|
||||
weiterhin das Richtige fuer die Uebersichtskachel: die zeigt nur einen
|
||||
Abstand, und eine dauerhafte Hochgenauigkeits-Ortung auf der Startseite
|
||||
waere Batterieverbrauch ohne Gegenwert.
|
||||
|
||||
Beendet wird die Verfolgung an denselben zwei Stellen, an denen auch SMAP
|
||||
abgeraeumt wird - sonst liefe sie nach dem Verlassen der Seite unbemerkt
|
||||
weiter. Genau das verlangt Apples Vorgabe fuer "Beim Verwenden der App":
|
||||
erfassen, solange der Nutzer die Karte ansieht, und keine Sekunde laenger. */
|
||||
function standortVerfolgungStarten() {
|
||||
if (USER_WATCH !== null) return;
|
||||
if (!navigator.geolocation) { USER_POS_FEHLER = "GPS offline"; standortAnsichtAktualisieren(); return; }
|
||||
USER_WATCH = navigator.geolocation.watchPosition(
|
||||
(pos) => {
|
||||
USER_POS = { lat: pos.coords.latitude, lon: pos.coords.longitude, genauigkeit: pos.coords.accuracy };
|
||||
USER_POS_TS = Date.now(); USER_POS_FEHLER = null;
|
||||
standortAnsichtAktualisieren();
|
||||
},
|
||||
(err) => {
|
||||
// Dieselbe Unterscheidung wie in standortErfassen(): die verweigerte
|
||||
// Freigabe ist der einzige Fall mit einer anderen Abhilfe.
|
||||
USER_POS_FEHLER = err.code === err.PERMISSION_DENIED ? "GPS-Freigabe fehlt" : "GPS offline";
|
||||
USER_POS_TS = Date.now();
|
||||
standortAnsichtAktualisieren();
|
||||
},
|
||||
// maximumAge klein: eine zwischengespeicherte Position aus der letzten
|
||||
// halben Minute waere hier das Gegenteil von "live".
|
||||
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 2000 }
|
||||
);
|
||||
}
|
||||
function standortVerfolgungBeenden() {
|
||||
if (USER_WATCH === null) return;
|
||||
navigator.geolocation.clearWatch(USER_WATCH);
|
||||
USER_WATCH = null;
|
||||
}
|
||||
|
||||
/* Reverse-Geocoding über die öffentliche Nominatim-API (OpenStreetMap) -
|
||||
wie die Kartenkacheln oben (TILE_URL) ohne eigenen Schlüssel; bei diesem
|
||||
Aufrufvolumen (ein Fahrzeug, nur bei geöffneter Standortansicht) im Rahmen
|
||||
@@ -961,7 +1078,11 @@ function standortMarkerZeichnen() {
|
||||
}
|
||||
function standortAufFahrzeug() { if (SMAP && standortBekannt()) SMAP.flyTo([CAR.standortLat, CAR.standortLon], Math.max(SMAP.getZoom(), 15)); }
|
||||
function standortAufUser() {
|
||||
if (!USER_POS) { standortErfassen(); return; }
|
||||
// Noch keine Position: die Verfolgung neu anstossen. Das ist zugleich der
|
||||
// Weg zurueck, wenn die Freigabe erst nachtraeglich erteilt wurde - ein
|
||||
// einmal gescheiterter watchPosition-Auftrag meldet sich nicht von selbst
|
||||
// wieder.
|
||||
if (!USER_POS) { standortVerfolgungBeenden(); standortVerfolgungStarten(); return; }
|
||||
if (SMAP) SMAP.flyTo([USER_POS.lat, USER_POS.lon], Math.max(SMAP.getZoom(), 15));
|
||||
}
|
||||
function standortBeideZeigen() {
|
||||
@@ -986,7 +1107,7 @@ async function initStandortMap() {
|
||||
standortMarkerZeichnen();
|
||||
standortBeideZeigen();
|
||||
standortAdresseLaden();
|
||||
standortErfassenFallsNoetig();
|
||||
standortVerfolgungStarten();
|
||||
setTimeout(() => SMAP && SMAP.invalidateSize(), 60);
|
||||
}
|
||||
async function initVorschauMap() {
|
||||
@@ -2254,7 +2375,7 @@ function vIdent() {
|
||||
<dl class="rows" style="margin-top:10px">
|
||||
<div class="row"><dt>Modell</dt><dd>${esc(CONFIG.fahrzeugtitel)}</dd></div>
|
||||
<div class="row"><dt>Ausführung</dt><dd>${CONFIG.ausfuehrung ? esc(CONFIG.ausfuehrung) : "noch offen"}</dd></div>
|
||||
<div class="row"><dt>Fahrgestellnummer</dt><dd>${esc(CAR.fin)}</dd></div>
|
||||
<div class="row"><dt>FIN</dt><dd>${esc(CAR.fin)}</dd></div>
|
||||
<div class="row"><dt>Motor und Leistung</dt><dd>${esc(CAR.details)}</dd></div>
|
||||
<div class="row"><dt>Erstzulassung</dt><dd>${esc(CAR.erstzulassung)}</dd></div>
|
||||
<div class="row"><dt>Kennzeichen</dt><dd>${esc(CONFIG.kennzeichen)}</dd></div>
|
||||
@@ -2763,7 +2884,7 @@ function vEinst() {
|
||||
</div>
|
||||
<div class="tile"><span class="label">Kraftstoff-Rabatt</span>
|
||||
<div class="feld"><label for="sdAktiv" style="display:flex;align-items:center;gap:6px">
|
||||
<img src="${STATIK}/bilder/${MARKENLOGO_DATEI}" alt="Shell" style="height:14px;width:auto" onerror="this.style.display='none'"> SmartDeal aktiv</label>
|
||||
<img src="${STATIK}/bilder/${MARKENLOGO_DATEI}" alt="Shell" style="height:16px;width:auto" onerror="this.style.display='none'"> SmartDeal aktiv</label>
|
||||
<label class="switch"><input id="sdAktiv" type="checkbox" data-sd ${CAR.smartdeal.aktiv ? "checked" : ""}><span class="track"></span></label></div>
|
||||
${CAR.smartdeal.aktiv ? `
|
||||
<div class="feld" style="border-bottom:0"><label>Gültig bis</label>
|
||||
@@ -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: '<path d="M5.46 4.05a80 80 0 0 1 13.08 0l.24.02.14.2A55 55 0 0 1 21.9 9.2l2.1 2.1v8.2c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V19l-14-.04v.54c0 .83-.67 1.5-1.5 1.5h-2A1.5 1.5 0 0 1 0 19.5v-8.2l2.1-2.1a55 55 0 0 1 2.98-4.93l.14-.2zM2.7 10 1 11.7v.3h4v1H1v6.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-1.55L20 18v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V13h-4v-1h4v-.3L21.3 10zm17.94-1a45 45 0 0 0-2.43-3.98 78 78 0 0 0-12.44 0L4.51 7c-.3.5-.7 1.16-1.16 2.01h17.3ZM7.3 12h9.42l1.36 1.36-.1.3c-.31 1-.6 1.78-.88 2.31-.13.27-.27.5-.41.67-.13.15-.36.36-.68.36H8a.9.9 0 0 1-.68-.36c-.14-.17-.28-.4-.41-.67a17 17 0 0 1-.89-2.32l-.09-.29zm.42 1-.64.64c.28.84.52 1.47.73 1.89a2.4 2.4 0 0 0 .3.47h7.8l.02-.01c.07-.08.16-.23.28-.46.21-.42.45-1.05.73-1.9L16.3 13H7.71Z"/>',
|
||||
trips: '<circle cx="6" cy="6" r="2.2"/><circle cx="18" cy="18" r="2.2"/><path d="M8 7.5c4 1.5 4.5 3 3 5s-1 3.5 4.5 5"/>',
|
||||
/* CI-Symbol tour-s statt der frueheren handgezeichneten Route - dieselbe
|
||||
Fuellflaeche-statt-Kontur-Behandlung wie "audi" und "stat" (siehe
|
||||
ICONS_VOLL). */
|
||||
trips: '<path d="M3.4 3C2.6 3 2 3.64 2 4.5c0 .16.08.46.27.89a15.5 15.5 0 0 0 1.23 2.2 18.4 18.4 0 0 0 1.23-2.2c.19-.43.27-.73.27-.89C5 3.68 4.32 3 3.5 3zm.1 5.5-.41.28-.03-.03a18 18 0 0 1-.3-.45l-.7-1.14a14 14 0 0 1-.7-1.37A3.5 3.5 0 0 1 1 4.5C1 3.16 2 2 3.4 2h.1A2.5 2.5 0 0 1 6 4.5c0 .39-.17.86-.35 1.29a27 27 0 0 1-1.71 2.96l-.02.02v.01zm0 0 .41.28-.41.6-.41-.6zM12.4 3c-.8 0-1.4.64-1.4 1.5 0 .16.08.46.27.89a15.5 15.5 0 0 0 1.23 2.2 18.4 18.4 0 0 0 1.23-2.2c.19-.43.27-.73.27-.89 0-.82-.68-1.5-1.5-1.5zm.1 5.5-.41.28-.03-.03a12 12 0 0 1-.3-.45l-.7-1.14c-.25-.44-.5-.92-.7-1.37A3.5 3.5 0 0 1 10 4.5C10 3.16 11 2 12.4 2h.1A2.5 2.5 0 0 1 15 4.5c0 .39-.17.86-.35 1.29a27 27 0 0 1-1.71 2.96l-.02.02v.01zm0 0 .41.28-.41.6-.41-.6zm5.79-.5h.21a4.5 4.5 0 0 1 4.5 4.5c0 .66-.3 1.52-.67 2.38-.38.88-.9 1.83-1.4 2.7a54 54 0 0 1-1.97 3.14l-.04.05v.01l-.42-.28-.41.28-.01-.01-.04-.05a40 40 0 0 1-.58-.89 33 33 0 0 1-2.8-4.95A6.6 6.6 0 0 1 14 12.5 4.34 4.34 0 0 1 18.28 8Zm.21 12.5-.41.28.41.6.41-.6zm0-.9a53 53 0 0 0 1.57-2.53c.5-.85.98-1.76 1.35-2.6s.58-1.53.58-1.97c0-1.92-1.58-3.5-3.5-3.5h-.19c-1.94.1-3.4 1.58-3.31 3.48v.02c0 .44.2 1.13.58 1.98.37.83.86 1.74 1.35 2.6a53 53 0 0 0 1.57 2.52M9.94 11H3v-1h10.06l-7.61 5.44 13.29 7.12-.48.88-14.7-7.88L9.93 11Z"/>',
|
||||
/* 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 = `<svg viewBox="0 0 20 20" width="16" height="16" fill="none" st
|
||||
|
||||
/* Gemeinsames Präfix zweier domain-freier IDs, bis zum letzten gemeinsamen
|
||||
"_" - oder null, wenn sie nicht einmal ein volles Segment teilen. */
|
||||
function praefixPaar(a, b) {
|
||||
let i = 0;
|
||||
while (i < a.length && i < b.length && a[i].toLowerCase() === b[i].toLowerCase()) i++;
|
||||
const idxUnter = a.slice(0, i).lastIndexOf("_");
|
||||
return idxUnter > 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
|
||||
? `<span class="setup-feld-hinweis">(z. B. <span class="setup-feld-einheit">${esc(erwartet)}</span>)</span>`
|
||||
: "";
|
||||
@@ -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 {
|
||||
<button class="back" id="back" aria-label="Zurück"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="m10.21 12 5.12-5.08-.7-.71L8.79 12l5.84 5.8.7-.72L10.22 12Z"/></svg></button> <button type="button" class="rings" id="marke" aria-label="Einstellungen"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 289 100" fill="currentColor" aria-hidden="true"><defs><clipPath id="audiRingsClip" clipPathUnits="userSpaceOnUse"><path d="M0 100h289V0H0z"/></clipPath></defs><g clip-path="url(#audiRingsClip)" transform="matrix(1 0 0 -1 0 100.00040017)"><path d="M239 11.1914c-8.885 0-17.075 3.008-23.624 8.05 6.646 8.489 10.624 19.165 10.624 30.759 0 11.593-3.978 22.269-10.624 30.759 6.549 5.041 14.739 8.05 23.624 8.05 21.399 0 38.809-17.41 38.809-38.809 0-21.4-17.41-38.809-38.809-38.809m-86.625 8.05c6.647 8.489 10.625 19.165 10.625 30.759 0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.885 0 17.075-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.549-5.042-14.739-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-63 0C96.022 27.7304 100 38.4064 100 50.0004c0 11.593-3.978 22.269-10.624 30.759 6.548 5.041 14.74 8.05 23.624 8.05 8.884 0 17.076-3.009 23.624-8.05-6.646-8.49-10.624-19.166-10.624-30.759 0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-8.884 0-17.076 3.008-23.625 8.05m-78.184 30.759c0 21.399 17.41 38.809 38.809 38.809 8.884 0 17.076-3.009 23.624-8.05C66.978 72.2694 63 61.5934 63 50.0004c0-11.594 3.978-22.27 10.624-30.759-6.548-5.042-14.74-8.05-23.624-8.05-21.399 0-38.809 17.409-38.809 38.809M81.5 27.3734c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m63 0c-4.593 6.375-7.309 14.187-7.309 22.627 0 8.439 2.716 16.252 7.309 22.627 4.593-6.375 7.309-14.188 7.309-22.627 0-8.44-2.716-16.252-7.309-22.627m31.5 72.627c-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.568 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-11.932 0-22.896-4.207-31.5-11.207-8.604 7-19.569 11.207-31.5 11.207-27.57 0-50-22.43-50-50 0-27.571 22.43-50 50-50 11.931 0 22.896 4.206 31.5 11.207C90.104 4.2064 101.068.0004 113 .0004c11.931 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 11.932 0 22.896 4.206 31.5 11.207 8.604-7.001 19.568-11.207 31.5-11.207 27.57 0 50 22.429 50 50 0 27.57-22.43 50-50 50"/></g></svg></button>
|
||||
<div class="head"><div class="eyebrow" id="eyebrow"></div><div class="title" id="title">Übersicht</div>
|
||||
<div class="sync" id="sync"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="m18.65 4.95 2.56 2.55-2.56 2.55-.7-.7L19.29 8H7.2A5.24 5.24 0 0 0 2 13.38V14H1v-.6A6.24 6.24 0 0 1 7.2 7h12.1l-1.34-1.35.7-.7ZM23 11.59A6.24 6.24 0 0 1 16.8 18H4.7l1.35 1.35-.7.7-2.56-2.55 2.56-2.55.7.7L4.71 17H16.8a5.24 5.24 0 0 0 5.2-5.38V11h1v.6Z"/></svg><span></span></div></div>
|
||||
<span class="kopfgegengewicht" aria-hidden="true"></span>
|
||||
<button class="themebtn" id="theme" hidden aria-hidden="true"><svg id="themeicon" viewBox="0 0 20 20" width="15" height="15"></svg></button>
|
||||
<button class="profilbtn" id="ringsbtn" aria-label="Einstellungen"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 0 0 .33 1.76l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.6 1.6 0 0 0-1.76-.33 1.6 1.6 0 0 0-1 1.47V21a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-1-1.47 1.6 1.6 0 0 0-1.77.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.6 1.6 0 0 0 4.6 15a1.6 1.6 0 0 0-1.47-1H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.47-1 1.6 1.6 0 0 0-.33-1.77l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.6 1.6 0 0 0 9 4.6 1.6 1.6 0 0 0 10 3.13V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 1 1.47 1.6 1.6 0 0 0 1.77-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.6 1.6 0 0 0 19.4 9v.04a1.6 1.6 0 0 0 1.47 1H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z"/></svg></button>
|
||||
</div>
|
||||
|
||||
@@ -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 <img> und am Platzhalter-<div> (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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user