@delacour/react-native-ui 0.1.0-alpha.20261006134908 → 0.1.0-alpha.20261006140500

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delacour/react-native-ui",
3
- "version": "0.1.0-alpha.20261006134908",
3
+ "version": "0.1.0-alpha.20261006140500",
4
4
  "description": "React Native UI components — Uniwind, Reanimated, Gesture Handler",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -70,6 +70,7 @@
70
70
  "./tabs": "./src/components/tabs/index.ts",
71
71
  "./text": "./src/components/text/index.ts",
72
72
  "./textarea": "./src/components/textarea/index.ts",
73
+ "./toast": "./src/components/toast/index.ts",
73
74
  "./toggle-button": "./src/components/toggle-button/index.ts",
74
75
  "./expo/navigation-theme": "./src/expo/navigation-theme.tsx",
75
76
  "./hooks/use-calm-motion": "./src/hooks/use-calm-motion.tsx",
@@ -101,8 +102,8 @@
101
102
  },
102
103
  "peerDependencies": {
103
104
  "@central-icons-react-native/round-outlined-radius-1-stroke-1.5": "^1.1",
104
- "@delacour/react-native-bottom-sheet": "0.1.0-alpha.20261006134908",
105
- "@delacour/react-native-charts": "0.1.0-alpha.20261006134908",
105
+ "@delacour/react-native-bottom-sheet": "0.1.0-alpha.20261006140500",
106
+ "@delacour/react-native-charts": "0.1.0-alpha.20261006140500",
106
107
  "@legendapp/list": ">=3.3",
107
108
  "expo-linear-gradient": ">=15",
108
109
  "expo-router": ">=57",
@@ -13,7 +13,8 @@ plus `Alert.Indicator`, `Alert.Content`, `Alert.Title`, `Alert.Description`,
13
13
  | --- | --- |
14
14
  | `index.ts` | → `@delacour/react-native-ui/alert` |
15
15
  | `alert.tsx` | Root + the `Object.assign` compound surface |
16
- | `alert-indicator.tsx` | `Alert.Indicator`, and the status → glyph map |
16
+ | `alert-indicator.tsx` | `Alert.Indicator` |
17
+ | `alert-glyphs.ts` | **Leaf.** `ALERT_GLYPHS`, the status → glyph map — `toast/` imports it |
17
18
  | `alert-content.tsx` | `Alert.Content` |
18
19
  | `alert-title.tsx` | `Alert.Title` |
19
20
  | `alert-description.tsx` | `Alert.Description` |
@@ -66,7 +67,7 @@ plus `Alert.Indicator`, `Alert.Content`, `Alert.Title`, `Alert.Description`,
66
67
  - **Glyphs differ in shape, not just colour.** `warning` is a triangle and
67
68
  `destructive` a circle, so the two stay apart for anyone who cannot tell amber
68
69
  from red. `default` shares `info`'s circle. The map lives in
69
- `alert-indicator.tsx` because the glyphs are RN SVG components and
70
+ `alert-glyphs.ts`, a leaf `Toast` shares, because the glyphs are RN SVG components and
70
71
  `alert.variants.ts` must stay importable from `bun test`.
71
72
  - **The indicator is hidden from assistive technology.** The title says what
72
73
  happened; "image" announced before it adds nothing. Children replace the
@@ -0,0 +1,25 @@
1
+ import { IconCircleCheck, IconCircleInfo, IconExclamationCircle, IconExclamationTriangle } from "../../icons/central";
2
+ import type { IconComponent } from "../icon";
3
+ import type { AlertStatus } from "./alert.variants";
4
+
5
+ /**
6
+ * The glyph each status draws.
7
+ *
8
+ * `warning` and `destructive` differ in shape as well as colour — a triangle
9
+ * against a circle — so the two stay distinguishable to anyone who cannot tell
10
+ * amber from red. `default` shares `info`'s glyph: a neutral note is still a
11
+ * note, and a status of its own would need a meaning it does not have.
12
+ *
13
+ * A leaf of its own rather than a constant in `alert-indicator.tsx`: `Toast`
14
+ * draws the same glyph per status, so the two read as one family, and it may
15
+ * import a leaf across folders where it may not import a part (package rule 3).
16
+ * Not in `alert.variants.ts` because the glyphs are React Native SVG components,
17
+ * and that file must stay importable from `bun test`.
18
+ */
19
+ export const ALERT_GLYPHS: Record<AlertStatus, IconComponent> = {
20
+ default: IconCircleInfo,
21
+ info: IconCircleInfo,
22
+ success: IconCircleCheck,
23
+ warning: IconExclamationTriangle,
24
+ destructive: IconExclamationCircle,
25
+ };
@@ -1,28 +1,9 @@
1
1
  import type { ReactElement, ReactNode } from "react";
2
2
  import { View, type ViewProps } from "react-native";
3
- import { IconCircleCheck, IconCircleInfo, IconExclamationCircle, IconExclamationTriangle } from "../../icons/central";
4
- import { Icon, type IconComponent } from "../icon";
3
+ import { Icon } from "../icon";
5
4
  import { useAlertPart } from "./alert.context";
6
- import { type AlertStatus, alertVariants } from "./alert.variants";
7
-
8
- /**
9
- * The glyph each status draws.
10
- *
11
- * `warning` and `destructive` differ in shape as well as colour — a triangle
12
- * against a circle — so the two stay distinguishable to anyone who cannot tell
13
- * amber from red. `default` shares `info`'s glyph: a neutral note is still a
14
- * note, and a status of its own would need a meaning it does not have.
15
- *
16
- * Lives here rather than in `alert.variants.ts` because the glyphs are React
17
- * Native SVG components, and that file must stay importable from `bun test`.
18
- */
19
- const ALERT_GLYPHS: Record<AlertStatus, IconComponent> = {
20
- default: IconCircleInfo,
21
- info: IconCircleInfo,
22
- success: IconCircleCheck,
23
- warning: IconExclamationTriangle,
24
- destructive: IconExclamationCircle,
25
- };
5
+ import { alertVariants } from "./alert.variants";
6
+ import { ALERT_GLYPHS } from "./alert-glyphs";
26
7
 
27
8
  export type AlertIndicatorProps = ViewProps & {
28
9
  className?: string;
@@ -0,0 +1,140 @@
1
+ # Toast
2
+
3
+ A brief, non-blocking message about something that just happened — "Link copied", "Saved",
4
+ "Upload failed — Retry" — shown from anywhere, including outside React, and gone on its own.
5
+ Anything that must be read or answered is an `Alert` or a `Dialog`, not a toast.
6
+
7
+ `import { Toast, ToastViewport, toast, useToast } from "@delacour/react-native-ui/toast";`
8
+
9
+ Three pieces: `toast` (a module-level function that writes to a store), `<ToastViewport />` (mounted
10
+ once, it draws the store) and `<Toast>` (the card, compound, also usable inside a custom `render`).
11
+
12
+ ## Files
13
+
14
+ | File | What it holds |
15
+ | --- | --- |
16
+ | `index.ts` | → `@delacour/react-native-ui/toast` |
17
+ | `toast.tsx` | `Toast` — the card root, one accessible element; the `Object.assign` names the parts |
18
+ | `toast-indicator.tsx` | `Toast.Indicator` — Alert's glyph for the status (`alert/alert-glyphs.ts`, a leaf) |
19
+ | `toast-content.tsx` | `Toast.Content` |
20
+ | `toast-title.tsx` | `Toast.Title` — coloured from Alert's token |
21
+ | `toast-description.tsx` | `Toast.Description` — muted |
22
+ | `toast-action.tsx` | `Toast.Action` — compact text button; registers itself as the card's `activate` |
23
+ | `toast-close.tsx` | `Toast.Close` — the ✕ |
24
+ | `toast.context.tsx` | **Leaf.** The card's context (`status`, `hide`, `registerAction`) and the viewport's per-item context |
25
+ | `toast.store.ts` | **Pure.** Types, `reduceToasts`, `normalizeToastInput`, `createToastStore`, `createToastApi` |
26
+ | `toast.timer.ts` | **Pure.** The pausable auto-hide clock |
27
+ | `toast.variants.ts` | **Pure.** The slotted `tv()`, the constants and every resolver — duration, stack, depth, stacked height, stagger, drag, release, haptic, announcement |
28
+ | `toast-api.ts` | `toastStore`, `toast`, `useToast` |
29
+ | `toast-viewport.tsx` | `ToastViewport` — two stacks in the overlay `toast` band; insets, keyboard, `AppState`, screen reader |
30
+ | `toast-item.tsx` | One drawn toast: entrance, depth, exit, clock, swipe, announcement, haptic |
31
+ | `*.test.ts` | The store, the clock and the resolvers |
32
+
33
+ ## Design
34
+
35
+ - **`toast` is a function on a module-level store, not a hook.** A toast's most common caller is
36
+ not a component — an API client, a mutation's `onError` — so `toast("Saved")` has to work with no
37
+ provider and no hook. `toast-api.ts` holds the one store; `ToastViewport` and `useToast` read it
38
+ with `useSyncExternalStore`. The store is `createToastStore()`, so a test (or an app wanting two
39
+ viewports) can make its own and hand it to `<ToastViewport store>`.
40
+ - **The store is a pure reducer.** `add | update | hide | remove | hideAll`. `hide` only marks a toast
41
+ `isExiting`, so the viewport can animate it out; `remove` drops it once the exit has run, and is
42
+ the moment `onHide` fires. An action that changes nothing returns the same array, so nobody is
43
+ notified. Showing with an existing `id` replaces that toast in place — and revives it if it was
44
+ leaving.
45
+ - **`revision` is how an update reaches the screen.** Every update bumps it; the item restarts its
46
+ clock, plays the haptic and announces again on a new revision. That is what makes
47
+ `toast.promise` one toast: shown loading (a spinner for the glyph, `duration` 0), then updated in
48
+ place to success or failure with a full duration, a haptic and a fresh announcement. `promise`
49
+ returns the caller's promise, so `await` still sees the rejection.
50
+ - **`isLoading` is an option, beyond the four statuses.** A loading toast is not a fifth status — it
51
+ says nothing about how things went — so it is a flag: a `Spinner` replaces the glyph, the ✕ is
52
+ dropped, and the toast stays until updated unless `duration` says otherwise.
53
+ - **Statuses are Alert's.** `TOAST_STATUSES` *is* `ALERT_STATUSES` and the title reads
54
+ `ALERT_FOREGROUND_TOKEN` (a test pins each class), and the glyph is Alert's — moved to the
55
+ `alert-glyphs.ts` leaf for this. A toast and an alert saying the same thing look like one family.
56
+ `toast.error` sets `destructive`, the token's name.
57
+ - **The card is a `popover` surface with a hairline and no shadow**, like every surface here, capped
58
+ at 560pt and centred so it stays a toast on a tablet.
59
+ - **Durations: 4000, 6000 with an action, `0` until hidden, ≥10000 under a screen reader.** Reaching
60
+ for a button takes longer than reading. A timed message must be readable in full before it leaves
61
+ (WCAG 2.2.1), so `resolveToastDuration` stretches any timed toast to ten seconds while VoiceOver or
62
+ TalkBack runs; `0` stays `0`.
63
+ - **The clock is data and pauses exactly.** `toast.timer.ts` keeps `{ remaining, startedAt }` and
64
+ every function takes `now`. The item runs one `setTimeout` per run and pauses — keeping what is
65
+ left — while the app is not `active` and while a finger is on the toast (the pan's `onBegin`, which
66
+ fires on touch-down, before the pan activates, so pressing the action pauses too).
67
+ - **Three drawn per edge, the rest queued.** `resolveToastStack` walks newest first; depth counts only
68
+ toasts that are staying, so when one is hidden the ones behind move up and the next queued toast is
69
+ drawn immediately while the hidden one fades out at the depth it left. Each older toast sits 8pt
70
+ further toward its entry edge, `1 − 0.05·depth` scale and `1 − 0.25·depth` opacity, scaled about
71
+ that edge (`transformOrigin`) so the peek is exactly 8pt. Depth is animated, and linear, so moving
72
+ up is smooth.
73
+ - **A toast behind takes the front toast's height.** Scaled about the edge, a taller older toast
74
+ rose past the front card and showed its title above it — seen on the simulator with a one-line
75
+ toast in front of a two-line one. The front item publishes its card height to a per-stack shared
76
+ value; `resolveToastStackedHeight` gives the ones behind that height, clipped from the centre side
77
+ so their edge-side border still peeks, and eases back to their own height as they move to the
78
+ front. The card is measured inside the clip, so the clip never feeds back into its own size.
79
+ - **Toasts shown in one tick enter 220ms apart.** The store numbers toasts created before a microtask
80
+ runs (`batchIndex`); `resolveToastEnterDelay` counts from `createdAt`, so a toast that sat in the
81
+ queue enters as soon as it is drawn rather than paying its stagger late.
82
+ - **Swipe toward the entry edge or sideways dismisses; toward the centre rubber-bands.** Past 40% of
83
+ the toast's own height or width, or a fling over 800pt/s. Toward the centre it follows the finger
84
+ to at most 24pt and never dismisses — that is not where a toast goes. The pan activates only past
85
+ 10pt, so a tap still reaches the action and the ✕. Under reduce motion the entrance is a fade; the
86
+ drag still follows the finger, which is direct manipulation, not decoration.
87
+ - **The viewport is `box-none` all the way down.** Two absolute stacks in `Overlay.Portal
88
+ layer="toast"` — above every sheet, dialog and popover — and only a card takes a touch, so the app
89
+ under the viewport stays tappable. It renders nothing while the store is empty, which keeps it out
90
+ of the overlay registry. The top stack sits `insets.top + 8` down; the bottom one `insets.bottom +
91
+ 8` up and lifts with the keyboard by the part of the keyboard above the home-indicator inset.
92
+ `offset` adds to either, to clear a tab bar.
93
+ - **One accessible element, never focused.** The card is `accessible`, so VoiceOver and TalkBack read
94
+ the title and description as one message; the action is its `activate` action (registered by
95
+ `Toast.Action` under its label) and the ✕ is `escape` plus a custom "Dismiss". `role="alert"` for a
96
+ failure, a polite live region on Android. On show, and on every revision, the item announces
97
+ `title. description` — on iOS a warning or failure with `queue: false`, so it cuts in. A toast
98
+ never moves focus.
99
+ - **Haptics follow the status.** Success, warning and destructive play their notification preset
100
+ (`playHaptic`, on the UI thread); default and info are silent; `haptic` overrides, `false` silences.
101
+ - **No back button.** The registry's `isTopOverlay` ignores the `toast` band, so Android back goes to
102
+ whatever is under the toasts.
103
+ - **Custom toasts compose `<Toast>`.** `render(handle)` draws the whole card; a `<Toast>` inside it
104
+ reads the viewport's per-item context and hides the right toast with no `onHide`. Outside the
105
+ viewport — a static preview — pass `onHide` yourself.
106
+
107
+ ## Mounting
108
+
109
+ ```tsx
110
+ <DelacourProvider>
111
+ <OverlayProvider>
112
+ <BottomSheetProvider>
113
+ <Stack />
114
+ <ToastViewport />
115
+ </BottomSheetProvider>
116
+ </OverlayProvider>
117
+ </DelacourProvider>
118
+ ```
119
+
120
+ `apps/playground/src/app/_layout.tsx` is the reference mount.
121
+
122
+ ## Testing
123
+
124
+ `bun test` reaches the reducer, the store's batching, the api (including `promise` both ways), the
125
+ clock and every resolver. Motion, gestures, insets, the keyboard and announcements are verified on a
126
+ simulator through `/toast`.
127
+
128
+ ## Preview capture (not yet shot)
129
+
130
+ No demo is marked `capture` — the capture tool is unavailable (see `docs/plans/overlays/README.md`).
131
+ The flows are in `.argent/flows/previews/toast/`. When it is back, set:
132
+
133
+ | Demo | `capture` |
134
+ | --- | --- |
135
+ | `statuses` | `{ flow: "toast/statuses", frame: "device", hero: true }` |
136
+ | `with-action` | `{ flow: "toast/with-action", frame: "device" }` |
137
+ | `placement` | `{ flow: "toast/placement", frame: "device" }` |
138
+ | `promise` | `{ flow: "toast/promise", frame: "device" }` |
139
+
140
+ `frame: "device"` because a toast draws in a portal, outside the demo's measured stage.
@@ -0,0 +1,74 @@
1
+ export { Toast, type ToastProps } from "./toast";
2
+ export {
3
+ type ToastContextValue,
4
+ type ToastItemContextValue,
5
+ useToastContext,
6
+ useToastItemContext,
7
+ } from "./toast.context";
8
+ export {
9
+ createToastApi,
10
+ createToastStore,
11
+ normalizeToastInput,
12
+ reduceToasts,
13
+ type ToastActionOptions,
14
+ type ToastApi,
15
+ type ToastCustomItem,
16
+ type ToastCustomOptions,
17
+ type ToastHandle,
18
+ type ToastInput,
19
+ type ToastItem,
20
+ type ToastMessageItem,
21
+ type ToastOptions,
22
+ type ToastPatch,
23
+ type ToastPlacement,
24
+ type ToastPromiseMessages,
25
+ type ToastState,
26
+ type ToastStatus,
27
+ type ToastStore,
28
+ type ToastStoreAction,
29
+ } from "./toast.store";
30
+ export {
31
+ createToastTimer,
32
+ isToastTimerExpired,
33
+ isToastTimerRunning,
34
+ pauseToastTimer,
35
+ resumeToastTimer,
36
+ startToastTimer,
37
+ type ToastTimer,
38
+ toastTimerRemaining,
39
+ } from "./toast.timer";
40
+ export {
41
+ resolveToastAnnouncement,
42
+ resolveToastDepthStyle,
43
+ resolveToastDrag,
44
+ resolveToastDuration,
45
+ resolveToastEnterDelay,
46
+ resolveToastHaptic,
47
+ resolveToastInterrupts,
48
+ resolveToastRelease,
49
+ resolveToastRole,
50
+ resolveToastStack,
51
+ resolveToastStackedHeight,
52
+ TOAST_DURATION,
53
+ TOAST_ENTER_DISTANCE,
54
+ TOAST_FOREGROUND_TOKEN,
55
+ TOAST_PLACEMENTS,
56
+ TOAST_RUBBER_BAND,
57
+ TOAST_STACK,
58
+ TOAST_STAGGER_MS,
59
+ TOAST_STATUSES,
60
+ TOAST_SWIPE,
61
+ type ToastRelease,
62
+ type ToastStackEntry,
63
+ type ToastStackInput,
64
+ type ToastVariantProps,
65
+ toastVariants,
66
+ } from "./toast.variants";
67
+ export type { ToastActionProps } from "./toast-action";
68
+ export { toast, toastStore, useToast } from "./toast-api";
69
+ export type { ToastCloseProps } from "./toast-close";
70
+ export type { ToastContentProps } from "./toast-content";
71
+ export type { ToastDescriptionProps } from "./toast-description";
72
+ export type { ToastIndicatorProps } from "./toast-indicator";
73
+ export type { ToastTitleProps } from "./toast-title";
74
+ export { ToastViewport, type ToastViewportProps } from "./toast-viewport";
@@ -0,0 +1,59 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useEffect } from "react";
2
+ import { Pressable, type PressableProps } from "../pressable";
3
+ import { Text } from "../text";
4
+ import { useToastPart } from "./toast.context";
5
+ import { toastVariants } from "./toast.variants";
6
+
7
+ export type ToastActionProps = Omit<PressableProps, "asChild" | "busy" | "children"> & {
8
+ /** The label. A string is set in the action's type; anything else is drawn as given. */
9
+ children: ReactNode;
10
+ };
11
+
12
+ /**
13
+ * The toast's one action — "Undo", "Retry", "View".
14
+ *
15
+ * A compact text button in the primary colour. Pressing it runs `onPress` and
16
+ * then hides the toast: an action answered is a message finished with.
17
+ *
18
+ * The toast is one accessible element, so this control is not reachable on its
19
+ * own with a screen reader. It registers itself with the root instead, which
20
+ * exposes it as the group's `activate` action under this label.
21
+ */
22
+ export function ToastAction({
23
+ accessibilityLabel,
24
+ children,
25
+ className,
26
+ feedback = "fade",
27
+ onPress,
28
+ ...props
29
+ }: ToastActionProps): ReactElement {
30
+ const { hide, registerAction } = useToastPart("Toast.Action");
31
+ const label = accessibilityLabel ?? (typeof children === "string" ? children : "Action");
32
+
33
+ const run = useCallback(() => {
34
+ onPress?.();
35
+ hide();
36
+ }, [onPress, hide]);
37
+
38
+ useEffect(() => {
39
+ registerAction({ label, run });
40
+ return () => registerAction(null);
41
+ }, [registerAction, label, run]);
42
+
43
+ const slots = toastVariants();
44
+
45
+ return (
46
+ <Pressable
47
+ accessibilityLabel={label}
48
+ accessibilityRole="button"
49
+ className={slots.action({ className })}
50
+ feedback={feedback}
51
+ hitSlop={6}
52
+ onPress={run}
53
+ {...props}
54
+ >
55
+ {typeof children === "string" ? <Text className={slots.actionLabel()}>{children}</Text> : children}
56
+ </Pressable>
57
+ );
58
+ }
59
+ ToastAction.displayName = "DelacourUI.Toast.Action";
@@ -0,0 +1,32 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { createToastApi, createToastStore, type ToastApi, type ToastState } from "./toast.store";
3
+
4
+ /**
5
+ * The one store every `toast()` call writes to and the viewport reads.
6
+ *
7
+ * Module scope on purpose: `toast` has to work where no hook can be called —
8
+ * an API client, a mutation's `onError`, a plain function in a service module.
9
+ */
10
+ export const toastStore = createToastStore();
11
+
12
+ /**
13
+ * Shows a toast from anywhere, inside React or out.
14
+ *
15
+ * Call it with a title, with options, or with a `render` for a custom card.
16
+ * `toast.success` / `.info` / `.warning` / `.error` set the status;
17
+ * `toast.update`, `.hide` and `.hideAll` act on what is shown; `toast.promise`
18
+ * shows one toast that turns into the success or the failure in place.
19
+ * Nothing appears until a `<ToastViewport />` is mounted.
20
+ *
21
+ * @example
22
+ * toast("Link copied");
23
+ * toast.error("Upload failed", { action: { label: "Retry", onPress: retry } });
24
+ * await toast.promise(save(), { loading: "Saving…", success: "Saved", error: "Could not save" });
25
+ */
26
+ export const toast: ToastApi = createToastApi(toastStore);
27
+
28
+ /** `toast`, and every toast in the store — shown, queued and leaving — oldest first. */
29
+ export function useToast(): { toast: ToastApi; toasts: ToastState } {
30
+ const toasts = useSyncExternalStore(toastStore.subscribe, toastStore.getSnapshot, toastStore.getSnapshot);
31
+ return { toast, toasts };
32
+ }
@@ -0,0 +1,44 @@
1
+ import type { ReactElement } from "react";
2
+ import { IconCrossSmall } from "../../icons/central";
3
+ import { Icon } from "../icon";
4
+ import { Pressable, type PressableProps } from "../pressable";
5
+ import { useToastPart } from "./toast.context";
6
+ import { toastVariants } from "./toast.variants";
7
+
8
+ export type ToastCloseProps = Omit<PressableProps, "asChild" | "busy" | "children">;
9
+
10
+ /**
11
+ * A trailing ✕ that hides the toast, then calls the caller's `onPress`.
12
+ *
13
+ * Muted rather than the status colour — it is about the toast, not part of
14
+ * what it says — and pressed with `fade`, since a spring on a glyph this small
15
+ * reads as a jitter. `hitSlop` lifts it to the 44-point target. A screen reader
16
+ * reaches the same thing through the toast's `escape` and "Dismiss" actions.
17
+ */
18
+ export function ToastClose({
19
+ accessibilityLabel = "Dismiss",
20
+ className,
21
+ feedback = "fade",
22
+ hitSlop = 12,
23
+ onPress,
24
+ ...props
25
+ }: ToastCloseProps): ReactElement {
26
+ const { hide } = useToastPart("Toast.Close");
27
+ return (
28
+ <Pressable
29
+ accessibilityLabel={accessibilityLabel}
30
+ accessibilityRole="button"
31
+ className={toastVariants().close({ className })}
32
+ feedback={feedback}
33
+ hitSlop={hitSlop}
34
+ onPress={() => {
35
+ hide();
36
+ onPress?.();
37
+ }}
38
+ {...props}
39
+ >
40
+ <Icon color="muted-foreground" icon={IconCrossSmall} />
41
+ </Pressable>
42
+ );
43
+ }
44
+ ToastClose.displayName = "DelacourUI.Toast.Close";
@@ -0,0 +1,11 @@
1
+ import type { ReactElement } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { toastVariants } from "./toast.variants";
4
+
5
+ export type ToastContentProps = ViewProps & { className?: string };
6
+
7
+ /** The column holding the title and the description; takes the width the glyph and the controls leave. */
8
+ export function ToastContent({ className, ...props }: ToastContentProps): ReactElement {
9
+ return <View className={toastVariants().content({ className })} {...props} />;
10
+ }
11
+ ToastContent.displayName = "DelacourUI.Toast.Content";
@@ -0,0 +1,11 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text, type TextPresetProps } from "../text";
3
+ import { toastVariants } from "./toast.variants";
4
+
5
+ export type ToastDescriptionProps = TextPresetProps;
6
+
7
+ /** A line under the title, always muted, so a long explanation never shouts. */
8
+ export function ToastDescription({ className, ...props }: ToastDescriptionProps): ReactElement {
9
+ return <Text className={toastVariants().description({ className })} {...props} />;
10
+ }
11
+ ToastDescription.displayName = "DelacourUI.Toast.Description";
@@ -0,0 +1,34 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { ALERT_GLYPHS } from "../alert/alert-glyphs";
4
+ import { Icon } from "../icon";
5
+ import { useToastPart } from "./toast.context";
6
+ import { toastVariants } from "./toast.variants";
7
+
8
+ export type ToastIndicatorProps = ViewProps & {
9
+ className?: string;
10
+ /** Replaces the status glyph — a `Spinner` while loading, an `Icon` of your own. Inherits the toast's icon size and colour. */
11
+ children?: ReactNode;
12
+ };
13
+
14
+ /**
15
+ * The toast's leading glyph — Alert's glyph for the same status, in Alert's
16
+ * colour for it, so a toast and an alert saying the same thing look alike.
17
+ *
18
+ * As tall as the title's line, so the glyph stays level with the first line.
19
+ * Hidden from assistive technology: the title says what happened.
20
+ */
21
+ export function ToastIndicator({ className, children, ...props }: ToastIndicatorProps): ReactElement {
22
+ const { status } = useToastPart("Toast.Indicator");
23
+ return (
24
+ <View
25
+ accessibilityElementsHidden
26
+ className={toastVariants().indicator({ className })}
27
+ importantForAccessibility="no-hide-descendants"
28
+ {...props}
29
+ >
30
+ {children ?? <Icon icon={ALERT_GLYPHS[status]} />}
31
+ </View>
32
+ );
33
+ }
34
+ ToastIndicator.displayName = "DelacourUI.Toast.Indicator";