Skip to content

Commit 327530f

Browse files
committed
feat: add SnackbarProvider and SnackbarSurface components
1 parent 762906f commit 327530f

68 files changed

Lines changed: 5045 additions & 157 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/docs/components/appbar.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 3
2+
sidebar_position: 1
33
---
44

55
# AppBar

‎docs/docs/components/dialog.mdx‎

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
---
2+
sidebar_position: 2
3+
---
4+
5+
# Dialog
6+
7+
Dialogs interrupt the user with an important prompt or a focused task.
8+
Follows the [Material Design 3 Dialog](https://m3.material.io/components/dialogs/overview) specification.
9+
10+
Dialog renders through [Portal](./portal.mdx), so your app must be wrapped in a `PortalHost` once at the root. It claims the `PORTAL_LAYERS.dialog` layer, which puts it above bottom sheets and below snackbars, menus, and tooltips.
11+
12+
## Usage
13+
14+
<SnackExample
15+
name="RootNative Dialog — Usage"
16+
code={`
17+
import { Button, Dialog, PortalHost } from '@rootnative/components'
18+
import { ThemeProvider } from '@rootnative/core'
19+
import { SafeAreaProvider } from 'react-native-safe-area-context'
20+
import { useState } from 'react'
21+
import { View } from 'react-native'
22+
23+
function Screen() {
24+
const [open, setOpen] = useState(false)
25+
return (
26+
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
27+
<Button onPress={() => setOpen(true)}>Delete file</Button>
28+
<Dialog visible={open} onDismiss={() => setOpen(false)}>
29+
<Dialog.Title>Delete file?</Dialog.Title>
30+
<Dialog.Content>
31+
This permanently removes report.pdf. You cannot undo this action.
32+
</Dialog.Content>
33+
<Dialog.Actions>
34+
<Button variant="text" onPress={() => setOpen(false)}>Cancel</Button>
35+
<Button variant="text" onPress={() => setOpen(false)}>Delete</Button>
36+
</Dialog.Actions>
37+
</Dialog>
38+
</View>
39+
)
40+
}
41+
42+
export default function App() {
43+
return (
44+
<SafeAreaProvider>
45+
<ThemeProvider>
46+
<PortalHost>
47+
<Screen />
48+
</PortalHost>
49+
</ThemeProvider>
50+
</SafeAreaProvider>
51+
)
52+
}
53+
`}
54+
/>
55+
56+
## Slots
57+
58+
Dialog composes from four slots. **Write them in any order** — Dialog places them
59+
in MD3 order (icon → headline → content → actions) itself, which is what lets the
60+
fullscreen variant move the title and actions into its header.
61+
62+
| Slot | Renders | MD3 role |
63+
|------|---------|----------|
64+
| `Dialog.Icon` | An `IconSource` at 24dp, centered | Optional hero icon. Its presence centers the headline. |
65+
| `Dialog.Title` | `headlineSmall` / `onSurface` | Headline |
66+
| `Dialog.Content` | Strings get `bodyMedium` / `onSurfaceVariant`; anything else renders as-is | Supporting text or arbitrary content |
67+
| `Dialog.Actions` | An end-aligned row with an 8dp gap | Action buttons — text `Button`s per MD3 |
68+
69+
Any child that isn't one of these is treated as content, so `<Dialog><MyForm /></Dialog>` works without ceremony.
70+
71+
## With an icon
72+
73+
An icon makes the dialog a hero dialog: the icon sits centered above the headline
74+
in `secondary`, and the headline centers with it.
75+
76+
<SnackExample
77+
name="RootNative Dialog — Icon"
78+
code={`
79+
import { Button, Dialog, PortalHost } from '@rootnative/components'
80+
import { ThemeProvider } from '@rootnative/core'
81+
import { SafeAreaProvider } from 'react-native-safe-area-context'
82+
import { useState } from 'react'
83+
import { View } from 'react-native'
84+
85+
function Screen() {
86+
const [open, setOpen] = useState(false)
87+
return (
88+
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
89+
<Button variant="tonal" onPress={() => setOpen(true)}>Reset settings</Button>
90+
<Dialog visible={open} onDismiss={() => setOpen(false)}>
91+
<Dialog.Icon icon="alert-circle-outline" />
92+
<Dialog.Title>Reset settings?</Dialog.Title>
93+
<Dialog.Content>
94+
Every preference returns to its default. Your data is untouched.
95+
</Dialog.Content>
96+
<Dialog.Actions>
97+
<Button variant="text" onPress={() => setOpen(false)}>Cancel</Button>
98+
<Button variant="text" onPress={() => setOpen(false)}>Reset</Button>
99+
</Dialog.Actions>
100+
</Dialog>
101+
</View>
102+
)
103+
}
104+
105+
export default function App() {
106+
return (
107+
<SafeAreaProvider>
108+
<ThemeProvider>
109+
<PortalHost>
110+
<Screen />
111+
</PortalHost>
112+
</ThemeProvider>
113+
</SafeAreaProvider>
114+
)
115+
}
116+
`}
117+
/>
118+
119+
## Full-screen
120+
121+
`variant="fullscreen"` fills the screen instead of centering a card. Dialog builds
122+
the MD3 header for you: a leading close button wired to `onDismiss`, the
123+
`Dialog.Title` as a `titleLarge` headline, and `Dialog.Actions` as the trailing
124+
confirming action. `Dialog.Content` scrolls below it. There is no scrim.
125+
126+
<SnackExample
127+
name="RootNative Dialog — Full-screen"
128+
code={`
129+
import { Button, Column, Dialog, PortalHost, TextField } from '@rootnative/components'
130+
import { ThemeProvider } from '@rootnative/core'
131+
import { SafeAreaProvider } from 'react-native-safe-area-context'
132+
import { useState } from 'react'
133+
import { View } from 'react-native'
134+
135+
function Screen() {
136+
const [open, setOpen] = useState(false)
137+
return (
138+
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
139+
<Button variant="tonal" onPress={() => setOpen(true)}>Edit profile</Button>
140+
<Dialog visible={open} variant="fullscreen" onDismiss={() => setOpen(false)}>
141+
<Dialog.Title>Edit profile</Dialog.Title>
142+
<Dialog.Actions>
143+
<Button variant="text" onPress={() => setOpen(false)}>Save</Button>
144+
</Dialog.Actions>
145+
<Dialog.Content>
146+
<Column gap="lg">
147+
<TextField label="Display name" value="Ada Lovelace" />
148+
<TextField label="Email" value="ada@example.com" />
149+
</Column>
150+
</Dialog.Content>
151+
</Dialog>
152+
</View>
153+
)
154+
}
155+
156+
export default function App() {
157+
return (
158+
<SafeAreaProvider>
159+
<ThemeProvider>
160+
<PortalHost>
161+
<Screen />
162+
</PortalHost>
163+
</ThemeProvider>
164+
</SafeAreaProvider>
165+
)
166+
}
167+
`}
168+
/>
169+
170+
## Dismissal
171+
172+
`onDismiss` fires for the *user-initiated* ways out — a scrim tap, the Android
173+
hardware back button, and the fullscreen close button. Buttons inside
174+
`Dialog.Actions` are yours to wire; Dialog never closes itself on an action press.
175+
176+
Set `dismissable={false}` when the user has to resolve the dialog with one of its
177+
actions. The scrim stops responding and stops being announced as an accessibility
178+
affordance, and the back button falls through to navigation.
179+
180+
## Motion
181+
182+
Basic dialogs scale up from 80% and fade in on `springDefaultSpatial`; the scrim
183+
fades on `springFastEffects`. The fullscreen variant rises 48dp instead of scaling.
184+
185+
Every one of these collapses to a hard cut under reduced motion — see [Motion](../motion.md).
186+
187+
> **Intentional deviation.** MD3 slides a full-screen dialog up from the bottom
188+
> edge of the screen. That needs the measured surface height, which fights the
189+
> safe-area layout, so RootNative uses a 48dp rise. At spring speed the two read
190+
> the same.
191+
192+
## Tokens
193+
194+
| Token | Basic | Full-screen |
195+
|-------|-------|-------------|
196+
| Container | `surfaceContainerHigh` | `surface` |
197+
| Corner | 28dp (`cornerExtraLarge`) | — |
198+
| Elevation | Level 3 | — |
199+
| Width | min 280dp, max 560dp | Fills the screen |
200+
| Padding | 24dp | 24dp body, 56dp header |
201+
| Icon | 24dp, `secondary` | 24dp, `secondary` |
202+
| Headline | `headlineSmall` / `onSurface` | `titleLarge` / `onSurface` |
203+
| Supporting text | `bodyMedium` / `onSurfaceVariant` | `bodyMedium` / `onSurfaceVariant` |
204+
| Actions | Text buttons, end-aligned, 8dp gap | Trailing text button in the header |
205+
| Scrim | `scrim` @ 32% | None |
206+
207+
## Accessibility
208+
209+
- The basic surface reports `role="alertdialog"`; the fullscreen surface reports `role="dialog"`. Both set `aria-modal` and `accessibilityViewIsModal`, so assistive tech treats the content beneath as inert.
210+
- `Dialog.Title` reports `role="heading"`.
211+
- The scrim is announced as a button labelled "Close dialog" — override with `scrimAccessibilityLabel`. It disappears from the accessibility tree when `dismissable={false}`.
212+
- The fullscreen close button is labelled "Close" — override with `closeAccessibilityLabel`.
213+
214+
## Props
215+
216+
<PropTable name="Dialog" />
217+
218+
### Dialog.Icon
219+
220+
<PropTable name="DialogIcon" />
221+
222+
### Dialog.Title
223+
224+
<PropTable name="DialogTitle" />
225+
226+
### Dialog.Content
227+
228+
<PropTable name="DialogContent" />
229+
230+
### Dialog.Actions
231+
232+
<PropTable name="DialogActions" />

‎docs/docs/components/divider.mdx‎

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
---
2+
sidebar_position: 3
3+
---
4+
5+
# Divider
6+
7+
Dividers are thin lines that group content in lists and containers.
8+
Follows the [Material Design 3 Divider](https://m3.material.io/components/divider/overview) specification — 1dp, drawn in `outlineVariant`.
9+
10+
## Usage
11+
12+
<SnackExample
13+
name="RootNative Divider — Usage"
14+
code={`
15+
import { Column, Divider, Typography } from '@rootnative/components'
16+
import { ThemeProvider } from '@rootnative/core'
17+
import { SafeAreaProvider } from 'react-native-safe-area-context'
18+
19+
export default function App() {
20+
return (
21+
<SafeAreaProvider>
22+
<ThemeProvider>
23+
<Column gap="md" style={{ padding: 24 }}>
24+
<Typography variant="bodyLarge">Above</Typography>
25+
<Divider />
26+
<Typography variant="bodyLarge">Below</Typography>
27+
</Column>
28+
</ThemeProvider>
29+
</SafeAreaProvider>
30+
)
31+
}
32+
`}
33+
/>
34+
35+
## Insets
36+
37+
`inset` insets the leading edge, `insetEnd` the trailing edge. Both take a number in dp.
38+
`inset` also accepts `true`, which applies the MD3 list inset of 56dp — the offset that
39+
aligns a divider with list text that follows a 24dp leading icon.
40+
41+
| Prop value | Result |
42+
|------------|--------|
43+
| `<Divider />` | Full-bleed |
44+
| `<Divider inset />` | 56dp leading inset (MD3 list inset) |
45+
| `<Divider inset={16} />` | 16dp leading inset |
46+
| `<Divider inset={16} insetEnd={16} />` | Inset on both ends |
47+
48+
<SnackExample
49+
name="RootNative Divider — Insets"
50+
code={`
51+
import { Column, Divider, Typography } from '@rootnative/components'
52+
import { ThemeProvider } from '@rootnative/core'
53+
import { SafeAreaProvider } from 'react-native-safe-area-context'
54+
55+
export default function App() {
56+
return (
57+
<SafeAreaProvider>
58+
<ThemeProvider>
59+
<Column gap="lg" style={{ padding: 24 }}>
60+
<Typography variant="labelSmall">Full-bleed</Typography>
61+
<Divider />
62+
<Typography variant="labelSmall">inset</Typography>
63+
<Divider inset />
64+
<Typography variant="labelSmall">inset + insetEnd</Typography>
65+
<Divider inset={16} insetEnd={16} />
66+
</Column>
67+
</ThemeProvider>
68+
</SafeAreaProvider>
69+
)
70+
}
71+
`}
72+
/>
73+
74+
## Vertical
75+
76+
Set `orientation="vertical"` to separate content side by side. A vertical divider
77+
stretches to the height of its parent, so it must sit inside a row-direction
78+
container that has a height. In this orientation `inset` and `insetEnd` apply to
79+
the top and bottom edges.
80+
81+
<SnackExample
82+
name="RootNative Divider — Vertical"
83+
code={`
84+
import { Divider, Row, Typography } from '@rootnative/components'
85+
import { ThemeProvider } from '@rootnative/core'
86+
import { SafeAreaProvider } from 'react-native-safe-area-context'
87+
88+
export default function App() {
89+
return (
90+
<SafeAreaProvider>
91+
<ThemeProvider>
92+
<Row gap="md" align="center" style={{ padding: 24, height: 80 }}>
93+
<Typography variant="bodyLarge">Drafts</Typography>
94+
<Divider orientation="vertical" />
95+
<Typography variant="bodyLarge">Sent</Typography>
96+
<Divider orientation="vertical" />
97+
<Typography variant="bodyLarge">Spam</Typography>
98+
</Row>
99+
</ThemeProvider>
100+
</SafeAreaProvider>
101+
)
102+
}
103+
`}
104+
/>
105+
106+
## Thickness and color
107+
108+
`thickness` sets the line weight in dp and `containerColor` overrides the line color.
109+
Both default to the MD3 values (1dp, `outlineVariant`) — reach for them for emphasis
110+
rules, not for ordinary content separation.
111+
112+
<SnackExample
113+
name="RootNative Divider — Thickness & Color"
114+
code={`
115+
import { Column, Divider } from '@rootnative/components'
116+
import { ThemeProvider, useTheme } from '@rootnative/core'
117+
import { SafeAreaProvider } from 'react-native-safe-area-context'
118+
119+
function Demo() {
120+
const theme = useTheme()
121+
return (
122+
<Column gap="lg" style={{ padding: 24 }}>
123+
<Divider />
124+
<Divider thickness={2} />
125+
<Divider thickness={4} containerColor={theme.colors.primary} />
126+
</Column>
127+
)
128+
}
129+
130+
export default function App() {
131+
return (
132+
<SafeAreaProvider>
133+
<ThemeProvider>
134+
<Demo />
135+
</ThemeProvider>
136+
</SafeAreaProvider>
137+
)
138+
}
139+
`}
140+
/>
141+
142+
## Relationship to `ListDivider`
143+
144+
`ListDivider` — exported from `@rootnative/components/list` — is an alias of `Divider`
145+
with the same props. It stays exported so existing list code keeps working; new code
146+
should import `Divider` directly.
147+
148+
## Props
149+
150+
<PropTable name="Divider" />

‎docs/docs/components/keyboard-avoiding-wrapper.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 2
2+
sidebar_position: 3
33
---
44

55
# KeyboardAvoidingWrapper

0 commit comments

Comments
 (0)