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 <[email protected]>
This commit is contained in:
@@ -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<string | null>(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 (
|
||||
<div className={classes.join(" ")}>
|
||||
{!showPlaceholder && (
|
||||
<img src={src} alt={alt} onError={() => setFailed(true)} />
|
||||
<img src={src} alt={alt} onError={() => setzeFehlgeschlagen(src)} />
|
||||
)}
|
||||
{showPlaceholder && (
|
||||
<div className="ads-platzhalter">
|
||||
|
||||
@@ -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 <button> 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* `<button data-art>`, das zwischen Privat und Arbeitsweg umschaltet. Ohne
|
||||
* diesen Zweig war sie in der App ein totes `<span>` - dieselbe Optik, aber
|
||||
* ohne die Bedienung, was von aussen wie ein Fehler aussieht und es de facto
|
||||
* auch war.
|
||||
*
|
||||
* Fehlt onClick, bleibt es beim `<span>`: 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 (
|
||||
<button type="button" className={classes.join(" ")} onClick={onClick} aria-label={ariaLabel}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <span className={classes.join(" ")}>{children}</span>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
<>
|
||||
<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,
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(
|
||||
<>
|
||||
<div className="ads-sheet-scrim" onClick={onClose} />
|
||||
<div className={classes.join(" ")} role="dialog" aria-modal="true">
|
||||
<span className="ads-sheet__griff" aria-hidden="true" />
|
||||
{(title || description) && (
|
||||
<div className="ads-sheet__kopf">
|
||||
{title && <strong>{title}</strong>}
|
||||
{description && <span>{description}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="ads-sheet__inhalt">{children}</div>
|
||||
</div>
|
||||
</>,
|
||||
target,
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user