@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.
Files changed (63) hide show
  1. package/README.md +714 -0
  2. package/dist/GnomeProvider/GnomeContext.d.ts +36 -0
  3. package/dist/GnomeProvider/GnomeProvider.d.ts +69 -0
  4. package/dist/GnomeProvider/index.d.ts +6 -0
  5. package/dist/GnomeProvider/resolveContext.d.ts +30 -0
  6. package/dist/components/ActionRow/ActionRow.d.ts +51 -0
  7. package/dist/components/ActionRow/index.d.ts +2 -0
  8. package/dist/components/BoxedList/BoxedList.d.ts +36 -0
  9. package/dist/components/BoxedList/index.d.ts +2 -0
  10. package/dist/components/Button/Button.d.ts +47 -0
  11. package/dist/components/Button/index.d.ts +2 -0
  12. package/dist/components/Card/Card.d.ts +34 -0
  13. package/dist/components/Card/index.d.ts +2 -0
  14. package/dist/components/Checkbox/Checkbox.d.ts +44 -0
  15. package/dist/components/Checkbox/index.d.ts +2 -0
  16. package/dist/components/HeaderBar/HeaderBar.d.ts +38 -0
  17. package/dist/components/HeaderBar/index.d.ts +2 -0
  18. package/dist/components/Link/Link.d.ts +33 -0
  19. package/dist/components/Link/index.d.ts +2 -0
  20. package/dist/components/PathBar/PathBar.d.ts +43 -0
  21. package/dist/components/PathBar/index.d.ts +2 -0
  22. package/dist/components/ProgressBar/ProgressBar.d.ts +72 -0
  23. package/dist/components/ProgressBar/index.d.ts +2 -0
  24. package/dist/components/RadioButton/RadioButton.d.ts +36 -0
  25. package/dist/components/RadioButton/index.d.ts +2 -0
  26. package/dist/components/SearchBar/SearchBar.d.ts +51 -0
  27. package/dist/components/SearchBar/index.d.ts +2 -0
  28. package/dist/components/Separator/Separator.d.ts +26 -0
  29. package/dist/components/Separator/index.d.ts +2 -0
  30. package/dist/components/Sidebar/Sidebar.d.ts +45 -0
  31. package/dist/components/Sidebar/SidebarItem.d.ts +37 -0
  32. package/dist/components/Sidebar/SidebarSection.d.ts +42 -0
  33. package/dist/components/Sidebar/filterUtils.d.ts +11 -0
  34. package/dist/components/Sidebar/index.d.ts +6 -0
  35. package/dist/components/Skeleton/Skeleton.d.ts +51 -0
  36. package/dist/components/Skeleton/index.d.ts +2 -0
  37. package/dist/components/Spinner/Spinner.d.ts +44 -0
  38. package/dist/components/Spinner/index.d.ts +2 -0
  39. package/dist/components/Switch/Switch.d.ts +34 -0
  40. package/dist/components/Switch/index.d.ts +2 -0
  41. package/dist/components/Tabs/TabBar.d.ts +35 -0
  42. package/dist/components/Tabs/TabItem.d.ts +54 -0
  43. package/dist/components/Tabs/TabPanel.d.ts +28 -0
  44. package/dist/components/Tabs/index.d.ts +6 -0
  45. package/dist/components/Text/Text.d.ts +26 -0
  46. package/dist/components/Text/index.d.ts +2 -0
  47. package/dist/components/TextField/TextField.d.ts +37 -0
  48. package/dist/components/TextField/index.d.ts +2 -0
  49. package/dist/components/Toast/Toast.d.ts +58 -0
  50. package/dist/components/Toast/Toaster.d.ts +45 -0
  51. package/dist/components/Toast/index.d.ts +4 -0
  52. package/dist/components/ViewSwitcher/ViewSwitcher.d.ts +32 -0
  53. package/dist/components/ViewSwitcher/ViewSwitcherItem.d.ts +27 -0
  54. package/dist/components/ViewSwitcher/index.d.ts +4 -0
  55. package/dist/index.cjs +2 -0
  56. package/dist/index.cjs.map +1 -0
  57. package/dist/index.d.ts +23 -0
  58. package/dist/index.js +2906 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/theme/index.d.ts +4 -0
  61. package/dist/theme/resolveTheme.d.ts +9 -0
  62. package/dist/theme/tokens.generated.d.ts +714 -0
  63. 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
+ [![npm](https://img.shields.io/npm/v/@gnome-ui/react-native)](https://www.npmjs.com/package/@gnome-ui/react-native)
10
+ [![CI](https://github.com/eljijuna/gnome-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/eljijuna/gnome-ui/actions/workflows/ci.yml)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../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