@oxyhq/bloom 1.0.0 → 1.0.2
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/README.md +16 -4
- package/docs/accordion.mdx +69 -0
- package/docs/activity-heatmap.mdx +71 -0
- package/docs/admonition.mdx +77 -0
- package/docs/alert-dialog.mdx +75 -0
- package/docs/alert.mdx +93 -0
- package/docs/animated-check.mdx +53 -0
- package/docs/aspect-ratio.mdx +45 -0
- package/docs/avatar-group.mdx +91 -0
- package/docs/avatar.mdx +126 -0
- package/docs/badge.mdx +74 -0
- package/docs/benefit-list.mdx +56 -0
- package/docs/bottom-sheet.mdx +103 -0
- package/docs/button.mdx +218 -0
- package/docs/card.mdx +106 -0
- package/docs/checkbox.mdx +59 -0
- package/docs/chip.mdx +53 -0
- package/docs/code.mdx +50 -0
- package/docs/combobox.mdx +64 -0
- package/docs/command.mdx +85 -0
- package/docs/composition-bar.mdx +63 -0
- package/docs/connection-dots.mdx +50 -0
- package/docs/connection-status.mdx +56 -0
- package/docs/content-panel.mdx +62 -0
- package/docs/context-menu.mdx +65 -0
- package/docs/design-tokens.mdx +254 -0
- package/docs/dialog.mdx +277 -0
- package/docs/divider.mdx +56 -0
- package/docs/dot-grid-meter.mdx +66 -0
- package/docs/dropdown-menu.mdx +161 -0
- package/docs/error-boundary.mdx +72 -0
- package/docs/fab.mdx +67 -0
- package/docs/field.mdx +70 -0
- package/docs/fill.mdx +58 -0
- package/docs/fonts.mdx +144 -0
- package/docs/frosted-icon-button.mdx +60 -0
- package/docs/getting-started.mdx +75 -0
- package/docs/grid.mdx +64 -0
- package/docs/hooks.mdx +81 -0
- package/docs/icon-circle.mdx +66 -0
- package/docs/icons.mdx +69 -0
- package/docs/image-aspect-ratio-cache.mdx +63 -0
- package/docs/image-resolver.mdx +66 -0
- package/docs/index.mdx +44 -0
- package/docs/input-group.mdx +58 -0
- package/docs/item.mdx +69 -0
- package/docs/kbd.mdx +57 -0
- package/docs/label.mdx +50 -0
- package/docs/link-preview.mdx +66 -0
- package/docs/list.mdx +72 -0
- package/docs/loading.mdx +129 -0
- package/docs/media-inset-border.mdx +52 -0
- package/docs/menubar.mdx +86 -0
- package/docs/migrating-to-1.0.mdx +434 -0
- package/docs/motion.mdx +83 -0
- package/docs/overlay.mdx +75 -0
- package/docs/popover.mdx +73 -0
- package/docs/portal.mdx +63 -0
- package/docs/pressable-scale.mdx +51 -0
- package/docs/profile-card.mdx +66 -0
- package/docs/progressive-blur.mdx +54 -0
- package/docs/prompt-input.mdx +183 -0
- package/docs/provider.mdx +64 -0
- package/docs/radio-indicator.mdx +60 -0
- package/docs/radio.mdx +65 -0
- package/docs/scroll.mdx +66 -0
- package/docs/search.mdx +67 -0
- package/docs/segmented-control.mdx +76 -0
- package/docs/select.mdx +93 -0
- package/docs/settings-list.mdx +186 -0
- package/docs/skeleton.mdx +66 -0
- package/docs/slider.mdx +73 -0
- package/docs/stat-bar.mdx +59 -0
- package/docs/stat-widgets.mdx +33 -0
- package/docs/styles.mdx +61 -0
- package/docs/subtle-hover.mdx +53 -0
- package/docs/surfaces.mdx +72 -0
- package/docs/switch.mdx +90 -0
- package/docs/tab-bar.mdx +386 -0
- package/docs/tabs.mdx +75 -0
- package/docs/text-field.mdx +187 -0
- package/docs/theme.mdx +67 -0
- package/docs/toast.mdx +285 -0
- package/docs/tooltip.mdx +67 -0
- package/docs/typography.mdx +88 -0
- package/docs/user-hover-card.mdx +140 -0
- package/docs/zoomable-image-gallery.mdx +66 -0
- package/lib/commonjs/design-tokens/color-roles.js +21 -1
- package/lib/commonjs/design-tokens/color-roles.js.map +1 -1
- package/lib/commonjs/design-tokens/index.js +6 -0
- package/lib/commonjs/design-tokens/index.js.map +1 -1
- package/lib/commonjs/design-tokens/tailwind-preset.js +3 -1
- package/lib/commonjs/design-tokens/tailwind-preset.js.map +1 -1
- package/lib/commonjs/design-tokens/theme-css.js +1 -0
- package/lib/commonjs/design-tokens/theme-css.js.map +1 -1
- package/lib/commonjs/design-tokens/theme.css +3 -0
- package/lib/commonjs/design-tokens/tokens-json.js +13 -4
- package/lib/commonjs/design-tokens/tokens-json.js.map +1 -1
- package/lib/commonjs/design-tokens/tokens.json +6227 -265
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +7 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/theme/ambient-store.js +16 -1
- package/lib/commonjs/theme/ambient-store.js.map +1 -1
- package/lib/commonjs/theme/color-policy.js +30 -33
- package/lib/commonjs/theme/color-policy.js.map +1 -1
- package/lib/commonjs/theme/color-presets.js +413 -166
- package/lib/commonjs/theme/color-presets.js.map +1 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js +1 -0
- package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/commonjs/theme/glass-colors.js +18 -21
- package/lib/commonjs/theme/glass-colors.js.map +1 -1
- package/lib/commonjs/theme/index.js +30 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/commonjs/theme/index.web.js +30 -0
- package/lib/commonjs/theme/index.web.js.map +1 -1
- package/lib/module/design-tokens/color-roles.js +20 -0
- package/lib/module/design-tokens/color-roles.js.map +1 -1
- package/lib/module/design-tokens/index.js +2 -2
- package/lib/module/design-tokens/index.js.map +1 -1
- package/lib/module/design-tokens/tailwind-preset.js +4 -2
- package/lib/module/design-tokens/tailwind-preset.js.map +1 -1
- package/lib/module/design-tokens/theme-css.js +2 -1
- package/lib/module/design-tokens/theme-css.js.map +1 -1
- package/lib/module/design-tokens/theme.css +3 -0
- package/lib/module/design-tokens/tokens-json.js +14 -5
- package/lib/module/design-tokens/tokens-json.js.map +1 -1
- package/lib/module/design-tokens/tokens.json +6227 -265
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/theme/ambient-store.js +16 -1
- package/lib/module/theme/ambient-store.js.map +1 -1
- package/lib/module/theme/color-policy.js +30 -33
- package/lib/module/theme/color-policy.js.map +1 -1
- package/lib/module/theme/color-presets.js +412 -165
- package/lib/module/theme/color-presets.js.map +1 -1
- package/lib/module/theme/color-scope/seed-scope.js +2 -1
- package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/module/theme/glass-colors.js +18 -21
- package/lib/module/theme/glass-colors.js.map +1 -1
- package/lib/module/theme/index.js +1 -1
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/index.web.js +1 -1
- package/lib/module/theme/index.web.js.map +1 -1
- package/lib/typescript/commonjs/design-tokens/color-roles.d.ts +20 -0
- package/lib/typescript/commonjs/design-tokens/color-roles.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/index.d.ts +3 -3
- package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/theme-css.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +12 -3
- package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/ambient-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-presets.d.ts +386 -62
- package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/glass-colors.d.ts +18 -21
- package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/index.d.ts +2 -2
- package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/theme/index.web.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/color-roles.d.ts +20 -0
- package/lib/typescript/module/design-tokens/color-roles.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/index.d.ts +3 -3
- package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/tailwind-preset.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/theme-css.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/tokens-json.d.ts +12 -3
- package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/theme/ambient-store.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-presets.d.ts +386 -62
- package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/module/theme/glass-colors.d.ts +18 -21
- package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
- package/lib/typescript/module/theme/index.d.ts +2 -2
- package/lib/typescript/module/theme/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/index.web.d.ts +2 -2
- package/lib/typescript/module/theme/index.web.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/__tests__/support/press-host.ts +30 -0
- package/src/__tests__/support/rendered-style.ts +99 -0
- package/src/__tests__/support/unread-hook-fixture.ts +33 -0
- package/src/design-tokens/color-roles.ts +21 -0
- package/src/design-tokens/index.ts +3 -2
- package/src/design-tokens/tailwind-preset.ts +3 -2
- package/src/design-tokens/theme-css.ts +2 -1
- package/src/design-tokens/theme.css +3 -0
- package/src/design-tokens/tokens-json.ts +29 -9
- package/src/design-tokens/tokens.json +6227 -265
- package/src/glass/GlassSurface.tsx +2 -2
- package/src/index.ts +2 -0
- package/src/index.web.ts +2 -0
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +4082 -0
- package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
- package/src/theme/ambient-store.ts +21 -1
- package/src/theme/color-policy.ts +33 -38
- package/src/theme/color-presets.ts +473 -146
- package/src/theme/color-scope/seed-scope.ts +2 -1
- package/src/theme/glass-colors.ts +18 -21
- package/src/theme/index.ts +16 -1
- package/src/theme/index.web.ts +16 -1
package/docs/tabs.mdx
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tabs
|
|
3
|
+
description: Switching which panel is shown, controlled from the outside.
|
|
4
|
+
order: 26
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Tabs
|
|
8
|
+
|
|
9
|
+
`Tabs` is the **strip**, and only the strip: the triggers, the sliding
|
|
10
|
+
indicator and the horizontal scroll. It is controlled — `value` is the open tab
|
|
11
|
+
and `onValueChange` reports a press — and the caller renders the panel for
|
|
12
|
+
whatever `value` currently is.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Tabs, TabsTrigger } from '@oxyhq/bloom/tabs';
|
|
16
|
+
|
|
17
|
+
<Tabs value={tab} onValueChange={setTab}>
|
|
18
|
+
<TabsTrigger value="posts" label="Posts" />
|
|
19
|
+
<TabsTrigger value="replies" label="Replies" />
|
|
20
|
+
<TabsTrigger value="media" label="Media" count={12} />
|
|
21
|
+
</Tabs>
|
|
22
|
+
|
|
23
|
+
{tab === 'posts' ? <Posts /> : null}
|
|
24
|
+
{tab === 'replies' ? <Replies /> : null}
|
|
25
|
+
{tab === 'media' ? <Media /> : null}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Keeping the panels outside is what lets a screen put the strip in a sticky
|
|
29
|
+
header, in a navigator, or beside the content — the strip does not have to be
|
|
30
|
+
an ancestor of what it switches.
|
|
31
|
+
|
|
32
|
+
## Which prop decides what
|
|
33
|
+
|
|
34
|
+
| prop | decides |
|
|
35
|
+
| --------------- | ------------------------------------------------------------- |
|
|
36
|
+
| `value` | the open tab |
|
|
37
|
+
| `onValueChange` | receives the pressed trigger's `value` |
|
|
38
|
+
| `hasSelection` | whether **any** tab is selected — see below |
|
|
39
|
+
| `variant` | `underline` · `filled` · `outlined` |
|
|
40
|
+
| `fullWidth` | spread the triggers across the container instead of hugging |
|
|
41
|
+
| `count` | a number beside a trigger's label |
|
|
42
|
+
|
|
43
|
+
## `hasSelection` exists for a state a tab strip usually cannot express
|
|
44
|
+
|
|
45
|
+
A filter row where nothing is chosen yet. Without it, the first tab looks
|
|
46
|
+
selected before the user has selected anything, and "All" and "no filter" are
|
|
47
|
+
indistinguishable.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<Tabs value={filter} hasSelection={filter != null} onValueChange={setFilter}>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## `count` is a quantity you are choosing between
|
|
54
|
+
|
|
55
|
+
3 drafts versus 41 sent — a number that helps you decide which tab to open. It
|
|
56
|
+
is not an unread badge; unread belongs on the thing that is unread, through
|
|
57
|
+
[`Badge`](/docs/bloom/main/badge), where `invisible` keeps the layout still as
|
|
58
|
+
the count reaches zero.
|
|
59
|
+
|
|
60
|
+
## Tabs, SegmentedControl or a Select
|
|
61
|
+
|
|
62
|
+
- **Tabs** — several views of one subject, with real content underneath.
|
|
63
|
+
- **[SegmentedControl](/docs/bloom/main/segmented-control)** — the choice *is*
|
|
64
|
+
the value (a setting). Announces as a radio group.
|
|
65
|
+
- **[Select](/docs/bloom/main/select)** — too many options to show at once, or
|
|
66
|
+
the options are data rather than fixed sections.
|
|
67
|
+
|
|
68
|
+
The first two look similar enough that the decision is worth making explicitly:
|
|
69
|
+
after the user picks, has something been *set*, or is the same thing now shown
|
|
70
|
+
differently?
|
|
71
|
+
|
|
72
|
+
## expo-router
|
|
73
|
+
|
|
74
|
+
`@oxyhq/bloom/tabs/expo-router` binds the strip to the router so `value` comes
|
|
75
|
+
from the current route and a press navigates, instead of a local `useState`.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: TextField
|
|
3
|
+
description: Composable text input — the field, its input, label, icon, suffix and ghost.
|
|
4
|
+
order: 11
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# TextField
|
|
8
|
+
|
|
9
|
+
A composable text input. `TextField` is the field itself; put a `TextFieldInput` inside it, plus any of `TextFieldIcon`, `TextFieldSuffix` and `TextFieldGhost`. The field tracks focus, hover and invalid state and hands it to those parts, so the whole chrome reacts as one.
|
|
10
|
+
|
|
11
|
+
## Basic
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { TextField, TextFieldInput } from '@oxyhq/bloom/text-field';
|
|
15
|
+
|
|
16
|
+
<TextField>
|
|
17
|
+
<TextFieldInput
|
|
18
|
+
label="Email"
|
|
19
|
+
value={value}
|
|
20
|
+
onChangeText={setValue}
|
|
21
|
+
keyboardType="email-address"
|
|
22
|
+
/>
|
|
23
|
+
</TextField>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`label` is required and is the input's accessible name. Without an explicit `placeholder` it doubles as the placeholder; pass `placeholder={null}` for none.
|
|
27
|
+
|
|
28
|
+
## One input needs no wrapper
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<TextFieldInput label="Email" value={value} onChangeText={setValue} />
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`TextFieldInput` on its own wraps itself in a `TextField`, so the common case is one element. Reach for the explicit `TextField` when you have more than one part to put in it, or when you need `isInvalid` or `radius` on the field.
|
|
35
|
+
|
|
36
|
+
## With an icon
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<TextField>
|
|
40
|
+
<TextFieldIcon icon={Envelope_Stroke2_Corner0_Rounded} />
|
|
41
|
+
<TextFieldInput label="Email" value={email} onChangeText={setEmail} />
|
|
42
|
+
</TextField>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The field lays its children out in a row and focuses the input when you click anywhere in it (web).
|
|
46
|
+
|
|
47
|
+
## Which parts need the field
|
|
48
|
+
|
|
49
|
+
`TextFieldIcon` and `TextFieldSuffix` read the field's focus, hover and invalid state, and **throw** when rendered outside a `TextField` — they have nothing to colour themselves against, and a part that renders anyway would look finished while never reacting to anything. The message names what is missing:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
TextField parts must be rendered inside a <TextField>.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`TextFieldInput` does not throw: a missing field is how it knows to wrap itself. `TextFieldLabel` sits above the field by design, and `TextFieldGhost` reads no state — both render anywhere.
|
|
56
|
+
|
|
57
|
+
## Validation
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
const [value, setValue] = useState('');
|
|
61
|
+
const [touched, setTouched] = useState(false);
|
|
62
|
+
const isInvalid = touched && !value.includes('@');
|
|
63
|
+
|
|
64
|
+
<TextField isInvalid={isInvalid}>
|
|
65
|
+
<TextFieldInput
|
|
66
|
+
label="Email"
|
|
67
|
+
value={value}
|
|
68
|
+
onChangeText={setValue}
|
|
69
|
+
onBlur={() => setTouched(true)}
|
|
70
|
+
/>
|
|
71
|
+
</TextField>
|
|
72
|
+
{isInvalid ? <Text style={{ color: theme.colors.negative }}>Enter a valid email</Text> : null}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`isInvalid` paints the field in the negative palette and reaches every part through context. `TextFieldInput` takes its own `isInvalid` too, for the wrapper-less spelling.
|
|
76
|
+
|
|
77
|
+
## Floating label
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<TextField>
|
|
81
|
+
<TextFieldInput floatingLabel label="Email" value={email} onChangeText={setEmail} />
|
|
82
|
+
</TextField>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The label starts inside the field and animates to a caption at the top on focus or once there is a value. State drives it, not CSS, so web and native behave identically; reduced motion snaps instead of animating.
|
|
86
|
+
|
|
87
|
+
## Multiline
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<TextField style={{ minHeight: 96, alignItems: 'flex-start' }}>
|
|
91
|
+
<TextFieldInput label="Bio" value={bio} onChangeText={setBio} multiline />
|
|
92
|
+
</TextField>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Suffix
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<TextField>
|
|
99
|
+
<TextFieldInput label="Workspace" value={url} onChangeText={setUrl} />
|
|
100
|
+
<TextFieldSuffix label="domain">.oxy.so</TextFieldSuffix>
|
|
101
|
+
</TextField>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Components
|
|
105
|
+
|
|
106
|
+
### `TextField`
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Description |
|
|
109
|
+
|------|------|-------------|
|
|
110
|
+
| `isInvalid?` | `boolean` | Paints the field in the negative palette and exposes it via context. |
|
|
111
|
+
| `radius?` | `number` | Corner radius of the chrome, default `10`. A large value (999) reads as a pill. |
|
|
112
|
+
| `style?` | `StyleProp<ViewStyle>` | Override the container style. |
|
|
113
|
+
| `children?` | `React.ReactNode` | The input, plus any icon / suffix / ghost. |
|
|
114
|
+
|
|
115
|
+
### `TextFieldInput`
|
|
116
|
+
|
|
117
|
+
Accepts React Native's `TextInputProps`, plus:
|
|
118
|
+
|
|
119
|
+
| Prop | Type | Description |
|
|
120
|
+
|------|------|-------------|
|
|
121
|
+
| `label` | `string` | Required. The accessible name, and the default placeholder. |
|
|
122
|
+
| `value?` / `onChangeText?` | `string` / `(v: string) => void` | The controlled value. |
|
|
123
|
+
| `placeholder?` | `string \| null` | Overrides the label as placeholder; `null` for none. |
|
|
124
|
+
| `isInvalid?` | `boolean` | For the wrapper-less spelling. |
|
|
125
|
+
| `floatingLabel?` | `boolean` | Material-style animated label. Default `false`. |
|
|
126
|
+
| `inputRef?` | `RefObject<TextInput \| null>` | Merged with the field's own ref. |
|
|
127
|
+
|
|
128
|
+
### `TextFieldIcon`
|
|
129
|
+
|
|
130
|
+
| Prop | Type | Description |
|
|
131
|
+
|------|------|-------------|
|
|
132
|
+
| `icon` | `React.ComponentType<SVGIconProps>` | A Bloom icon component. Takes the field's hover / focus / error colour. |
|
|
133
|
+
|
|
134
|
+
Renders before the input, so put it first.
|
|
135
|
+
|
|
136
|
+
### `TextFieldLabel`
|
|
137
|
+
|
|
138
|
+
A visible label rendered ABOVE the field, outside it:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<TextFieldLabel>Workspace</TextFieldLabel>
|
|
142
|
+
<TextField>
|
|
143
|
+
<TextFieldInput label="Workspace" value={name} onChangeText={setName} />
|
|
144
|
+
</TextField>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `TextFieldSuffix`
|
|
148
|
+
|
|
149
|
+
Inline trailing text — domain suffixes, units. `label` is its accessible name; `accessibilityHint` and `style` are optional.
|
|
150
|
+
|
|
151
|
+
### `TextFieldGhost`
|
|
152
|
+
|
|
153
|
+
Greyed completion text drawn behind the input, for "type `acme`, see `acme.oxy.so`". Takes the typed prefix as `children` and the suggested remainder as `value`; it is `aria-hidden`.
|
|
154
|
+
|
|
155
|
+
## Accessibility
|
|
156
|
+
|
|
157
|
+
- Clicking anywhere in the field focuses the input (web).
|
|
158
|
+
- `label` is the input's `accessibilityLabel`; the floating label itself is hidden from assistive tech so it is not announced twice.
|
|
159
|
+
- On web the browser's own focus ring is stripped, because it follows the input's box rather than the field's and pokes out of a pill's corners. The animated border IS the focus signal.
|
|
160
|
+
- Invalid state paints the chrome. Surface the reason as text under the field — colour alone is not an announcement.
|
|
161
|
+
|
|
162
|
+
## Recipes
|
|
163
|
+
|
|
164
|
+
### Password with visibility toggle
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
const [show, setShow] = useState(false);
|
|
168
|
+
|
|
169
|
+
<TextField>
|
|
170
|
+
<TextFieldInput
|
|
171
|
+
label="Password"
|
|
172
|
+
value={password}
|
|
173
|
+
onChangeText={setPassword}
|
|
174
|
+
secureTextEntry={!show}
|
|
175
|
+
/>
|
|
176
|
+
<Button
|
|
177
|
+
variant="icon"
|
|
178
|
+
icon={show ? <EyeSlash_Stroke2_Corner0_Rounded /> : <Eye_Stroke2_Corner0_Rounded />}
|
|
179
|
+
accessibilityLabel={show ? 'Hide password' : 'Show password'}
|
|
180
|
+
onPress={() => setShow(!show)}
|
|
181
|
+
/>
|
|
182
|
+
</TextField>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Search
|
|
186
|
+
|
|
187
|
+
Reach for `Search` rather than rebuilding it: it is a `TextField` at the pill radius with the magnifier, the clear button and the keyboard settings a query field wants.
|
package/docs/theme.mdx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Theme
|
|
3
|
+
description: Color presets, light/dark modes, and design tokens used by every Bloom component.
|
|
4
|
+
order: 2
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Theme
|
|
8
|
+
|
|
9
|
+
Wrap your app with `BloomThemeProvider`. It accepts controlled `mode` and `colorPreset` props — persist them however you like (AsyncStorage, Zustand, etc.).
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { BloomThemeProvider } from '@oxyhq/bloom/theme';
|
|
13
|
+
|
|
14
|
+
<BloomThemeProvider mode="system" colorPreset="teal">
|
|
15
|
+
<App />
|
|
16
|
+
</BloomThemeProvider>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Modes
|
|
20
|
+
|
|
21
|
+
| Mode | Behavior |
|
|
22
|
+
|------|----------|
|
|
23
|
+
| `light` | Force light theme. |
|
|
24
|
+
| `dark` | Force dark theme. |
|
|
25
|
+
| `system` | Follow OS preference (`useColorScheme`). |
|
|
26
|
+
| `adaptive` | iOS/Android native dynamic colors when available, falls back to `system`. |
|
|
27
|
+
|
|
28
|
+
## Color presets
|
|
29
|
+
|
|
30
|
+
10 named palettes:
|
|
31
|
+
|
|
32
|
+
`teal` · `blue` · `green` · `amber` · `red` · `purple` · `pink` · `sky` · `orange` · `mint`
|
|
33
|
+
|
|
34
|
+
Each preset has a tuned light + dark variant and a contrast-checked primary color.
|
|
35
|
+
|
|
36
|
+
## Reading theme values
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { useTheme } from '@oxyhq/bloom/theme';
|
|
40
|
+
|
|
41
|
+
function MyComponent() {
|
|
42
|
+
const theme = useTheme();
|
|
43
|
+
return (
|
|
44
|
+
<View style={{ backgroundColor: theme.colors.surface }}>
|
|
45
|
+
<Text style={{ color: theme.colors.text }}>{theme.isDark ? 'Dark' : 'Light'}</Text>
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Tokens
|
|
52
|
+
|
|
53
|
+
Every preset exposes:
|
|
54
|
+
|
|
55
|
+
- `colors.primary` — brand color.
|
|
56
|
+
- `colors.background` — page background.
|
|
57
|
+
- `colors.surface` — elevated surface (cards, sheets).
|
|
58
|
+
- `colors.text` — body text.
|
|
59
|
+
- `colors.muted` — secondary text.
|
|
60
|
+
- `colors.border` — divider lines.
|
|
61
|
+
- `colors.success` / `colors.warning` / `colors.danger` — semantic colors.
|
|
62
|
+
- `isDark` — boolean.
|
|
63
|
+
- `mode` — resolved mode (`'light' | 'dark'`).
|
|
64
|
+
|
|
65
|
+
<Callout variant="info" title="Web CSS variables">
|
|
66
|
+
On web, Bloom also writes the active palette to CSS variables on `:root`, so non-React surfaces (e.g. raw HTML pages) can theme themselves with `var(--bloom-primary)`.
|
|
67
|
+
</Callout>
|
package/docs/toast.mdx
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Toast
|
|
3
|
+
description: Passive notifications, powered by Bloom's own universal toast engine.
|
|
4
|
+
order: 5
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Toast
|
|
8
|
+
|
|
9
|
+
Passive notifications. Bloom ships its own engine — **one** implementation that runs
|
|
10
|
+
on native and on web (through react-native-web), so there is nothing extra to
|
|
11
|
+
install and the API is identical everywhere.
|
|
12
|
+
|
|
13
|
+
The engine is derived from [sonner-native](https://github.com/gunnartorfis/sonner-native)
|
|
14
|
+
v0.26.4 (MIT), itself a React Native port of [sonner](https://github.com/emilkowalski/sonner).
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
Mount the outlet once near the app root, inside `BloomThemeProvider`:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { ToastOutlet } from '@oxyhq/bloom/toast';
|
|
22
|
+
|
|
23
|
+
<BloomThemeProvider>
|
|
24
|
+
<App />
|
|
25
|
+
<ToastOutlet />
|
|
26
|
+
</BloomThemeProvider>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Apps that mount `OxyProvider` from `@oxyhq/services` already get an outlet and
|
|
30
|
+
should **not** mount a second one — two outlets render every toast twice.
|
|
31
|
+
|
|
32
|
+
Native apps must wrap the root in `GestureHandlerRootView` (as they already do for
|
|
33
|
+
`Dialog` / `BottomSheet`) for swipe-to-dismiss. On web the toast host provides its
|
|
34
|
+
own, so no setup is needed.
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { toast } from '@oxyhq/bloom';
|
|
40
|
+
|
|
41
|
+
toast('Saved');
|
|
42
|
+
toast.success('Profile updated');
|
|
43
|
+
toast.error('Network error', { duration: 5000 });
|
|
44
|
+
toast.warning('Please verify your email');
|
|
45
|
+
toast.info('A new version is available');
|
|
46
|
+
toast.loading('Uploading…');
|
|
47
|
+
|
|
48
|
+
const id = toast('Uploading…');
|
|
49
|
+
toast.dismiss(id); // one toast
|
|
50
|
+
toast.dismiss(); // all of them
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Every method returns the toast's id (`string | number`), so `const id = toast(…)`
|
|
54
|
+
followed by `toast.dismiss(id)` works. Passing your own `id` updates that toast in
|
|
55
|
+
place instead of adding another:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
toast.loading('Uploading…', { id: 'upload' });
|
|
59
|
+
toast.success('Uploaded', { id: 'upload' }); // replaces the row, keeps its position
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`toast(content, options?)` takes a string or a React element; anything else throws.
|
|
63
|
+
`options.type` is the public variant knob and the typed helpers above are shorthands
|
|
64
|
+
for it. `type: 'default'` (the default) renders a neutral toast — no icon, no tint.
|
|
65
|
+
|
|
66
|
+
### The variant colours the icon, not the surface
|
|
67
|
+
|
|
68
|
+
Every toast renders the SAME neutral surface — `backgroundSecondary` with the
|
|
69
|
+
`border`, `text` and `textSecondary` roles — whatever its `type`, and with
|
|
70
|
+
`richColors` on or off. The variant shows up only in the leading icon, which uses
|
|
71
|
+
the theme's first-class status tokens: `success`, `error`, `warning`, `info`. A
|
|
72
|
+
`loading` or variant-less toast gets `textSecondary`.
|
|
73
|
+
|
|
74
|
+
That is deliberate, and matches sonner. `toast.success('Saved')` should read as a
|
|
75
|
+
normal toast with a green check, not as a full brand-coloured card.
|
|
76
|
+
|
|
77
|
+
`richColors` lifts the status colour from the icon alone to the **border and
|
|
78
|
+
title** as well. That is all it does: the surface stays `backgroundSecondary`, and
|
|
79
|
+
the description, close button and action colours stay neutral, so a rich-coloured
|
|
80
|
+
row still reads as a toast rather than a status banner. All four status variants
|
|
81
|
+
behave identically under it — `warning` and `info` are not special-cased and get
|
|
82
|
+
exactly the same border+title treatment as `success` and `error`, because the
|
|
83
|
+
lift is the only tinting mechanism there is. `loading` and a variant-less toast
|
|
84
|
+
have no status colour, so `richColors` has nothing to make more prominent and
|
|
85
|
+
they stay fully neutral either way.
|
|
86
|
+
|
|
87
|
+
<Callout variant="info" title="Tinted status surfaces need new tokens">
|
|
88
|
+
There is no `warningSubtle` / `infoSubtle` (or `successSubtle` / `errorSubtle`)
|
|
89
|
+
pair to tint a surface with, which is why every variant gets the same border+title
|
|
90
|
+
lift instead. Bloom has `*Subtle` / `*SubtleForeground` pairs for the **brand**
|
|
91
|
+
roles (`primarySubtle`, `negativeSubtle`) only — those are not status colours, and
|
|
92
|
+
reaching for them here is what once made a success toast render brand-purple.
|
|
93
|
+
Genuinely tinted status surfaces would need that missing status token family: a
|
|
94
|
+
theme addition affecting every app, not something to fake from a brand pair or a
|
|
95
|
+
derived alpha.
|
|
96
|
+
</Callout>
|
|
97
|
+
|
|
98
|
+
### Recolouring toasts with `BloomColorScope`
|
|
99
|
+
|
|
100
|
+
`BloomColorScope` is a React context override, and React context flows through a
|
|
101
|
+
portal — so a scope **does** reach the toast even though the row renders into
|
|
102
|
+
`#bloom-portal-root` at the end of `<body>`. The catch is which side of the scope
|
|
103
|
+
the OUTLET is on, not which side the `toast()` call is on:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// ✅ tints toasts — the scope is an ancestor of the outlet
|
|
107
|
+
<BloomColorScope colorPreset="green">
|
|
108
|
+
<App />
|
|
109
|
+
<ToastOutlet />
|
|
110
|
+
</BloomColorScope>
|
|
111
|
+
|
|
112
|
+
// ❌ does NOT tint toasts — the outlet is outside the scope
|
|
113
|
+
<BloomColorScope colorPreset="green">
|
|
114
|
+
<SomeScreen /> {/* toast() called from in here */}
|
|
115
|
+
</BloomColorScope>
|
|
116
|
+
<ToastOutlet />
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Apps that mount `OxyProvider` are in the second shape.** It renders
|
|
120
|
+
`<ToastOutlet />` at app root, so a `BloomColorScope` wrapped around a screen deep
|
|
121
|
+
in the tree will not tint toasts fired from that screen — they follow the root
|
|
122
|
+
preset. To recolour toasts, move the scope up so it wraps the provider.
|
|
123
|
+
|
|
124
|
+
What a scope changes is the **preset-derived** roles: the surface, text and
|
|
125
|
+
border. Status colours are fixed theme tokens, so a `success` icon (and, under
|
|
126
|
+
`richColors`, the border and title) is the same green whichever preset is in
|
|
127
|
+
scope — only the neutral card around it moves.
|
|
128
|
+
|
|
129
|
+
### Width
|
|
130
|
+
|
|
131
|
+
A toast is a card, not a banner. The row is `width: 100%` up to a **388px cap**,
|
|
132
|
+
centred — which puts the visible card on 356px, sonner's reference width, plus its
|
|
133
|
+
two 16px gutters. Below a 388px viewport the cap never binds and the card stays
|
|
134
|
+
screen-width-minus-32, so phone portrait is unaffected; above it (desktop, tablet,
|
|
135
|
+
phone landscape) the card caps and centres instead of spanning the viewport.
|
|
136
|
+
|
|
137
|
+
Widen it per outlet through the existing style slot:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<ToastOutlet toastOptions={{ toastContainerStyle: { maxWidth: 600 } }} />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
That moves the card only. The swipe-to-dismiss threshold and the stacking depth
|
|
144
|
+
cue are fractions of the **default** cap, so a widened card keeps the default
|
|
145
|
+
gesture distances.
|
|
146
|
+
|
|
147
|
+
### Stacking
|
|
148
|
+
|
|
149
|
+
Toasts **stack** by default, the way sonner's do: the newest row sits in front at
|
|
150
|
+
full size, older ones sit behind it scaled down and offset by `gap` (8px), and
|
|
151
|
+
pressing the stack expands it into a readable column. Pressing outside collapses
|
|
152
|
+
it again, and timers pause for as long as it is expanded.
|
|
153
|
+
|
|
154
|
+
Measured on a 1280px viewport with three rows, front to back: `scaleX` 1 / 0.959 /
|
|
155
|
+
0.918 at 0 / -8 / -16px, expanding to 1 / 1 / 1 at 0 / -54 / -108px.
|
|
156
|
+
|
|
157
|
+
This is a deliberate deviation from sonner-**native**, which defaults it off —
|
|
158
|
+
that default is right for a phone and wrong for the desktop consumers Bloom also
|
|
159
|
+
serves. Two consequences to know about:
|
|
160
|
+
|
|
161
|
+
- Only the front row's action button is reachable while the stack is collapsed.
|
|
162
|
+
Expand it (a press) to reach the others.
|
|
163
|
+
- While more than one toast is live, a press on the row toggles the stack **and**
|
|
164
|
+
still calls the toast's own `onPress`.
|
|
165
|
+
- No press on a stacked row is inert: it dismisses if it lands on a ✕ that is
|
|
166
|
+
actually rendered (`closeButton` and `dismissible` both on), and otherwise
|
|
167
|
+
expands or collapses like the rest of the row.
|
|
168
|
+
|
|
169
|
+
Opt out per outlet for a surface that must show several toasts at once with no
|
|
170
|
+
interaction:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<ToastOutlet enableStacking={false} />
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Hover (web)
|
|
177
|
+
|
|
178
|
+
On web a **mouse** over the stack expands it and moving away collapses it again,
|
|
179
|
+
the way sonner does — no prop, no setup. Press keeps working everywhere and is the
|
|
180
|
+
only trigger on native and on touch: the handlers ignore any pointer that is not a
|
|
181
|
+
mouse, so a tap cannot expand-then-collapse the stack from its own
|
|
182
|
+
`pointerenter`/`pointerleave` pair. While a mouse is over the stack, a press does
|
|
183
|
+
not toggle the expansion (hover owns it there) but still runs the toast's `onPress`.
|
|
184
|
+
|
|
185
|
+
**The pointer pauses the auto-close timers for as long as it is over a toast**, so
|
|
186
|
+
hovering to read one cannot let it expire under the cursor. Measured: three rows
|
|
187
|
+
with the default 3000ms duration survive 5s of hover, then resume when the pointer
|
|
188
|
+
leaves. This holds with `enableStacking={false}` too — there the pointer pauses
|
|
189
|
+
without expanding anything, since a flat column has nothing to open.
|
|
190
|
+
|
|
191
|
+
Dismissing a row **out of** a hovered stack leaves the rest expanded and still
|
|
192
|
+
paused, rather than collapsing under the cursor and resuming. Without a pointer
|
|
193
|
+
holding it — on native, or once the mouse leaves — a stack still collapses itself
|
|
194
|
+
once one row is left, which is invisible (a lone row sits at the same offset and
|
|
195
|
+
scale either way) and is what stops that last toast's paused timer hanging forever.
|
|
196
|
+
|
|
197
|
+
## Options
|
|
198
|
+
|
|
199
|
+
Per-toast options (`ToastOptions`):
|
|
200
|
+
|
|
201
|
+
| Option | Type | Description |
|
|
202
|
+
|--------|------|-------------|
|
|
203
|
+
| `id?` | `string \| number` | Stable id. Reusing one updates that toast in place. |
|
|
204
|
+
| `type?` | `'default' \| 'success' \| 'error' \| 'warning' \| 'info' \| 'loading'` | Visual variant. `'default'` is neutral. |
|
|
205
|
+
| `duration?` | `number` | Milliseconds before auto-dismiss. Defaults to `3000`. `Infinity` never auto-closes. |
|
|
206
|
+
| `description?` | `string` | Secondary text under the title. |
|
|
207
|
+
| `action?` | `{ label, onClick }` \| `ReactNode` | Primary button, or your own node. |
|
|
208
|
+
| `cancel?` | `{ label, onClick }` \| `ReactNode` | Secondary button; the object form also dismisses. |
|
|
209
|
+
| `closeButton?` | `boolean` | Show the ✕ affordance. |
|
|
210
|
+
| `dismissible?` | `boolean` | Allow swipe / close. Defaults to `true`. |
|
|
211
|
+
| `position?` | `'top-center' \| 'bottom-center' \| 'center'` | Override the outlet's position for this toast. |
|
|
212
|
+
| `icon?` | `ReactNode` | Replaces the variant icon. |
|
|
213
|
+
| `richColors?` | `boolean` | Show the status colour more prominently: it moves from the icon alone to the border and title as well. The surface stays neutral, as do the description, close button and action colours. No effect on `loading` or a variant-less toast, which have no status colour. |
|
|
214
|
+
| `important?` | `boolean` | Announce assertively to screen readers. |
|
|
215
|
+
| `onDismiss?` / `onAutoClose?` | `(id) => void` | Called when the toast goes away. |
|
|
216
|
+
| `onPress?` | `() => void` | Press anywhere on the row. |
|
|
217
|
+
| `style?` / `styles?` / `unstyled?` | — | Escape hatches for the row's own styling. |
|
|
218
|
+
| `animation?` | `{ enter?, exit? }` | A predefined Reanimated builder, a `Keyframe`, or `'default'`. On web, prefer a **predefined** builder for `enter`: a custom `Keyframe` handed to Reanimated's `entering` gets frozen in place by its web layout-animation cleanup, which breaks the stack. Bloom's own default enter avoids that path entirely. |
|
|
219
|
+
|
|
220
|
+
Outlet options (`ToasterProps`) cover `position`, `offset`, `gap`, `visibleToasts`,
|
|
221
|
+
`duration`, `swipeToDismissDirection`, `closeButton`, `richColors`,
|
|
222
|
+
`enableStacking`, `autoWiggleOnUpdate`, `pauseWhenPageIsHidden`, `icons`,
|
|
223
|
+
`toastOptions` and `positionerStyle`. Their defaults all come from the exported
|
|
224
|
+
`toastDefaults`, so `<ToastOutlet position="top-center" />` changes only that one.
|
|
225
|
+
|
|
226
|
+
<Callout variant="info" title="Documented no-ops">
|
|
227
|
+
`theme` and `invert` are accepted but do nothing: light/dark and per-subtree
|
|
228
|
+
recolouring belong to `BloomThemeProvider` / `BloomColorScope`.
|
|
229
|
+
</Callout>
|
|
230
|
+
|
|
231
|
+
## Patterns
|
|
232
|
+
|
|
233
|
+
### Promise toast
|
|
234
|
+
|
|
235
|
+
The pending row shows a spinner; the promise's result is formatted into the final
|
|
236
|
+
title. `success` is a formatter, `error` is a formatter **or** a plain string:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
toast.promise(saveProfile(), {
|
|
240
|
+
loading: 'Saving…',
|
|
241
|
+
success: (profile) => `Saved ${profile.name}`,
|
|
242
|
+
error: (err) => `Failed: ${err instanceof Error ? err.message : 'unknown'}`,
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A promise toast cannot be swiped away while it is pending, and its auto-close timer
|
|
247
|
+
only starts once it settles.
|
|
248
|
+
|
|
249
|
+
### Action toast
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
toast('Item moved to trash', {
|
|
253
|
+
action: {
|
|
254
|
+
label: 'Undo',
|
|
255
|
+
onClick: () => restore(item.id),
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Fully custom row
|
|
261
|
+
|
|
262
|
+
`toast.custom` hands the whole row to your element — Bloom renders no surface,
|
|
263
|
+
icon or buttons around it, but still animates, stacks and swipes it:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
toast.custom(<MyRow />, { duration: 8000 });
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Above a native iOS modal
|
|
270
|
+
|
|
271
|
+
Bloom does not depend on `react-native-screens`. Inject the overlay host yourself
|
|
272
|
+
when a toast has to appear above a native modal:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import { FullWindowOverlay } from 'react-native-screens';
|
|
276
|
+
|
|
277
|
+
<ToastOutlet ToasterOverlayWrapper={FullWindowOverlay} />
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
<Callout variant="info" title="Toast vs alert()">
|
|
281
|
+
Reach for `toast()` for passive feedback (the user doesn't have to react). Use [`alert()`](/docs/bloom/main/alert) when you need a one-shot confirmation that blocks the rest of the UI.
|
|
282
|
+
</Callout>
|
|
283
|
+
|
|
284
|
+
Upgrading from 0.50.x? `show`, `api`, `DURATION`, `type Toast` and the composition
|
|
285
|
+
exports were removed, and the default position moved to `bottom-center`.
|
package/docs/tooltip.mdx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tooltip
|
|
3
|
+
description: A hint about an already-labelled control — controlled, because what opens it differs by platform.
|
|
4
|
+
order: 39
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Tooltip
|
|
8
|
+
|
|
9
|
+
A short hint attached to a control.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Tooltip, TooltipTrigger, TooltipTextBubble } from '@oxyhq/bloom/tooltip';
|
|
13
|
+
|
|
14
|
+
const [visible, setVisible] = useState(false);
|
|
15
|
+
|
|
16
|
+
<Tooltip visible={visible} onVisibleChange={setVisible} position="top">
|
|
17
|
+
<TooltipTrigger>
|
|
18
|
+
<Pressable
|
|
19
|
+
onHoverIn={() => setVisible(true)}
|
|
20
|
+
onHoverOut={() => setVisible(false)}
|
|
21
|
+
onPress={copy}
|
|
22
|
+
accessibilityLabel="Copy link"
|
|
23
|
+
>
|
|
24
|
+
<LinkIcon />
|
|
25
|
+
</Pressable>
|
|
26
|
+
</TooltipTrigger>
|
|
27
|
+
<TooltipTextBubble>Copies the link to your clipboard</TooltipTextBubble>
|
|
28
|
+
</Tooltip>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why it is controlled
|
|
32
|
+
|
|
33
|
+
What should open a tooltip differs by platform and by trigger — hover for a
|
|
34
|
+
desktop pointer, long-press on touch, focus for a keyboard — and the right
|
|
35
|
+
answer depends on what the trigger already does with a press. `Tooltip` takes
|
|
36
|
+
`visible` and leaves that decision at the call site rather than guessing.
|
|
37
|
+
|
|
38
|
+
`TooltipTextBubble` renders its own content surface, so it is a **sibling** of
|
|
39
|
+
the trigger inside `Tooltip`, not something you wrap in a `TooltipContent`.
|
|
40
|
+
|
|
41
|
+
## A tooltip is never the only place the information appears
|
|
42
|
+
|
|
43
|
+
It cannot be reached by touch without an interaction that also does something
|
|
44
|
+
else, and it dismisses on the next tap. So:
|
|
45
|
+
|
|
46
|
+
- The control still needs its own `accessibilityLabel`. A tooltip is a hint
|
|
47
|
+
about a labelled control, not a substitute for the label.
|
|
48
|
+
- Nothing required to complete a task goes in one. If the user must know it,
|
|
49
|
+
put it on the page.
|
|
50
|
+
|
|
51
|
+
## `position` is a preference
|
|
52
|
+
|
|
53
|
+
A bubble near a screen edge is nudged back into view, so `position="top"` on an
|
|
54
|
+
element at the top of the viewport still renders somewhere readable rather than
|
|
55
|
+
off screen.
|
|
56
|
+
|
|
57
|
+
## Two platform notes
|
|
58
|
+
|
|
59
|
+
The **web** tooltip is not portaled and takes no rank in the overlay stack; the
|
|
60
|
+
**native** one is portaled and does. So on native it participates in overlay
|
|
61
|
+
ordering and on web it does not — check for a `Portal` before reasoning about
|
|
62
|
+
what paints above what.
|
|
63
|
+
|
|
64
|
+
**Known gap on native:** `TooltipTrigger` measures in page coordinates while
|
|
65
|
+
the content renders into the root portal group, so a tooltip inside a
|
|
66
|
+
`BottomSheet` portals *outside* that window and lands in the wrong place.
|
|
67
|
+
Fixing it needs a real device — jest cannot see a native window boundary.
|