@gnome-ui/react-native 1.0.0
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 +714 -0
- package/dist/GnomeProvider/GnomeContext.d.ts +36 -0
- package/dist/GnomeProvider/GnomeProvider.d.ts +69 -0
- package/dist/GnomeProvider/index.d.ts +6 -0
- package/dist/GnomeProvider/resolveContext.d.ts +30 -0
- package/dist/components/ActionRow/ActionRow.d.ts +51 -0
- package/dist/components/ActionRow/index.d.ts +2 -0
- package/dist/components/BoxedList/BoxedList.d.ts +36 -0
- package/dist/components/BoxedList/index.d.ts +2 -0
- package/dist/components/Button/Button.d.ts +47 -0
- package/dist/components/Button/index.d.ts +2 -0
- package/dist/components/Card/Card.d.ts +34 -0
- package/dist/components/Card/index.d.ts +2 -0
- package/dist/components/Checkbox/Checkbox.d.ts +44 -0
- package/dist/components/Checkbox/index.d.ts +2 -0
- package/dist/components/HeaderBar/HeaderBar.d.ts +38 -0
- package/dist/components/HeaderBar/index.d.ts +2 -0
- package/dist/components/Link/Link.d.ts +33 -0
- package/dist/components/Link/index.d.ts +2 -0
- package/dist/components/PathBar/PathBar.d.ts +43 -0
- package/dist/components/PathBar/index.d.ts +2 -0
- package/dist/components/ProgressBar/ProgressBar.d.ts +72 -0
- package/dist/components/ProgressBar/index.d.ts +2 -0
- package/dist/components/RadioButton/RadioButton.d.ts +36 -0
- package/dist/components/RadioButton/index.d.ts +2 -0
- package/dist/components/SearchBar/SearchBar.d.ts +51 -0
- package/dist/components/SearchBar/index.d.ts +2 -0
- package/dist/components/Separator/Separator.d.ts +26 -0
- package/dist/components/Separator/index.d.ts +2 -0
- package/dist/components/Sidebar/Sidebar.d.ts +45 -0
- package/dist/components/Sidebar/SidebarItem.d.ts +37 -0
- package/dist/components/Sidebar/SidebarSection.d.ts +42 -0
- package/dist/components/Sidebar/filterUtils.d.ts +11 -0
- package/dist/components/Sidebar/index.d.ts +6 -0
- package/dist/components/Skeleton/Skeleton.d.ts +51 -0
- package/dist/components/Skeleton/index.d.ts +2 -0
- package/dist/components/Spinner/Spinner.d.ts +44 -0
- package/dist/components/Spinner/index.d.ts +2 -0
- package/dist/components/Switch/Switch.d.ts +34 -0
- package/dist/components/Switch/index.d.ts +2 -0
- package/dist/components/Tabs/TabBar.d.ts +35 -0
- package/dist/components/Tabs/TabItem.d.ts +54 -0
- package/dist/components/Tabs/TabPanel.d.ts +28 -0
- package/dist/components/Tabs/index.d.ts +6 -0
- package/dist/components/Text/Text.d.ts +26 -0
- package/dist/components/Text/index.d.ts +2 -0
- package/dist/components/TextField/TextField.d.ts +37 -0
- package/dist/components/TextField/index.d.ts +2 -0
- package/dist/components/Toast/Toast.d.ts +58 -0
- package/dist/components/Toast/Toaster.d.ts +45 -0
- package/dist/components/Toast/index.d.ts +4 -0
- package/dist/components/ViewSwitcher/ViewSwitcher.d.ts +32 -0
- package/dist/components/ViewSwitcher/ViewSwitcherItem.d.ts +27 -0
- package/dist/components/ViewSwitcher/index.d.ts +4 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +2906 -0
- package/dist/index.js.map +1 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/resolveTheme.d.ts +9 -0
- package/dist/theme/tokens.generated.d.ts +714 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
# @gnome-ui/react-native
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/ElJijuna/gnome-ui/main/public/assets/gnome-ui.png" alt="gnome-ui" width="120" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
React Native component library following the [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/), targeting iOS/Android/GNOME-mobile shells.
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@gnome-ui/react-native)
|
|
10
|
+
[](https://github.com/eljijuna/gnome-ui/actions/workflows/ci.yml)
|
|
11
|
+
[](../../LICENSE)
|
|
12
|
+
|
|
13
|
+
> **Status:** theme tokens, `GnomeProvider`, Tier 1 Base (`Button`, `Text`,
|
|
14
|
+
> `Link`, `TextField`, `Switch`, `Checkbox`, `RadioButton`), Tier 2 Layout &
|
|
15
|
+
> Containers (`Separator`, `Card`, `BoxedList`, `ActionRow`, `HeaderBar`),
|
|
16
|
+
> and Tier 3 Navigation (`Tabs`, `ViewSwitcher`, `Sidebar`, `SearchBar`,
|
|
17
|
+
> `PathBar`) fully ported. Tier 4 Feedback in progress: `Spinner`,
|
|
18
|
+
> `ProgressBar`, `Skeleton`, and `Toast`/`Toaster` shipped — `Banner`,
|
|
19
|
+
> `Dialog`, `Tooltip`, `Status Page`, and `AnimatedIcon` remain. Component
|
|
20
|
+
> ports from `@gnome-ui/react` continue tier by tier. See
|
|
21
|
+
> [ROADMAP.md](../../ROADMAP.md) Priority 3.
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
Bare React Native — no Expo SDK dependency, so the package works in both
|
|
26
|
+
Expo-managed and bare RN apps. Components are rebuilt with native primitives
|
|
27
|
+
(`View`, `Text`, `Pressable`, `StyleSheet`) rather than ported 1:1 from
|
|
28
|
+
`@gnome-ui/react`'s DOM-based JSX, but mirror its component API/props where
|
|
29
|
+
the platforms overlap.
|
|
30
|
+
|
|
31
|
+
## Theme tokens
|
|
32
|
+
|
|
33
|
+
`scripts/generate-theme.mjs` parses `@gnome-ui/core`'s `src/tokens.css` with
|
|
34
|
+
`postcss` and emits `src/theme/tokens.generated.ts` — four plain JS objects
|
|
35
|
+
(`lightTheme`, `darkTheme`, `highContrastTheme`, `highContrastDarkTheme`),
|
|
36
|
+
one per `@media (prefers-color-scheme)` / `@media (prefers-contrast)`
|
|
37
|
+
combination in the source CSS. `var()` chains are resolved at generation
|
|
38
|
+
time following the same cascade order as the CSS (base → dark → high
|
|
39
|
+
contrast → high contrast + dark), so each object is a flat, fully-resolved
|
|
40
|
+
map — no runtime CSS engine needed.
|
|
41
|
+
|
|
42
|
+
Units are converted to what RN styles expect: `px`/`rem` lengths become bare
|
|
43
|
+
dp numbers (1rem = 16), `ms` durations become numbers, modern
|
|
44
|
+
`rgb(r g b / a)` colors become `rgba(r, g, b, a)` strings, `cubic-bezier()`
|
|
45
|
+
becomes a 4-number array, and a font stack like `"Adwaita Sans", cantarell,
|
|
46
|
+
…` is reduced to just `"Adwaita Sans"` (RN's `fontFamily` takes one native
|
|
47
|
+
family name — the app still has to load `@gnome-ui/core`'s `.ttf` files).
|
|
48
|
+
|
|
49
|
+
`oklch()` (unsupported by RN's color parser), `box-shadow` strings, and the
|
|
50
|
+
two `clamp()`-based sidebar-width tokens aren't auto-converted — they're
|
|
51
|
+
still available, unconverted, in the matching `*RawTokens` export (e.g.
|
|
52
|
+
`lightRawTokens['--gnome-shadow-md']`) so nothing is silently lost.
|
|
53
|
+
|
|
54
|
+
Pick a variant at runtime with `resolveGnomeTheme`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { resolveGnomeTheme } from '@gnome-ui/react-native';
|
|
58
|
+
import { useColorScheme } from 'react-native';
|
|
59
|
+
|
|
60
|
+
const colorScheme = useColorScheme() ?? 'light';
|
|
61
|
+
const theme = resolveGnomeTheme({ colorScheme, contrast: 'normal' });
|
|
62
|
+
|
|
63
|
+
theme.accentColor; // '#3584e4'
|
|
64
|
+
theme.space2; // 12
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The generated file is committed, but always regenerate it after changing
|
|
68
|
+
`@gnome-ui/core`'s tokens — `npm run theme:generate`, or just run `build`
|
|
69
|
+
/ `typecheck` / `test`, which each regenerate it first.
|
|
70
|
+
|
|
71
|
+
## GnomeProvider
|
|
72
|
+
|
|
73
|
+
RN has no CSS cascade, so components can't read a custom-property-style
|
|
74
|
+
theme the way `@gnome-ui/react`'s components do — they need the resolved
|
|
75
|
+
theme object handed to them directly. `GnomeProvider` computes it once and
|
|
76
|
+
exposes it (plus locale, direction, and formatting defaults) via context:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { GnomeProvider, useGnomeTheme } from '@gnome-ui/react-native';
|
|
80
|
+
import { Text, View } from 'react-native';
|
|
81
|
+
|
|
82
|
+
function App() {
|
|
83
|
+
return (
|
|
84
|
+
<GnomeProvider accentColor="green">
|
|
85
|
+
<Screen />
|
|
86
|
+
</GnomeProvider>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Screen() {
|
|
91
|
+
const theme = useGnomeTheme();
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<View style={{ backgroundColor: theme.windowBgColor, padding: theme.space4 }}>
|
|
95
|
+
<Text style={{ color: theme.windowFgColor, fontSize: theme.fontSizeBody }}>Hello</Text>
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`colorScheme` and `contrast` both default to `"system"`: color scheme
|
|
102
|
+
follows `useColorScheme()`/`Appearance`, and contrast follows the OS
|
|
103
|
+
accessibility setting where one exists — Android's "High text contrast",
|
|
104
|
+
iOS's "Increase Contrast" — falling back to `"normal"` elsewhere (e.g. web).
|
|
105
|
+
Pass `"light"`/`"dark"` or `"normal"`/`"more"` to override either
|
|
106
|
+
explicitly.
|
|
107
|
+
|
|
108
|
+
`accentColor` accepts a named Adwaita palette color (`"green"`, `"red"`,
|
|
109
|
+
…) — resolved to the matching shade for the active color scheme, same as
|
|
110
|
+
`@gnome-ui/react` — or any RN color string. It's threaded through
|
|
111
|
+
`theme.accentColor`/`theme.accentBgColor` (and `theme.focusRingColor`
|
|
112
|
+
outside high contrast, which keeps its own fixed value there for maximum
|
|
113
|
+
contrast, matching `tokens.css`).
|
|
114
|
+
|
|
115
|
+
Other hooks: `useLocale`, `useDir`, `useNumberFormatter`,
|
|
116
|
+
`useDateTimeFormatter`, `useColorScheme`/`useResolvedColorScheme`,
|
|
117
|
+
`useContrast`/`useResolvedContrast`, `useAccentColor` — each reads one slice
|
|
118
|
+
of the same context, mirroring `@gnome-ui/react`'s `GnomeProvider` hook set.
|
|
119
|
+
|
|
120
|
+
Unlike the web provider, `dir` is exposed for consumers to branch on but
|
|
121
|
+
never calls `I18nManager.forceRTL()` — RN's layout direction is a single
|
|
122
|
+
global flag that needs an app reload and is set once at bootstrap, not per
|
|
123
|
+
provider tree.
|
|
124
|
+
|
|
125
|
+
`useReducedMotion()` is the one hook in this set **not** scoped to
|
|
126
|
+
`GnomeProvider`'s context — it reads the OS "Reduce Motion" accessibility
|
|
127
|
+
setting (`AccessibilityInfo.isReduceMotionEnabled`/`reduceMotionChanged`,
|
|
128
|
+
supported on both iOS and Android) directly and works without a provider
|
|
129
|
+
at all. Unlike `contrast`/`colorScheme`, the web `GnomeProvider` has no
|
|
130
|
+
corresponding override prop for this — `prefers-reduced-motion` is a pure
|
|
131
|
+
CSS media query there, always OS-driven — so there's nothing to mirror on
|
|
132
|
+
the context side. Any component with a continuously looping `Animated`
|
|
133
|
+
value (e.g. `Spinner`) reads it to slow down or skip that animation.
|
|
134
|
+
|
|
135
|
+
## Components
|
|
136
|
+
|
|
137
|
+
### Button
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { Button } from '@gnome-ui/react-native';
|
|
141
|
+
|
|
142
|
+
<Button variant="suggested" onPress={() => save()}>
|
|
143
|
+
Save
|
|
144
|
+
</Button>;
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Mirrors `@gnome-ui/react`'s `Button` props (`variant`, `size`, `shape`, `osd`,
|
|
148
|
+
`leadingIcon`/`trailingIcon`), rebuilt on `Pressable` — hover/`:active` CSS
|
|
149
|
+
states become the `pressed` render-prop, and `filter: brightness()` (not
|
|
150
|
+
available in RN) becomes a `0.85` opacity dip on press for the solid
|
|
151
|
+
`suggested`/`destructive` variants. `leadingIcon`/`trailingIcon` render
|
|
152
|
+
as-is: RN has no `currentColor` equivalent, so size and color icons
|
|
153
|
+
yourself, matching the resolved label color (`theme.accentFgColor`,
|
|
154
|
+
`theme.destructiveFgColor`, `theme.windowFgColor`, …) if you want them to
|
|
155
|
+
match.
|
|
156
|
+
|
|
157
|
+
### Text
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { Text } from '@gnome-ui/react-native';
|
|
161
|
+
|
|
162
|
+
<Text variant="title-1">Settings</Text>
|
|
163
|
+
<Text variant="caption" color="dim">Last synced 5 minutes ago</Text>;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
All 12 Adwaita text styles — `large-title`, `title-1`–`title-4`, `heading`,
|
|
167
|
+
`body`, `document`, `caption`, `caption-heading`, `monospace`, `numeric` —
|
|
168
|
+
and the same 7 semantic colors as `@gnome-ui/react` (`default`, `dim`,
|
|
169
|
+
`accent`, `destructive`, `success`, `warning`, `error`).
|
|
170
|
+
|
|
171
|
+
| Variant | Role | Use case |
|
|
172
|
+
|---------|------|----------|
|
|
173
|
+
| `large-title` | `header` | Display heading with lots of whitespace |
|
|
174
|
+
| `title-1` | `header` | Primary screen title |
|
|
175
|
+
| `title-2` | `header` | Section title |
|
|
176
|
+
| `title-3` | `header` | Sub-section title |
|
|
177
|
+
| `title-4` | `header` | Minor heading |
|
|
178
|
+
| `heading` | `header` | UI labels, boxed list headers |
|
|
179
|
+
| `body` | — | Default UI text, descriptions |
|
|
180
|
+
| `document` | — | Reading content (chat, articles) |
|
|
181
|
+
| `caption` | — | Sub-text, metadata |
|
|
182
|
+
| `caption-heading` | — | Small group labels (uppercase) |
|
|
183
|
+
| `monospace` | — | Code, logs, shell commands |
|
|
184
|
+
| `numeric` | — | Aligned numbers, counters |
|
|
185
|
+
|
|
186
|
+
RN has no element to choose, so `@gnome-ui/react`'s `as` prop is replaced by
|
|
187
|
+
`accessibilityRole`: the six heading variants default to `"header"` — the
|
|
188
|
+
native equivalent of `<h1>`–`<h4>`, and what the VoiceOver/TalkBack heading
|
|
189
|
+
rotor reads — and passing `accessibilityRole` explicitly overrides that.
|
|
190
|
+
|
|
191
|
+
Three CSS-only typography features are resolved at render time instead:
|
|
192
|
+
relative `line-height` ratios and `em` letter-spacing become absolute dp
|
|
193
|
+
against each variant's own font size (RN accepts nothing else), and
|
|
194
|
+
`color="dim"` stays an *opacity* rather than a flat gray, so it keeps
|
|
195
|
+
working over any background — exactly what `.color-dim` does on the web.
|
|
196
|
+
|
|
197
|
+
Everything else is plain RN `Text`: `numberOfLines`, `selectable`,
|
|
198
|
+
`onPress`, `adjustsFontSizeToFit` and the rest of `TextProps` pass straight
|
|
199
|
+
through, `style` merges over the variant style, and `ref` reaches the
|
|
200
|
+
underlying host `Text`.
|
|
201
|
+
|
|
202
|
+
### Link
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
import { Link } from '@gnome-ui/react-native';
|
|
206
|
+
|
|
207
|
+
<Link href="https://gnome.org" external>
|
|
208
|
+
GNOME
|
|
209
|
+
</Link>;
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`href` is opened via `Linking.openURL` when pressed. Pass a custom `onPress`
|
|
213
|
+
to hand it to a router instead (e.g. `navigation.navigate` for an internal
|
|
214
|
+
link) — that fully replaces the default `Linking.openURL` call rather than
|
|
215
|
+
running alongside it.
|
|
216
|
+
|
|
217
|
+
RN's `Pressable` has no `:hover`, so the underline the web `Link` reveals on
|
|
218
|
+
hover instead reveals on press — the closest native equivalent — alongside
|
|
219
|
+
the same `0.7` press-opacity dip as `@gnome-ui/react`'s `:active` state.
|
|
220
|
+
|
|
221
|
+
`external` appends a trailing ↗ indicator and sets an `"Opens in browser"`
|
|
222
|
+
accessibility hint on the pressable; the indicator itself is hidden from
|
|
223
|
+
accessibility (`accessibilityElementsHidden`) since the hint already
|
|
224
|
+
announces the same thing, mirroring the web version's `aria-label` on its
|
|
225
|
+
icon span. Unlike the web `Link`, RN has no tab concept, so `external` is
|
|
226
|
+
purely presentational — `href` always opens the same way regardless.
|
|
227
|
+
|
|
228
|
+
### TextField
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
import { TextField } from '@gnome-ui/react-native';
|
|
232
|
+
|
|
233
|
+
<TextField
|
|
234
|
+
label="Username"
|
|
235
|
+
helperText="Enter your username"
|
|
236
|
+
value={username}
|
|
237
|
+
onChangeText={setUsername}
|
|
238
|
+
/>;
|
|
239
|
+
|
|
240
|
+
<TextField label="Email" error="This field is required" />;
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
RN has no `<label htmlFor>`/`aria-describedby` pairing, so `label` doubles
|
|
244
|
+
as `accessibilityLabel` and `error`/`helperText` doubles as
|
|
245
|
+
`accessibilityHint` on the underlying `TextInput` — announced together the
|
|
246
|
+
same way `aria-describedby` reads them on the web. `error`, when set,
|
|
247
|
+
replaces `helperText` in both the rendered hint row and the accessibility
|
|
248
|
+
hint, and colors the border and hint text with `theme.errorColor`.
|
|
249
|
+
|
|
250
|
+
There's no `:focus-visible` distinction on RN, so the accent border on
|
|
251
|
+
focus is plain `onFocus`/`onBlur` state rather than a keyboard-only ring;
|
|
252
|
+
there's also no outer `box-shadow`, so focus is a border-color change only,
|
|
253
|
+
not a grown ring like the web version's. The web `disabled` prop is RN's
|
|
254
|
+
own `editable={false}` — mirrored as a dimmed wrapper (label, input, and
|
|
255
|
+
hint together), matching `@gnome-ui/react`'s `.disabled` wrapper class.
|
|
256
|
+
|
|
257
|
+
`style` targets the wrapping `View`; `inputStyle` targets the `TextInput`
|
|
258
|
+
itself. Everything else (`value`, `onChangeText`, `placeholder`,
|
|
259
|
+
`keyboardType`, `secureTextEntry`, …) is plain RN `TextInputProps`, and
|
|
260
|
+
`ref` reaches the underlying `TextInput`.
|
|
261
|
+
|
|
262
|
+
### Switch
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
import { useState } from 'react';
|
|
266
|
+
import { Switch } from '@gnome-ui/react-native';
|
|
267
|
+
|
|
268
|
+
function WifiRow() {
|
|
269
|
+
const [enabled, setEnabled] = useState(true);
|
|
270
|
+
|
|
271
|
+
return <Switch value={enabled} onValueChange={setEnabled} accessibilityLabel="Wi-Fi" />;
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Rebuilt on `Pressable`/`Animated.View` rather than ported from
|
|
276
|
+
`@gnome-ui/react`'s `<input type="checkbox" role="switch">`: RN has no
|
|
277
|
+
checkbox primitive to skin, and the platform-supplied `Switch` can't be
|
|
278
|
+
made to match Adwaita, so the track and thumb are drawn by hand.
|
|
279
|
+
`value`/`onValueChange` (not `checked`/`onChange`) mirror RN's own `Switch`
|
|
280
|
+
API instead — the ecosystem convention this component overlaps with. It's
|
|
281
|
+
fully controlled: there's no `defaultValue` escape hatch, matching the
|
|
282
|
+
platform's own `Switch`.
|
|
283
|
+
|
|
284
|
+
Track background and border color animate on every `value` change after
|
|
285
|
+
the initial mount — matching the CSS `transition` on `.switch` — using
|
|
286
|
+
`theme.durationFast`/`theme.easingDefault`; the thumb's fill color does not
|
|
287
|
+
animate, since the source CSS only transitions the thumb's `transform`, not
|
|
288
|
+
its `background-color`.
|
|
289
|
+
|
|
290
|
+
Two of `Switch.module.css`'s colors are hardcoded per color scheme inside a
|
|
291
|
+
component-level `@media (prefers-color-scheme: dark)` block rather than
|
|
292
|
+
driven by a semantic token. Unlike `Button`/`Link`/`TextField`, which each
|
|
293
|
+
read a single token that already resolves correctly per theme, the
|
|
294
|
+
unchecked track/thumb colors here branch explicitly on
|
|
295
|
+
`useResolvedColorScheme()` to match.
|
|
296
|
+
|
|
297
|
+
### Checkbox
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import { useState } from 'react';
|
|
301
|
+
import { Checkbox } from '@gnome-ui/react-native';
|
|
302
|
+
|
|
303
|
+
function TermsRow() {
|
|
304
|
+
const [accepted, setAccepted] = useState(false);
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<Checkbox value={accepted} onValueChange={setAccepted} accessibilityLabel="Accept terms" />
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// "Select all" with a mixed group:
|
|
312
|
+
<Checkbox value={allSelected} indeterminate={someSelected && !allSelected} onValueChange={selectAll} />;
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Three states — unchecked, checked, and `indeterminate` (mixed) — same as
|
|
316
|
+
`@gnome-ui/react`'s. `value`/`onValueChange` mirror `Switch`'s convention
|
|
317
|
+
rather than the web version's `checked`/`onChange`.
|
|
318
|
+
|
|
319
|
+
RN has no `indeterminate` DOM property to set imperatively — the entire
|
|
320
|
+
reason the web version needs a ref and an effect — so here it's just a
|
|
321
|
+
render branch: `indeterminate` draws a short bar, otherwise a checkmark,
|
|
322
|
+
both fading in on the same `Animated.Value` that drives the border/
|
|
323
|
+
background transition. The checkmark itself is a `✓` glyph rather than the
|
|
324
|
+
web version's `clip-path` polygon, since this package has no SVG dependency
|
|
325
|
+
to draw one exactly — the same Unicode-glyph fallback `Link`'s external-link
|
|
326
|
+
indicator already established for a small decorative mark.
|
|
327
|
+
|
|
328
|
+
The idle border color is another case (like `Switch`) where the source CSS
|
|
329
|
+
hardcodes a palette swatch per color scheme rather than a token that
|
|
330
|
+
already resolves per theme, so it branches on `useResolvedColorScheme()` —
|
|
331
|
+
and on `useResolvedContrast()` for the high-contrast border color/width —
|
|
332
|
+
to match.
|
|
333
|
+
|
|
334
|
+
### RadioButton
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
import { useState } from 'react';
|
|
338
|
+
import { RadioButton } from '@gnome-ui/react-native';
|
|
339
|
+
|
|
340
|
+
function SizeOptions() {
|
|
341
|
+
const [size, setSize] = useState<'sm' | 'md' | 'lg'>('md');
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<>
|
|
345
|
+
{(['sm', 'md', 'lg'] as const).map((option) => (
|
|
346
|
+
<RadioButton
|
|
347
|
+
key={option}
|
|
348
|
+
value={size === option}
|
|
349
|
+
onSelect={() => setSize(option)}
|
|
350
|
+
accessibilityLabel={option}
|
|
351
|
+
/>
|
|
352
|
+
))}
|
|
353
|
+
</>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Reuses `Checkbox`'s exact border/background transition technique — same
|
|
359
|
+
`Animated.Value`, same mount-skip guard so it never animates before the
|
|
360
|
+
user touches it, same `useResolvedColorScheme()`/`useResolvedContrast()`
|
|
361
|
+
branching for the idle border color — just circular, with a filled dot
|
|
362
|
+
instead of a checkmark, and no indeterminate state.
|
|
363
|
+
|
|
364
|
+
The web version's `<input type="radio" name="...">` groups mutually
|
|
365
|
+
exclusive options natively via the shared `name` attribute; RN has no
|
|
366
|
+
equivalent, so grouping is fully manual — render one `RadioButton` per
|
|
367
|
+
option and drive `value` from shared selection state in the parent, same
|
|
368
|
+
as any other controlled list of options. `onSelect` (not `onValueChange`)
|
|
369
|
+
only fires when pressed while unselected, matching native radio semantics:
|
|
370
|
+
pressing an already-selected radio is a no-op, so there's no boolean to
|
|
371
|
+
report back.
|
|
372
|
+
|
|
373
|
+
### Separator
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
import { Separator } from '@gnome-ui/react-native';
|
|
377
|
+
|
|
378
|
+
<Separator />;
|
|
379
|
+
<Separator orientation="vertical" style={{ height: 24 }} />;
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Thin dividing line — the first component from Tier 2 (Layout &
|
|
383
|
+
Containers). Color comes entirely from `theme.cardShadeColor`, which
|
|
384
|
+
already resolves correctly per color scheme, so — unlike `Switch`/
|
|
385
|
+
`Checkbox`/`RadioButton` — there's no `useResolvedColorScheme()` branching
|
|
386
|
+
needed here.
|
|
387
|
+
|
|
388
|
+
Rebuilt as a plain `View` rather than ported from `@gnome-ui/react`'s
|
|
389
|
+
`<hr>`/`<div role="separator">`: RN's `AccessibilityRole` union has no
|
|
390
|
+
`"separator"` value, so — since a divider carries no information a screen
|
|
391
|
+
reader user needs — it's excluded from the accessibility tree entirely
|
|
392
|
+
with `accessible={false}`, the RN-idiomatic way to mark a purely
|
|
393
|
+
decorative element.
|
|
394
|
+
|
|
395
|
+
### Sidebar
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
import { Sidebar, SidebarItem, SidebarSection } from '@gnome-ui/react-native';
|
|
399
|
+
|
|
400
|
+
<Sidebar>
|
|
401
|
+
<SidebarSection title="Mailboxes">
|
|
402
|
+
<SidebarItem label="Inbox" icon={<InboxIcon />} active onPress={() => go('inbox')} />
|
|
403
|
+
<SidebarItem label="Starred" icon={<StarIcon />} suffix={<Text variant="caption">3</Text>} />
|
|
404
|
+
</SidebarSection>
|
|
405
|
+
<SidebarSection title="Labels" collapsible>
|
|
406
|
+
<SidebarItem label="Work" />
|
|
407
|
+
<SidebarItem label="Archived" disabled />
|
|
408
|
+
</SidebarSection>
|
|
409
|
+
</Sidebar>;
|
|
410
|
+
|
|
411
|
+
// Rail (icon-only) mode:
|
|
412
|
+
<Sidebar collapsed>…</Sidebar>;
|
|
413
|
+
|
|
414
|
+
// Controlled filtering — pair with your own search input:
|
|
415
|
+
<Sidebar filter={query}>…</Sidebar>;
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Lateral navigation panel. Consecutive top-level children (typically
|
|
419
|
+
`SidebarSection`s) get a `Separator` inserted between them — the same
|
|
420
|
+
divider-on-index-boundary technique `BoxedList` uses for its rows — standing
|
|
421
|
+
in for the web version's `.section + .section` adjacent-sibling CSS rule,
|
|
422
|
+
which RN has no equivalent of. A child that `filter` hides is excluded from
|
|
423
|
+
that index count too, so a lone visible row never ends up sandwiched between
|
|
424
|
+
two stray dividers.
|
|
425
|
+
|
|
426
|
+
`SidebarSection` is `collapsible` via its header `Pressable` or imperatively
|
|
427
|
+
through a `ref` (`expand`/`collapse`/`toggle`) — the body stays mounted and
|
|
428
|
+
toggles `display: 'none'` rather than unmounting, the same "stays mounted
|
|
429
|
+
but hidden" approach `TabPanel` uses, instead of porting the web version's
|
|
430
|
+
animated CSS-grid collapse. In rail (`collapsed`) mode every section header
|
|
431
|
+
is hidden and every body is always shown.
|
|
432
|
+
|
|
433
|
+
Dropped relative to `@gnome-ui/react`'s `Sidebar`/`SidebarItem`: `searchable`
|
|
434
|
+
(would pull in a `SearchBar`, not yet ported to this package — use `filter`
|
|
435
|
+
with your own input instead), `mode`/auto page-layout switch (depends on the
|
|
436
|
+
web-only `useBreakpoint` hook), `variant` (tinted/blurred backgrounds — the
|
|
437
|
+
blurred variant needs a native blur view this package doesn't depend on),
|
|
438
|
+
`tooltip` (no `Tooltip` port yet, and nothing to trigger one from on a
|
|
439
|
+
touch-first device), `menuItems` (context menu — no portal/positioning
|
|
440
|
+
primitive exists in this package yet), and `onDrop`/`acceptTypes` (HTML5
|
|
441
|
+
drag-and-drop has no RN equivalent without a gesture-handler dependency this
|
|
442
|
+
package doesn't have).
|
|
443
|
+
|
|
444
|
+
### SearchBar
|
|
445
|
+
|
|
446
|
+
```tsx
|
|
447
|
+
import { SearchBar } from '@gnome-ui/react-native';
|
|
448
|
+
|
|
449
|
+
<SearchBar
|
|
450
|
+
open
|
|
451
|
+
value={query}
|
|
452
|
+
onChangeText={setQuery}
|
|
453
|
+
onClear={() => setQuery('')}
|
|
454
|
+
onClose={() => setOpen(false)}
|
|
455
|
+
/>;
|
|
456
|
+
|
|
457
|
+
// Filter chips below the bar:
|
|
458
|
+
<SearchBar open value={query} onChangeText={setQuery}>
|
|
459
|
+
<Chip label="Apps" />
|
|
460
|
+
<Chip label="Documents" />
|
|
461
|
+
</SearchBar>;
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Collapsible search input. `open={false}` renders nothing at all rather than
|
|
465
|
+
porting the web version's CSS height/opacity transition — no established
|
|
466
|
+
animated-height pattern exists yet in this package (the same trade-off
|
|
467
|
+
`SidebarSection` made for its collapsible body) — and mounting on
|
|
468
|
+
`open={true}` auto-focuses the input, standing in for the web version's
|
|
469
|
+
`requestAnimationFrame`-on-open focus effect.
|
|
470
|
+
|
|
471
|
+
`onClose` renders a trailing "Cancel" button rather than being wired to an
|
|
472
|
+
Escape keypress: touch keyboards have no reliable Escape key, so a visible
|
|
473
|
+
button is the RN-idiomatic stand-in. The clear (×) button appears whenever
|
|
474
|
+
`value` is non-empty, mirroring the web version, and both icons are Unicode
|
|
475
|
+
glyphs (`🔍`/`×`) rather than `@gnome-ui/icons`, matching every other
|
|
476
|
+
no-SVG-dependency component in this package.
|
|
477
|
+
|
|
478
|
+
Dropped relative to `@gnome-ui/react`'s `SearchBar`: the `suggestions` /
|
|
479
|
+
`onSuggestionSelect` / `loadingSuggestions` / `renderSuggestion` /
|
|
480
|
+
`suggestionsLabel` autocomplete popover — it depends on a portal +
|
|
481
|
+
viewport-anchored positioning primitive (`createPortal` +
|
|
482
|
+
`getBoundingClientRect`) this package doesn't have yet, the same gap that
|
|
483
|
+
dropped `SidebarItem`'s `menuItems` context menu — and a `Spinner`
|
|
484
|
+
component, not yet ported.
|
|
485
|
+
|
|
486
|
+
### PathBar
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
import { PathBar } from '@gnome-ui/react-native';
|
|
490
|
+
|
|
491
|
+
<PathBar
|
|
492
|
+
segments={[
|
|
493
|
+
{ label: 'Home', path: '/home' },
|
|
494
|
+
{ label: 'Documents', path: '/home/documents' },
|
|
495
|
+
{ label: 'Projects', path: '/home/documents/projects' },
|
|
496
|
+
]}
|
|
497
|
+
onNavigate={(path, index) => go(path)}
|
|
498
|
+
/>;
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Breadcrumb location bar. Segments are separated by a `›` chevron; every
|
|
502
|
+
segment except the last is a pressable button that calls `onNavigate` with
|
|
503
|
+
its `path` and index, and the last segment renders as a static bold label —
|
|
504
|
+
the current location.
|
|
505
|
+
|
|
506
|
+
Rebuilt with `Pressable`/`View`/`Text` rather than ported from
|
|
507
|
+
`@gnome-ui/react`'s `<nav><ol><li>`: RN's `AccessibilityRole` union has
|
|
508
|
+
neither a "navigation" landmark nor a breadcrumb-list role (the same gap
|
|
509
|
+
that dropped `Sidebar`'s `<nav>` role), so those are dropped rather than
|
|
510
|
+
faked — each interactive segment still gets its own
|
|
511
|
+
`accessibilityRole="button"` and `accessibilityLabel`. The separator is a
|
|
512
|
+
Unicode `›` glyph instead of the web version's inline SVG chevron, matching
|
|
513
|
+
this package's established no-SVG-dependency convention.
|
|
514
|
+
|
|
515
|
+
### Spinner
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
import { Spinner } from '@gnome-ui/react-native';
|
|
519
|
+
|
|
520
|
+
<Spinner />;
|
|
521
|
+
<Spinner size="lg" label="Syncing your library…" />;
|
|
522
|
+
|
|
523
|
+
// Rendered alongside your own label instead of announcing its own:
|
|
524
|
+
<Spinner label="" />;
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Indeterminate loading ring — the first component from Tier 4 (Feedback).
|
|
528
|
+
`size` is `"sm"` | `"md"` | `"lg"` (16/24/36px). `label` defaults to
|
|
529
|
+
`"Loading…"`; pass `""` to silence it when a sibling label already
|
|
530
|
+
describes the loading state (mirrors the web version's same convention).
|
|
531
|
+
|
|
532
|
+
Rebuilt on `Animated.View` rather than ported from `@gnome-ui/react`'s
|
|
533
|
+
pure-CSS `@keyframes spin`: the ring itself reuses the same per-side-border
|
|
534
|
+
trick the CSS does (`borderColor` for the track, `borderTopColor` for the
|
|
535
|
+
accent-colored "head", on a fully-rounded circle) — RN's `View` supports
|
|
536
|
+
independent per-side border colors too, so that part translates directly.
|
|
537
|
+
The rotation is an `Animated.loop`d `Animated.timing` driving a `rotate`
|
|
538
|
+
transform with `useNativeDriver: true`. `useReducedMotion()` (see
|
|
539
|
+
`GnomeProvider` above) mirrors the source CSS's own
|
|
540
|
+
`@media (prefers-reduced-motion: reduce) { animation-duration: 2s }` —
|
|
541
|
+
slowed to 2s, not stopped outright, matching the web behavior exactly
|
|
542
|
+
rather than dropping the animation entirely.
|
|
543
|
+
|
|
544
|
+
RN's `AccessibilityRole` union has no "status" value (the web version's
|
|
545
|
+
`role="status"`); `"progressbar"` is the closest match for an
|
|
546
|
+
indeterminate loading indicator, with no `accessibilityValue` set — RN's
|
|
547
|
+
equivalent of omitting `aria-valuenow` for an indeterminate progress bar.
|
|
548
|
+
|
|
549
|
+
### ProgressBar
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
import { ProgressBar } from '@gnome-ui/react-native';
|
|
553
|
+
|
|
554
|
+
<ProgressBar value={0.6} accessibilityLabel="Download progress" />;
|
|
555
|
+
<ProgressBar variant="success" value={1} />;
|
|
556
|
+
|
|
557
|
+
// Indeterminate — unknown duration:
|
|
558
|
+
<ProgressBar accessibilityLabel="Loading" />;
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Determinate and indeterminate progress bar. `value` (0–1) shows exact
|
|
562
|
+
progress with an animated width transition on every change; omit it for
|
|
563
|
+
an indeterminate 40%-wide bar that slides left to right on a loop.
|
|
564
|
+
`variant` is `"accent"` (default) | `"success"` | `"warning"` | `"error"`.
|
|
565
|
+
|
|
566
|
+
`useReducedMotion()` (see `GnomeProvider` above) is honored per the
|
|
567
|
+
*source CSS's own* per-state behavior rather than one uniform rule:
|
|
568
|
+
determinate width changes simply skip the transition, while the
|
|
569
|
+
indeterminate pulse stops entirely and freezes as a static, full-width,
|
|
570
|
+
50%-opacity bar — exactly what the source
|
|
571
|
+
`@media (prefers-reduced-motion: reduce)` block does. This differs from
|
|
572
|
+
`Spinner`, whose reduced-motion behavior *slows* its animation instead of
|
|
573
|
+
stopping it outright — each component mirrors its own source CSS rather
|
|
574
|
+
than a single reduced-motion policy applied uniformly across the package.
|
|
575
|
+
|
|
576
|
+
`role="progressbar"` maps directly to RN's own `accessibilityRole` (no
|
|
577
|
+
substitution needed, unlike `Spinner`'s web `role="status"`).
|
|
578
|
+
`accessibilityValue` carries `min`/`max`/`now` for the determinate case;
|
|
579
|
+
the indeterminate case omits all three — RN's equivalent of the web
|
|
580
|
+
version omitting `aria-valuenow`/`aria-valuemin`/`aria-valuemax`. The web
|
|
581
|
+
version's `aria-labelledby` (an id-relationship prop) has no RN
|
|
582
|
+
equivalent — RN has no DOM ids — so only `aria-label`
|
|
583
|
+
(`accessibilityLabel`) is ported.
|
|
584
|
+
|
|
585
|
+
### Skeleton
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
import { Skeleton } from '@gnome-ui/react-native';
|
|
589
|
+
|
|
590
|
+
<Skeleton />;
|
|
591
|
+
<Skeleton width={220} height={16} />;
|
|
592
|
+
<Skeleton variant="circle" size={48} />;
|
|
593
|
+
<Skeleton variant="text" lines={3} />;
|
|
594
|
+
<Skeleton animated={false} />;
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Content-shaped loading placeholder — a pragmatic web-style extension for
|
|
598
|
+
layouts that benefit from placeholder shape (GNOME HIG itself recommends
|
|
599
|
+
`Spinner`/`ProgressBar` for loading states, but this is ported as-is from
|
|
600
|
+
`@gnome-ui/react` for parity). `variant` is `"rect"` (default, `width`/
|
|
601
|
+
`height`) | `"circle"` (`size` diameter) | `"text"` (`lines` rows, the
|
|
602
|
+
last one narrower).
|
|
603
|
+
|
|
604
|
+
The web version's shimmer is a `linear-gradient` swept across the shape
|
|
605
|
+
via `transform: translateX()`; this package has no gradient dependency
|
|
606
|
+
(no `expo-linear-gradient`/`react-native-linear-gradient` in its
|
|
607
|
+
dependency tree, and adding one for a single component would be scope
|
|
608
|
+
creep), so `animated` drives a plain opacity pulse instead — the same
|
|
609
|
+
1.4s round-trip cycle length as the web shimmer. This is a common
|
|
610
|
+
RN-idiomatic substitute for a CSS shimmer effect (compare Tailwind's own
|
|
611
|
+
`animate-pulse` utility, which uses the identical technique).
|
|
612
|
+
|
|
613
|
+
Unlike `Spinner` (slows) and `ProgressBar` (stops one state, slows the
|
|
614
|
+
other), `useReducedMotion()` here fully disables the pulse and shows a
|
|
615
|
+
static base color — mirroring the source CSS's own `animation: none`,
|
|
616
|
+
which has no partial-motion in-between state to preserve. Each Tier 4
|
|
617
|
+
component's reduced-motion behavior follows its own source CSS rather
|
|
618
|
+
than one policy applied uniformly across the package.
|
|
619
|
+
|
|
620
|
+
`accessible={false}` mirrors the web version's `aria-hidden="true"` — a
|
|
621
|
+
loading placeholder carries no information a screen reader user needs,
|
|
622
|
+
the same reasoning `Separator` already established for a purely
|
|
623
|
+
decorative element.
|
|
624
|
+
|
|
625
|
+
### Toast / Toaster
|
|
626
|
+
|
|
627
|
+
```tsx
|
|
628
|
+
import { Toast, Toaster } from '@gnome-ui/react-native';
|
|
629
|
+
|
|
630
|
+
function App() {
|
|
631
|
+
const [toasts, setToasts] = useState<{ id: number; message: string }[]>([]);
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<View style={{ flex: 1 }}>
|
|
635
|
+
<YourAppContent />
|
|
636
|
+
|
|
637
|
+
<Toaster>
|
|
638
|
+
{toasts.map((t) => (
|
|
639
|
+
<Toast
|
|
640
|
+
key={t.id}
|
|
641
|
+
title={t.message}
|
|
642
|
+
dismissible
|
|
643
|
+
onDismiss={() => setToasts((prev) => prev.filter((x) => x.id !== t.id))}
|
|
644
|
+
/>
|
|
645
|
+
))}
|
|
646
|
+
</Toaster>
|
|
647
|
+
</View>
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Non-blocking temporary notification. `Toast` auto-dismisses after
|
|
653
|
+
`duration` ms (default 3000, `0` disables it) and stays fully prop-driven
|
|
654
|
+
— you own the list of active toasts and remove one from it in `onDismiss`,
|
|
655
|
+
exactly like the web version. `Toaster` stacks them, positioned
|
|
656
|
+
`"bottom"` (default) or `"top"`.
|
|
657
|
+
|
|
658
|
+
RN has no `document.body`/portal target to render into the way the web
|
|
659
|
+
version's `createPortal` does, so there's no `container` prop — mount
|
|
660
|
+
`Toaster` yourself as the **last** child of your app's root-level `View`
|
|
661
|
+
so it paints on top of everything else (see the example above).
|
|
662
|
+
`pointerEvents="box-none"` on `Toaster` is the RN equivalent of the web
|
|
663
|
+
version's `pointer-events: none` on the container: empty space around the
|
|
664
|
+
stack doesn't intercept touches, but each `Toast` (a `Pressable`) still
|
|
665
|
+
handles its own.
|
|
666
|
+
|
|
667
|
+
The timer-pause behavior is ported verbatim (`setTimeout`/`Date.now()`
|
|
668
|
+
bookkeeping, no DOM API involved) — only the *trigger* changes: RN has no
|
|
669
|
+
hover, so `onPressIn`/`onPressOut` (touch-down/touch-up) stand in for the
|
|
670
|
+
web version's `onMouseEnter`/`onMouseLeave`, pausing the auto-dismiss
|
|
671
|
+
timer while the user is actively touching the toast. There's no
|
|
672
|
+
`onFocus`/`onBlur`-triggered pause either — the card itself isn't
|
|
673
|
+
focusable in RN's touch-first model, only its action/dismiss buttons are,
|
|
674
|
+
and RN has no "focus-within" primitive to detect that.
|
|
675
|
+
|
|
676
|
+
The entrance is an `Animated.timing` fading + sliding + scaling in,
|
|
677
|
+
matching the web version's `@keyframes toast-in`; `useReducedMotion()`
|
|
678
|
+
skips straight to the settled state. RN's `AccessibilityRole` union has no
|
|
679
|
+
"status" value (the web version's `role="status"`); `"alert"` is the
|
|
680
|
+
closest available role, paired with `accessibilityLiveRegion="polite"`
|
|
681
|
+
(Android's live-region API) as the nearest match to `aria-live="polite"`.
|
|
682
|
+
|
|
683
|
+
## Installation
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
npm install @gnome-ui/react-native react-native
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
## Example app
|
|
690
|
+
|
|
691
|
+
[`apps/react-native-example`](../../apps/react-native-example) is a
|
|
692
|
+
Storybook-style gallery for every component in this package, runnable in
|
|
693
|
+
[Expo Go](https://expo.dev/go) — no native build needed. From the repo
|
|
694
|
+
root: `npm start --workspace=@gnome-ui/react-native-example`.
|
|
695
|
+
|
|
696
|
+
## Testing
|
|
697
|
+
|
|
698
|
+
This package uses **Jest**, not the Vitest used elsewhere in the monorepo.
|
|
699
|
+
`react-native`'s published entry ships untranspiled Flow syntax
|
|
700
|
+
(`import typeof * as X from './index.js.flow'`) that only Jest's official
|
|
701
|
+
`@react-native/jest-preset` + `@react-native/babel-preset` know how to
|
|
702
|
+
strip before the module loads — Vitest has no hook that reaches a plain
|
|
703
|
+
`require("react-native")` call inside an already-external dependency (confirmed
|
|
704
|
+
against both `resolve.alias` and a custom `resolveId` plugin), so component
|
|
705
|
+
tests run under `jest.config.cjs` / `babel.config.cjs` instead. Pure-logic
|
|
706
|
+
tests (`resolveTheme`, `resolveContext`) run under the same Jest setup for
|
|
707
|
+
consistency rather than splitting the package across two runners.
|
|
708
|
+
|
|
709
|
+
Rendering uses `@testing-library/react-native` on top of `test-renderer`
|
|
710
|
+
(the actively maintained successor to the deprecated `react-test-renderer`).
|
|
711
|
+
|
|
712
|
+
## License
|
|
713
|
+
|
|
714
|
+
[MIT](../../LICENSE) © el_jijuna
|