b1411625aa
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>
99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
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(
|
|
<>
|
|
<div className="ads-sheet-scrim" onClick={onClose} />
|
|
<div className={classes.join(" ")} role="dialog" aria-modal="true">
|
|
<div className="ads-aktionsblatt__gruppe">
|
|
{(title || description) && (
|
|
<div className="ads-aktionsblatt__kopf">
|
|
{title && <strong>{title}</strong>}
|
|
{description && <span>{description}</span>}
|
|
</div>
|
|
)}
|
|
{children && <div className="ads-aktionsblatt__zusatz">{children}</div>}
|
|
{actions.map((aktion, i) => (
|
|
<button
|
|
key={i}
|
|
type="button"
|
|
className={aktion.destructive ? "ads-aktionsblatt__loeschen" : undefined}
|
|
disabled={aktion.disabled}
|
|
onClick={aktion.onClick}
|
|
>
|
|
{aktion.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{cancelLabel !== null && (
|
|
<button type="button" className="ads-aktionsblatt__abbrechen" onClick={onClose}>
|
|
{cancelLabel}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</>,
|
|
target,
|
|
);
|
|
}
|