@delacour/react-native-ui 0.1.0-alpha.20260925063807 → 0.1.0-alpha.20260925064429
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 +4 -2
- package/src/components/alert/AGENTS.md +96 -0
- package/src/components/alert/alert-action.tsx +17 -0
- package/src/components/alert/alert-close-button.tsx +50 -0
- package/src/components/alert/alert-content.tsx +15 -0
- package/src/components/alert/alert-description.tsx +16 -0
- package/src/components/alert/alert-indicator.tsx +59 -0
- package/src/components/alert/alert-title.tsx +18 -0
- package/src/components/alert/alert.context.tsx +69 -0
- package/src/components/alert/alert.tsx +155 -0
- package/src/components/alert/alert.types.ts +10 -0
- package/src/components/alert/alert.variants.test.ts +218 -0
- package/src/components/alert/alert.variants.ts +180 -0
- package/src/components/alert/index.ts +23 -0
- package/src/components/card/AGENTS.md +88 -0
- package/src/components/card/card-action.tsx +16 -0
- package/src/components/card/card-content.tsx +12 -0
- package/src/components/card/card-description.tsx +12 -0
- package/src/components/card/card-footer.tsx +30 -0
- package/src/components/card/card-header.tsx +45 -0
- package/src/components/card/card-title.tsx +25 -0
- package/src/components/card/card.context.tsx +64 -0
- package/src/components/card/card.tsx +94 -0
- package/src/components/card/card.types.ts +13 -0
- package/src/components/card/card.variants.test.ts +240 -0
- package/src/components/card/card.variants.ts +177 -0
- package/src/components/card/index.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delacour/react-native-ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20260925064429",
|
|
4
4
|
"description": "React Native UI components — Uniwind, Reanimated, Gesture Handler",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,10 +34,12 @@
|
|
|
34
34
|
"./styles/tokens": "./src/styles/tokens.css",
|
|
35
35
|
"./styles/theme": "./src/styles/theme.css",
|
|
36
36
|
"./accordion": "./src/components/accordion/index.ts",
|
|
37
|
+
"./alert": "./src/components/alert/index.ts",
|
|
37
38
|
"./avatar": "./src/components/avatar/index.ts",
|
|
38
39
|
"./badge": "./src/components/badge/index.ts",
|
|
39
40
|
"./bottom-sheet": "./src/components/bottom-sheet/index.ts",
|
|
40
41
|
"./button": "./src/components/button/index.ts",
|
|
42
|
+
"./card": "./src/components/card/index.ts",
|
|
41
43
|
"./chart": "./src/components/chart/index.ts",
|
|
42
44
|
"./checkbox": "./src/components/checkbox/index.ts",
|
|
43
45
|
"./chip": "./src/components/chip/index.ts",
|
|
@@ -94,7 +96,7 @@
|
|
|
94
96
|
},
|
|
95
97
|
"peerDependencies": {
|
|
96
98
|
"@central-icons-react-native/round-outlined-radius-1-stroke-1.5": "^1.1",
|
|
97
|
-
"@delacour/react-native-charts": "0.1.0-alpha.
|
|
99
|
+
"@delacour/react-native-charts": "0.1.0-alpha.20260925064429",
|
|
98
100
|
"@gorhom/bottom-sheet": "^5.2.8",
|
|
99
101
|
"@legendapp/list": ">=3.3",
|
|
100
102
|
"expo-linear-gradient": ">=15",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Alert
|
|
2
|
+
|
|
3
|
+
A status message on a `Surface`: a glyph picked from its status, a title, a
|
|
4
|
+
description and, optionally, an action row and a dismiss control. Compound root
|
|
5
|
+
plus `Alert.Indicator`, `Alert.Content`, `Alert.Title`, `Alert.Description`,
|
|
6
|
+
`Alert.Action` and `Alert.CloseButton`.
|
|
7
|
+
|
|
8
|
+
`import { Alert } from "@delacour/react-native-ui/alert";`
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | What it holds |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `index.ts` | → `@delacour/react-native-ui/alert` |
|
|
15
|
+
| `alert.tsx` | Root + the `Object.assign` compound surface |
|
|
16
|
+
| `alert-indicator.tsx` | `Alert.Indicator`, and the status → glyph map |
|
|
17
|
+
| `alert-content.tsx` | `Alert.Content` |
|
|
18
|
+
| `alert-title.tsx` | `Alert.Title` |
|
|
19
|
+
| `alert-description.tsx` | `Alert.Description` |
|
|
20
|
+
| `alert-action.tsx` | `Alert.Action` |
|
|
21
|
+
| `alert-close-button.tsx` | `Alert.CloseButton`, the dismiss pressable |
|
|
22
|
+
| `alert.context.tsx` | `AlertProvider`, `useAlert()`, `useAlertContext()`, `useAlertPart()` |
|
|
23
|
+
| `alert.types.ts` | `AlertSlotProps`, shared by `Content` and `Action` |
|
|
24
|
+
| `alert.variants.ts` | The slotted `tv()`, `ALERT_FOREGROUND_TOKEN`, `ALERT_SURFACE_PADDING` and three pure resolvers — no RN imports |
|
|
25
|
+
| `alert.variants.test.ts` | |
|
|
26
|
+
|
|
27
|
+
## Design
|
|
28
|
+
|
|
29
|
+
- **Two axes: `status` and `variant`.** `status` says what the message means —
|
|
30
|
+
`default`, `info`, `success`, `warning`, `destructive` — and `variant` says
|
|
31
|
+
how loudly: `soft` (the default) washes the surface in the status's `-soft`
|
|
32
|
+
fill, `surface` keeps a neutral fill and lets the glyph and title carry the
|
|
33
|
+
colour. **Sizes**: `sm`, `md`, `lg`, which move padding, gap, type and glyph
|
|
34
|
+
together. `destructive` rather than `danger`, per the package's token rule.
|
|
35
|
+
- **It is built on `Surface`, and it adds only what a surface lacks.** The
|
|
36
|
+
corner, the continuous curve, the padding (`ALERT_SURFACE_PADDING` maps each
|
|
37
|
+
size onto the surface's own step) and the neutral fill all come from
|
|
38
|
+
[Surface](../surface/AGENTS.md). `alertVariants`' root carries the row and the
|
|
39
|
+
tint, and a test asserts it sets no padding of its own — a second one would
|
|
40
|
+
fight the surface's through the merge.
|
|
41
|
+
- **Only a tinted alert names a fill.** `resolveAlertTinted` is true for `soft`
|
|
42
|
+
plus a status, and those four cells swap `bg-card` for `bg-X-soft` and clear
|
|
43
|
+
the card's hairline: a grey rule around a red wash reads as two components
|
|
44
|
+
stacked. Every other alert leaves the surface's `variant` undefined
|
|
45
|
+
(`resolveAlertSurfaceVariant`), so it steps from the plane it sits on — an
|
|
46
|
+
alert inside a card lands on `secondary` rather than vanishing into the card.
|
|
47
|
+
A tinted alert pins the surface to `default` instead, so what nests in it
|
|
48
|
+
steps from a card like anything else.
|
|
49
|
+
- **One colour token per status, not per status and variant.**
|
|
50
|
+
`ALERT_FOREGROUND_TOKEN` names `X-soft-foreground` for each status and the
|
|
51
|
+
page's `foreground` for `default`; those are tuned to read on the soft fill
|
|
52
|
+
and on the neutral fills alike. The title slot emits the same token as a
|
|
53
|
+
class and the root publishes it through `IconDefaultsProvider`, so the glyph
|
|
54
|
+
and the title are always one shade. A test pins the pair and asserts every
|
|
55
|
+
token exists in both themes. The classes are written out literally —
|
|
56
|
+
Tailwind's scanner is static, so a class assembled from the map at runtime
|
|
57
|
+
would never be compiled.
|
|
58
|
+
- **The description is always muted.** A long explanation under a red title
|
|
59
|
+
must not shout; the title carries the status.
|
|
60
|
+
- **`items-start`, and the indicator is as tall as the title's line.** A
|
|
61
|
+
description that wraps to four lines must not drag the glyph to its middle.
|
|
62
|
+
The indicator's height is the title's line height at each size (`h-5`/`h-6`/
|
|
63
|
+
`h-7` against `text-sm`/`base`/`lg`), and it centres the glyph in that box, so
|
|
64
|
+
the glyph sits level with the first line. Nothing else is a fixed height:
|
|
65
|
+
`Text` respects OS font scaling, and a fixed box would clip a wrapped title.
|
|
66
|
+
- **Glyphs differ in shape, not just colour.** `warning` is a triangle and
|
|
67
|
+
`destructive` a circle, so the two stay apart for anyone who cannot tell amber
|
|
68
|
+
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.variants.ts` must stay importable from `bun test`.
|
|
71
|
+
- **The indicator is hidden from assistive technology.** The title says what
|
|
72
|
+
happened; "image" announced before it adds nothing. Children replace the
|
|
73
|
+
glyph and inherit the alert's icon size and colour — a `Spinner` for a
|
|
74
|
+
pending state needs nothing else.
|
|
75
|
+
- **Announced as an `alert`, and as a live region on Android.**
|
|
76
|
+
`resolveAlertLiveRegion` makes a `warning` or `destructive` alert assertive
|
|
77
|
+
and anything else polite. The root is not made `accessible`: grouping it
|
|
78
|
+
would swallow the close control and any action into one element.
|
|
79
|
+
- **Dismissal is controllable.** `isDismissible` composes an
|
|
80
|
+
`Alert.CloseButton` in at the end. Uncontrolled, the alert hides itself when
|
|
81
|
+
dismissed (`defaultOpen`); pass `isOpen` with `onOpenChange` to own it.
|
|
82
|
+
`useAlert().dismiss` is on the context so an action ("Got it") can close the
|
|
83
|
+
alert without the caller threading a setter down. A dismissed alert fades out
|
|
84
|
+
over 150 ms rather than vanishing — the one `Animated.View` wrapper exists for
|
|
85
|
+
that `exiting`, and a caller's `className` still reaches the surface.
|
|
86
|
+
- **The close glyph is muted, not the status colour.** The control is about the
|
|
87
|
+
alert rather than part of what it says, and a red cross beside a red title
|
|
88
|
+
reads as a second warning. It presses with `fade` — a spring on a glyph that
|
|
89
|
+
small is a jitter — and carries `hitSlop={10}` to reach the 44-point target.
|
|
90
|
+
- **Actions are the caller's.** `Alert.Action` is a wrapping row and styles
|
|
91
|
+
nothing in it; a `Button` in there is sized and painted however the caller
|
|
92
|
+
says. It wraps rather than overflowing, because two buttons and a long label
|
|
93
|
+
outgrow a phone's width.
|
|
94
|
+
- **Parts are composed, not configured.** There is no `title` or `icon` prop:
|
|
95
|
+
omit `Alert.Indicator` for a text-only alert, omit `Alert.Description` for a
|
|
96
|
+
one-liner. The same reason `Button` and `Badge` compose their icons.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type { AlertSlotProps } from "./alert.types";
|
|
4
|
+
import { alertVariants } from "./alert.variants";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A wrapping row for the alert's buttons, placed under the description inside
|
|
8
|
+
* `Alert.Content`.
|
|
9
|
+
*
|
|
10
|
+
* Takes any children — a `Button`, a `Text.Link` — and styles none of them: an
|
|
11
|
+
* action is the caller's control, sized and painted the way they want it. Read
|
|
12
|
+
* `useAlert().dismiss` from inside to close the alert from an action.
|
|
13
|
+
*/
|
|
14
|
+
export function AlertAction({ className, ...props }: AlertSlotProps): ReactElement {
|
|
15
|
+
return <View className={alertVariants().action({ className })} {...props} />;
|
|
16
|
+
}
|
|
17
|
+
AlertAction.displayName = "DelacourUI.Alert.Action";
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { useAlertPart } from "./alert.context";
|
|
6
|
+
import { alertVariants } from "./alert.variants";
|
|
7
|
+
|
|
8
|
+
export type AlertCloseButtonProps = Omit<PressableProps, "asChild" | "busy" | "children">;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The alert's trailing dismiss control.
|
|
12
|
+
*
|
|
13
|
+
* The root composes one in whenever `isDismissible` is set, so reach for this
|
|
14
|
+
* by hand only to place it somewhere else. Pressing it calls the alert's
|
|
15
|
+
* `dismiss` and then the caller's own `onPress`.
|
|
16
|
+
*
|
|
17
|
+
* The glyph is drawn in `muted-foreground` rather than the status colour: the
|
|
18
|
+
* control is about the alert, not part of what it says, and a red cross beside
|
|
19
|
+
* a red title reads as a second warning. `fade` rather than `scale` — a spring
|
|
20
|
+
* on a glyph this small reads as a jitter. `hitSlop` lifts the target to the
|
|
21
|
+
* 44-point minimum the glyph alone falls short of.
|
|
22
|
+
*/
|
|
23
|
+
export function AlertCloseButton({
|
|
24
|
+
accessibilityLabel = "Dismiss",
|
|
25
|
+
className,
|
|
26
|
+
feedback = "fade",
|
|
27
|
+
hitSlop = 10,
|
|
28
|
+
onPress,
|
|
29
|
+
...props
|
|
30
|
+
}: AlertCloseButtonProps): ReactElement {
|
|
31
|
+
const { size, dismiss } = useAlertPart("Alert.CloseButton");
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Pressable
|
|
35
|
+
accessibilityLabel={accessibilityLabel}
|
|
36
|
+
accessibilityRole="button"
|
|
37
|
+
className={alertVariants({ size }).closeButton({ className })}
|
|
38
|
+
feedback={feedback}
|
|
39
|
+
hitSlop={hitSlop}
|
|
40
|
+
onPress={() => {
|
|
41
|
+
dismiss();
|
|
42
|
+
onPress?.();
|
|
43
|
+
}}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<Icon color="muted-foreground" icon={IconCrossSmall} />
|
|
47
|
+
</Pressable>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
AlertCloseButton.displayName = "DelacourUI.Alert.CloseButton";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type { AlertSlotProps } from "./alert.types";
|
|
4
|
+
import { alertVariants } from "./alert.variants";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The column holding the title, the description and any action.
|
|
8
|
+
*
|
|
9
|
+
* `flex-1` with `min-w-0` is what keeps a long description wrapping inside the
|
|
10
|
+
* alert instead of pushing the close control off its edge.
|
|
11
|
+
*/
|
|
12
|
+
export function AlertContent({ className, ...props }: AlertSlotProps): ReactElement {
|
|
13
|
+
return <View className={alertVariants().content({ className })} {...props} />;
|
|
14
|
+
}
|
|
15
|
+
AlertContent.displayName = "DelacourUI.Alert.Content";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text, type TextPresetProps } from "../text";
|
|
3
|
+
import { useAlertPart } from "./alert.context";
|
|
4
|
+
import { alertVariants } from "./alert.variants";
|
|
5
|
+
|
|
6
|
+
export type AlertDescriptionProps = TextPresetProps;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The alert's body text — always muted, whatever the status, so a long
|
|
10
|
+
* explanation under a red title never shouts.
|
|
11
|
+
*/
|
|
12
|
+
export function AlertDescription({ className, ...props }: AlertDescriptionProps): ReactElement {
|
|
13
|
+
const { size } = useAlertPart("Alert.Description");
|
|
14
|
+
return <Text className={alertVariants({ size }).description({ className })} {...props} />;
|
|
15
|
+
}
|
|
16
|
+
AlertDescription.displayName = "DelacourUI.Alert.Description";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { IconCircleCheck, IconCircleInfo, IconExclamationCircle, IconExclamationTriangle } from "../../icons/central";
|
|
4
|
+
import { Icon, type IconComponent } from "../icon";
|
|
5
|
+
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
|
+
};
|
|
26
|
+
|
|
27
|
+
export type AlertIndicatorProps = ViewProps & {
|
|
28
|
+
className?: string;
|
|
29
|
+
/** Replaces the status glyph — a `Spinner`, an `Icon` of your own. It inherits the alert's icon size and colour. */
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The alert's leading glyph, picked from its status.
|
|
35
|
+
*
|
|
36
|
+
* Carries the title's line height as its own height and centres the glyph in
|
|
37
|
+
* it, so the glyph sits level with the title's first line however far the
|
|
38
|
+
* description wraps.
|
|
39
|
+
*
|
|
40
|
+
* Hidden from assistive technology. The title says what happened, and a screen
|
|
41
|
+
* reader announcing "image" before it adds nothing. Children replace the glyph
|
|
42
|
+
* and inherit the alert's icon size and colour, so `<Spinner />` or
|
|
43
|
+
* `<Icon icon={IconCloud} />` needs nothing else.
|
|
44
|
+
*/
|
|
45
|
+
export function AlertIndicator({ className, children, ...props }: AlertIndicatorProps): ReactElement {
|
|
46
|
+
const { status, size } = useAlertPart("Alert.Indicator");
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View
|
|
50
|
+
accessibilityElementsHidden
|
|
51
|
+
className={alertVariants({ size }).indicator({ className })}
|
|
52
|
+
importantForAccessibility="no-hide-descendants"
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{children ?? <Icon icon={ALERT_GLYPHS[status]} />}
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
AlertIndicator.displayName = "DelacourUI.Alert.Indicator";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text, type TextPresetProps } from "../text";
|
|
3
|
+
import { useAlertPart } from "./alert.context";
|
|
4
|
+
import { alertVariants } from "./alert.variants";
|
|
5
|
+
|
|
6
|
+
export type AlertTitleProps = TextPresetProps;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The alert's heading, coloured by its status.
|
|
10
|
+
*
|
|
11
|
+
* The colour is the same token the indicator's glyph reads, so the two are
|
|
12
|
+
* always one shade — a test pins the pair.
|
|
13
|
+
*/
|
|
14
|
+
export function AlertTitle({ className, ...props }: AlertTitleProps): ReactElement {
|
|
15
|
+
const { status, size } = useAlertPart("Alert.Title");
|
|
16
|
+
return <Text className={alertVariants({ size, status }).title({ className })} {...props} />;
|
|
17
|
+
}
|
|
18
|
+
AlertTitle.displayName = "DelacourUI.Alert.Title";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { AlertSize, AlertStatus, AlertVariant } from "./alert.variants";
|
|
3
|
+
|
|
4
|
+
export type AlertContextValue = {
|
|
5
|
+
/** What the alert says about the thing it describes. */
|
|
6
|
+
status: AlertStatus;
|
|
7
|
+
/** How the alert's surface is painted. */
|
|
8
|
+
variant: AlertVariant;
|
|
9
|
+
/** Size of the alert. */
|
|
10
|
+
size: AlertSize;
|
|
11
|
+
/**
|
|
12
|
+
* Closes the alert — hides it when uncontrolled, and reports `false` through
|
|
13
|
+
* `onOpenChange` either way. Lets an action inside the alert ("Got it")
|
|
14
|
+
* dismiss it without the caller threading a setter down.
|
|
15
|
+
*/
|
|
16
|
+
dismiss: () => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const AlertContext = createContext<AlertContextValue | null>(null);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Supplies the enclosing alert's status, variant, size and `dismiss` to its
|
|
23
|
+
* subtree.
|
|
24
|
+
*
|
|
25
|
+
* Lives in its own module, importing nothing but `alert.variants`, so a part
|
|
26
|
+
* can read it without importing `./alert`. That import would close a cycle, and
|
|
27
|
+
* Metro serves a partially initialised module for a cycle — leaving the context
|
|
28
|
+
* `undefined` at import time and red-boxing the app on a cold start.
|
|
29
|
+
*/
|
|
30
|
+
export function AlertProvider({ value, children }: { value: AlertContextValue; children: ReactNode }): ReactElement {
|
|
31
|
+
return <AlertContext value={value}>{children}</AlertContext>;
|
|
32
|
+
}
|
|
33
|
+
AlertProvider.displayName = "DelacourUI.Alert.Provider";
|
|
34
|
+
|
|
35
|
+
/** The enclosing alert's context, or null outside an `<Alert>`. */
|
|
36
|
+
export function useAlertContext(): AlertContextValue | null {
|
|
37
|
+
return use(AlertContext);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reads the enclosing alert's status, variant, size and `dismiss`.
|
|
42
|
+
*
|
|
43
|
+
* Lets a custom child match the alert — or close it — without the alert passing
|
|
44
|
+
* props down. Throws outside an `<Alert>`; use {@link useAlertContext} where the
|
|
45
|
+
* enclosing alert is optional.
|
|
46
|
+
*/
|
|
47
|
+
export function useAlert(): AlertContextValue {
|
|
48
|
+
const context = useAlertContext();
|
|
49
|
+
if (!context) {
|
|
50
|
+
throw new Error("useAlert must be called inside an <Alert>.");
|
|
51
|
+
}
|
|
52
|
+
return context;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The enclosing alert's context, for a compound part that cannot work without
|
|
57
|
+
* one.
|
|
58
|
+
*
|
|
59
|
+
* Internal: deliberately not re-exported from `index.ts`. A caller outside the
|
|
60
|
+
* library wants {@link useAlert}, whose error message names the hook rather than
|
|
61
|
+
* a part.
|
|
62
|
+
*/
|
|
63
|
+
export function useAlertPart(component: string): AlertContextValue {
|
|
64
|
+
const context = useAlertContext();
|
|
65
|
+
if (!context) {
|
|
66
|
+
throw new Error(`${component} must be rendered inside an <Alert>.`);
|
|
67
|
+
}
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useCallback, useMemo } from "react";
|
|
2
|
+
import Animated, { FadeOut } from "react-native-reanimated";
|
|
3
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
4
|
+
import { IconDefaultsProvider } from "../icon";
|
|
5
|
+
import { Surface, type SurfaceProps } from "../surface";
|
|
6
|
+
import { type AlertContextValue, AlertProvider } from "./alert.context";
|
|
7
|
+
import {
|
|
8
|
+
ALERT_FOREGROUND_TOKEN,
|
|
9
|
+
ALERT_SURFACE_PADDING,
|
|
10
|
+
type AlertSize,
|
|
11
|
+
type AlertStatus,
|
|
12
|
+
type AlertVariant,
|
|
13
|
+
alertVariants,
|
|
14
|
+
resolveAlertLiveRegion,
|
|
15
|
+
resolveAlertSurfaceVariant,
|
|
16
|
+
} from "./alert.variants";
|
|
17
|
+
import { AlertAction } from "./alert-action";
|
|
18
|
+
import { AlertCloseButton } from "./alert-close-button";
|
|
19
|
+
import { AlertContent } from "./alert-content";
|
|
20
|
+
import { AlertDescription } from "./alert-description";
|
|
21
|
+
import { AlertIndicator } from "./alert-indicator";
|
|
22
|
+
import { AlertTitle } from "./alert-title";
|
|
23
|
+
|
|
24
|
+
/** A dismissed alert fades rather than vanishing, so the layout below it has a beat to follow. */
|
|
25
|
+
const EXITING = FadeOut.duration(150);
|
|
26
|
+
|
|
27
|
+
export type AlertProps = Omit<SurfaceProps, "variant" | "padding"> & {
|
|
28
|
+
/** What the alert says: `default`, `info`, `success`, `warning` or `destructive`. Picks the glyph and the colour. */
|
|
29
|
+
status?: AlertStatus;
|
|
30
|
+
/** `soft` washes the surface in the status's colour; `surface` keeps the neutral fill and colours only the glyph and title. */
|
|
31
|
+
variant?: AlertVariant;
|
|
32
|
+
/** Padding, gap, type scale and glyph size together. */
|
|
33
|
+
size?: AlertSize;
|
|
34
|
+
/** Composes a trailing dismiss control in. */
|
|
35
|
+
isDismissible?: boolean;
|
|
36
|
+
/** Whether the alert is shown. Pass it to control the alert; omit it and the alert hides itself when dismissed. */
|
|
37
|
+
isOpen?: boolean;
|
|
38
|
+
/** Whether an uncontrolled alert starts shown. */
|
|
39
|
+
defaultOpen?: boolean;
|
|
40
|
+
/** Called with `false` when the alert is dismissed, controlled or not. */
|
|
41
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
42
|
+
/** Name a screen reader gives the dismiss control. Defaults to `Dismiss`. */
|
|
43
|
+
closeAccessibilityLabel?: string;
|
|
44
|
+
children?: ReactNode;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function AlertRoot({
|
|
48
|
+
status = "default",
|
|
49
|
+
variant = "soft",
|
|
50
|
+
size = "md",
|
|
51
|
+
isDismissible = false,
|
|
52
|
+
isOpen,
|
|
53
|
+
defaultOpen = true,
|
|
54
|
+
onOpenChange,
|
|
55
|
+
closeAccessibilityLabel,
|
|
56
|
+
className,
|
|
57
|
+
children,
|
|
58
|
+
...props
|
|
59
|
+
}: AlertProps): ReactElement | null {
|
|
60
|
+
const [open, setOpen] = useControllableState({ defaultValue: defaultOpen, onChange: onOpenChange, value: isOpen });
|
|
61
|
+
const dismiss = useCallback(() => setOpen(false), [setOpen]);
|
|
62
|
+
|
|
63
|
+
const context = useMemo<AlertContextValue>(
|
|
64
|
+
() => ({ dismiss, size, status, variant }),
|
|
65
|
+
[dismiss, size, status, variant]
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const slots = alertVariants({ size, status, variant });
|
|
69
|
+
|
|
70
|
+
// A glyph composed anywhere inside — the indicator's own, a `Spinner`, an
|
|
71
|
+
// `Icon` in an action — adopts the alert's step and its status's colour.
|
|
72
|
+
const iconClassName = slots.icon();
|
|
73
|
+
const iconColor = ALERT_FOREGROUND_TOKEN[status];
|
|
74
|
+
const iconDefaults = useMemo(() => ({ className: iconClassName, color: iconColor }), [iconClassName, iconColor]);
|
|
75
|
+
|
|
76
|
+
if (!open) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<AlertProvider value={context}>
|
|
82
|
+
<Animated.View exiting={EXITING}>
|
|
83
|
+
<Surface
|
|
84
|
+
accessibilityLiveRegion={resolveAlertLiveRegion(status)}
|
|
85
|
+
accessibilityRole="alert"
|
|
86
|
+
className={slots.root({ className })}
|
|
87
|
+
padding={ALERT_SURFACE_PADDING[size]}
|
|
88
|
+
variant={resolveAlertSurfaceVariant({ status, variant })}
|
|
89
|
+
{...props}
|
|
90
|
+
>
|
|
91
|
+
<IconDefaultsProvider value={iconDefaults}>
|
|
92
|
+
{children}
|
|
93
|
+
{isDismissible ? <AlertCloseButton accessibilityLabel={closeAccessibilityLabel} /> : null}
|
|
94
|
+
</IconDefaultsProvider>
|
|
95
|
+
</Surface>
|
|
96
|
+
</Animated.View>
|
|
97
|
+
</AlertProvider>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A status message — a glyph, a title, a description and, optionally, an
|
|
103
|
+
* action — drawn on a `Surface`.
|
|
104
|
+
*
|
|
105
|
+
* `status` says what the message means: `default`, `info`, `success`,
|
|
106
|
+
* `warning` or `destructive`. It picks the indicator's glyph and colours the
|
|
107
|
+
* glyph and the title from one token; the description stays muted whatever the
|
|
108
|
+
* status. `variant` says how loudly: `soft` washes the surface in the status's
|
|
109
|
+
* soft fill, `surface` keeps the neutral fill and lets the glyph and title carry
|
|
110
|
+
* the colour. A neutral or `surface` alert takes its fill from the surface
|
|
111
|
+
* ladder, so one inside a card steps off the card instead of vanishing into it.
|
|
112
|
+
*
|
|
113
|
+
* `isDismissible` composes a close control in. Uncontrolled, the alert hides
|
|
114
|
+
* itself when dismissed; pass `isOpen` and `onOpenChange` to own that state.
|
|
115
|
+
* `useAlert().dismiss` closes it from inside, for a "Got it" action.
|
|
116
|
+
*
|
|
117
|
+
* Announced as an `alert`, and as a live region on Android — assertive for a
|
|
118
|
+
* warning or a failure, polite for anything else.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* <Alert status="warning">
|
|
122
|
+
* <Alert.Indicator />
|
|
123
|
+
* <Alert.Content>
|
|
124
|
+
* <Alert.Title>Card expiring</Alert.Title>
|
|
125
|
+
* <Alert.Description>The card ending 4242 expires next month.</Alert.Description>
|
|
126
|
+
* </Alert.Content>
|
|
127
|
+
* </Alert>
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* <Alert isDismissible status="destructive">
|
|
131
|
+
* <Alert.Indicator />
|
|
132
|
+
* <Alert.Content>
|
|
133
|
+
* <Alert.Title>Payment failed</Alert.Title>
|
|
134
|
+
* <Alert.Description>Your bank declined the charge.</Alert.Description>
|
|
135
|
+
* <Alert.Action>
|
|
136
|
+
* <Button onPress={retry} size="sm">Try again</Button>
|
|
137
|
+
* </Alert.Action>
|
|
138
|
+
* </Alert.Content>
|
|
139
|
+
* </Alert>
|
|
140
|
+
*/
|
|
141
|
+
export const Alert = Object.assign(AlertRoot, {
|
|
142
|
+
/** The leading status glyph, picked from `status`. Children replace it. */
|
|
143
|
+
Indicator: AlertIndicator,
|
|
144
|
+
/** The column holding the title, description and action; takes the remaining width. */
|
|
145
|
+
Content: AlertContent,
|
|
146
|
+
/** The heading, coloured by the status. */
|
|
147
|
+
Title: AlertTitle,
|
|
148
|
+
/** The body text, always muted. */
|
|
149
|
+
Description: AlertDescription,
|
|
150
|
+
/** A wrapping row of buttons under the description. */
|
|
151
|
+
Action: AlertAction,
|
|
152
|
+
/** The trailing dismiss control. Composed in automatically whenever `isDismissible` is set. */
|
|
153
|
+
CloseButton: AlertCloseButton,
|
|
154
|
+
displayName: "DelacourUI.Alert",
|
|
155
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ViewProps } from "react-native";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shape of an alert's layout parts.
|
|
5
|
+
*
|
|
6
|
+
* Shared by `Alert.Content` and `Alert.Action`, which are both a styled `View`
|
|
7
|
+
* and nothing else, so it lives in a leaf rather than in one of the two files
|
|
8
|
+
* arbitrarily.
|
|
9
|
+
*/
|
|
10
|
+
export type AlertSlotProps = ViewProps & { className?: string };
|