@gnome-ui/react-native 1.9.0 → 1.11.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 CHANGED
@@ -25,14 +25,17 @@ React Native component library following the [GNOME Human Interface Guidelines](
25
25
  > (Tier 20), `Chip` (Tier 7), `IconButton`/`Drawer` (Tier 8/Tier 20), and
26
26
  > `Clamp` (Tier 6), `Box` (Tier 20), `WrapBox`/`ToggleGroup` (Tier 7), and
27
27
  > `InlineViewSwitcher` (Tier 8), `PreferencesGroup` (Tier 13), and
28
- > `EntryRow`/`PasswordEntryRow`/`ComboRow` (Tier 12), `ColorPicker`
28
+ > `EntryRow`/`PasswordEntryRow`/`ComboRow`/`SpinRow` (Tier 12), `SplitButton`
29
+ > (Tier 8), `ColorPicker`
29
30
  > (Tier 20), `Bin` (Tier 15), `Blockquote` (Tier 20), `ButtonRow`
30
31
  > (Tier 8), `Callout` (Tier 20), `CheckRow` (Tier 12), `ExpanderRow`
31
32
  > (Tier 8), `FieldGroup` (Tier 20), and `MultiSelectDropdown` (Tier 20)
32
33
  > also shipped, along with `FilterableMultiSelectDropdown` — an original
33
34
  > `@gnome-ui/react`-only component (not a GNOME HIG port) built once its
34
35
  > prerequisite `MultiSelectDropdown` landed — and `PasswordField`/
35
- > `RangeSlider`/`StatusBadge` (all Tier 20). `BottomTabBar` also shipped —
36
+ > `RangeSlider`/`StatusBadge`/`WidgetManager` (all Tier 20) `WidgetManager`
37
+ > was previously deferred as low-priority, built once named directly since
38
+ > every piece it composes had already shipped. `BottomTabBar` also shipped —
36
39
  > a React Native-only original with no `@gnome-ui/react` source at all
37
40
  > (desktop apps don't have a bottom tab bar pattern to mirror), built on
38
41
  > explicit request for the iOS/Android fixed bottom-navigation shape.
@@ -42,7 +45,9 @@ React Native component library following the [GNOME Human Interface Guidelines](
42
45
  > query to lean on. `ButtonContent` (Tier 15) also shipped — an icon+label
43
46
  > layout helper mostly redundant with `Button`'s own `leadingIcon`/
44
47
  > `trailingIcon`, kept for composing the same spacing outside `Button`
45
- > itself. Component ports from `@gnome-ui/react` continue tier by
48
+ > itself. `TagInput` (Tier 20) also shipped — a `WrapBox` + `Chip`
49
+ > composition, unblocked once both had shipped. Component ports from
50
+ > `@gnome-ui/react` continue tier by
46
51
  > tier — see this package's own [ROADMAP.md](./ROADMAP.md) for full
47
52
  > per-tier status against all 130 `@gnome-ui/react` components, and the
48
53
  > main [ROADMAP.md](../../ROADMAP.md) Priority 3 for the framework
@@ -182,6 +187,42 @@ yourself, matching the resolved label color (`theme.accentFgColor`,
182
187
  `theme.destructiveFgColor`, `theme.windowFgColor`, …) if you want them to
183
188
  match.
184
189
 
190
+ ### SplitButton
191
+
192
+ ```tsx
193
+ import { SplitButton, Button } from '@gnome-ui/react-native';
194
+
195
+ <SplitButton
196
+ label="Save"
197
+ onPress={() => save()}
198
+ dropdownContent={
199
+ <Button variant="flat" size="sm" onPress={() => saveAs()}>
200
+ Save as Template
201
+ </Button>
202
+ }
203
+ />;
204
+ ```
205
+
206
+ Primary action button with an attached dropdown arrow, mirroring
207
+ `AdwSplitButton` and `@gnome-ui/react`'s own `SplitButton`. Pressing the
208
+ label half fires `onPress`; pressing the arrow half opens a floating panel
209
+ with `dropdownContent` (menus, options, etc.). Supports the same
210
+ `default`/`suggested`/`destructive` variants as `Button`.
211
+
212
+ The label half is a real `Button` — its resting/pressed/disabled colors
213
+ come for free. The arrow half is a hand-rolled `Pressable`, not a second
214
+ `Button`: `Popover` (which supplies the floating panel) clones a prop-level
215
+ `accessibilityState` onto its trigger, and RN merges a spread prop object
216
+ outright rather than key-by-key — nesting a full `Button` there would
217
+ silently clobber `Button`'s own internal `accessibilityState={{ disabled
218
+ }}`. The hand-rolled `Pressable` owns `accessibilityState={{ disabled }}`
219
+ itself instead, letting `Popover`'s clone merge in `expanded` alongside it.
220
+ Its resting colors are derived from a small local color formula mirroring
221
+ `Button`'s own (unexported) one, so the two halves render pixel-identical
222
+ colors; they read as one connected control via zeroed shared inner corner
223
+ radii plus a 1px separator, the RN equivalent of the web CSS's split
224
+ `border-radius` and `.separator` span.
225
+
185
226
  ### Text
186
227
 
187
228
  ```tsx
@@ -1423,6 +1464,41 @@ thin composition of those same three pieces. `label` is required since the
1423
1464
  button has no visible text. Built as a genuine prerequisite for `Drawer`'s
1424
1465
  `rail`, not scope creep — every piece it composes already existed.
1425
1466
 
1467
+ ### CopyButton
1468
+
1469
+ ```tsx
1470
+ import { CopyButton } from '@gnome-ui/react-native';
1471
+
1472
+ <CopyButton value="CVE-2024-3094" />
1473
+ <CopyButton value={installCommand} label="Copy install command" copiedLabel="Added to clipboard" />
1474
+ ```
1475
+
1476
+ Icon button that copies `value` to the clipboard, swapping to a checkmark
1477
+ and a "Copied!" tooltip for `resetDelay` ms (default 2000) as confirmation
1478
+ — mirrors `@gnome-ui/react`'s `CopyButton`. RN has no `navigator.clipboard`,
1479
+ so this is built on `@react-native-clipboard/clipboard` (a new peer
1480
+ dependency, deliberately chosen over `expo-clipboard` so this package
1481
+ works the same in bare RN and Expo, not just this repo's own Expo example
1482
+ app — the same "no new peer dependency without a deliberate decision"
1483
+ standard `AnimatedIcon`'s `react-native-svg` addition was held to).
1484
+
1485
+ **Real environment finding**: `@react-native-clipboard/clipboard`'s native
1486
+ module isn't part of Expo Go's preinstalled module set, so any app using
1487
+ this component needs a custom dev client (`npx expo prebuild` +
1488
+ `expo run:ios`/`run:android`) rather than plain Expo Go — confirmed by
1489
+ hitting `TurboModuleRegistry.getEnforcing(...): 'RNCClipboard' could not
1490
+ be found` in Expo Go before building this repo's own example app a dev
1491
+ client to verify the component on-device.
1492
+
1493
+ Its `setString` is synchronous and void — unlike the web version's
1494
+ `navigator.clipboard.writeText`, which returns a `Promise` that can
1495
+ reject, there's no error channel to observe under normal operation.
1496
+ `onCopyError` is kept for API parity (and wraps the native call in a
1497
+ `try`/`catch` defensively) but in practice won't fire the way it can on
1498
+ web. The live-region announcement uses `role="status"` directly — RN's
1499
+ newer `Role` union does include `"status"`, unlike the older
1500
+ `AccessibilityRole` enum `Toast` had to substitute `"alert"` for.
1501
+
1426
1502
  ### Drawer
1427
1503
 
1428
1504
  ```tsx
@@ -1453,6 +1529,44 @@ no exit keyframes at all, so this follows `Dialog`'s simpler animation
1453
1529
  shape instead. `backdrop-filter: blur(4px)` has no port (no native blur
1454
1530
  dependency in this package).
1455
1531
 
1532
+ ### Toolbar
1533
+
1534
+ ```tsx
1535
+ import { Button, Spacer, Toolbar } from '@gnome-ui/react-native';
1536
+
1537
+ <Toolbar>
1538
+ <Button variant="flat">Cancel</Button>
1539
+ <Spacer />
1540
+ <Button variant="flat">Done</Button>
1541
+ </Toolbar>
1542
+ ```
1543
+
1544
+ Horizontal action bar following the libadwaita `.toolbar` pattern —
1545
+ mirrors `@gnome-ui/react`'s `Toolbar`. Directly portable, no web-only APIs
1546
+ — a flex row with `theme.space1` (6 dp) padding and gap, the standard
1547
+ spacing for rows of flat buttons in header bars, action bars, and tool
1548
+ rows. Use `Button variant="flat"` for buttons that blend into the bar, or
1549
+ `variant="raised"` for one that needs explicit elevation within a flat
1550
+ context. The web CSS's `color`/`font-family` on `.toolbar` are dropped —
1551
+ RN has no style inheritance from a parent `View` down to child `Text`
1552
+ the way CSS `color` cascades, so a value there would reach nothing (every
1553
+ child, e.g. `Button`, already sets its own explicit colors).
1554
+
1555
+ ### Spacer
1556
+
1557
+ ```tsx
1558
+ import { Spacer } from '@gnome-ui/react-native';
1559
+ ```
1560
+
1561
+ Invisible `flex: 1` filler for `Toolbar` and `HeaderBar` — mirrors
1562
+ `@gnome-ui/react`'s `Spacer`. Place between leading and trailing groups to
1563
+ push trailing items to the end. `accessible={false}` mirrors the web
1564
+ version's `aria-hidden="true"` — the same "purely decorative, exclude
1565
+ from the accessibility tree entirely" call `Separator` already made,
1566
+ rather than reaching for `role`'s newer `"separator"` value (which
1567
+ exists, but implies a divider a screen reader user might care about — a
1568
+ plain flex filler has no such meaning).
1569
+
1456
1570
  ### AvatarGroup
1457
1571
 
1458
1572
  ```tsx
@@ -1685,6 +1799,41 @@ zero height before `alignItems` gets to stretch anything into it. Caught
1685
1799
  on-device; it's a no-op in the ordinary case where the container hugs its
1686
1800
  content rather than having a fixed height.
1687
1801
 
1802
+ ### TagInput
1803
+
1804
+ ```tsx
1805
+ import { TagInput } from '@gnome-ui/react-native';
1806
+
1807
+ const [tags, setTags] = useState(['react', 'gnome']);
1808
+
1809
+ <TagInput label="Tags" value={tags} onChange={setTags} placeholder="Add a tag…" />;
1810
+ ```
1811
+
1812
+ Type-to-add multi-value input rendering entries as removable `Chip`s in a
1813
+ `WrapBox`, mirroring `@gnome-ui/react`'s own `TagInput`. Type and press
1814
+ Return, or type a `,`, to commit the draft as a tag; paste a comma or
1815
+ newline-separated list to add several at once; Backspace with an empty
1816
+ draft removes the last tag; tap a chip's `×` to remove that one.
1817
+
1818
+ The web version wires typed-`,` and pasted-list handling as two separate
1819
+ handlers (`onKeyDown`'s `,` case, `onPaste`). RN's `TextInput` has no
1820
+ `paste` event to mirror — but a paste still flows through `onChangeText`
1821
+ with the full resulting text, exactly like a typed `,` does, so both
1822
+ collapse into one handler here: whenever the text contains a `,` or
1823
+ newline, split on it and commit every non-empty part. Return is handled via
1824
+ `onSubmitEditing` (no `Enter` keystroke to catch on a touch keyboard), and
1825
+ Backspace-on-empty uses `onKeyPress` — the one `TextInput` event that still
1826
+ fires with the field already empty (`onChangeText` doesn't fire deleting
1827
+ from nothing).
1828
+
1829
+ The tag box is a `Pressable` (mirrors the web version's
1830
+ `onClick={() => inputRef.current?.focus()}` on the container) with
1831
+ `accessible={false}` set explicitly: `Pressable` defaults `accessible` to
1832
+ `true`, which would otherwise collapse every `Chip`'s remove button and the
1833
+ draft input into a single VoiceOver stop — the same container-swallows-
1834
+ subtree trap documented for a bare `View` plus `accessibilityRole`, hit
1835
+ here via `Pressable`'s own default instead.
1836
+
1688
1837
  ### StatusPage
1689
1838
 
1690
1839
  ```tsx
@@ -2022,6 +2171,38 @@ components means the flip-to-fit placement, the tap-outside dismissal and the
2022
2171
  — same behaviour as the web version, one level up. The keyboard layer drops
2023
2172
  as it does everywhere else here.
2024
2173
 
2174
+ ### SpinRow
2175
+
2176
+ ```tsx
2177
+ import { SpinRow } from '@gnome-ui/react-native';
2178
+
2179
+ <BoxedList>
2180
+ <SpinRow
2181
+ title="Volume"
2182
+ subtitle="Output level"
2183
+ value={volume}
2184
+ onValueChange={setVolume}
2185
+ min={0}
2186
+ max={100}
2187
+ />
2188
+ </BoxedList>
2189
+ ```
2190
+
2191
+ Settings row with an integrated spin button for numeric values — mirrors
2192
+ `AdwSpinRow` and `@gnome-ui/react`'s own `SpinRow`. Use it inside a
2193
+ `BoxedList` for settings with numeric ranges (volume, timeout duration,
2194
+ count limits, etc.). Controlled (`value`) and uncontrolled (`defaultValue`)
2195
+ modes both work.
2196
+
2197
+ **This is a composition of `ActionRow` + `SpinButton`, the same shape as
2198
+ `ComboRow`'s `ActionRow` + `Dropdown` composition above.** `SpinButton` is
2199
+ controlled-only, so the uncontrolled `defaultValue` state lives in `SpinRow`,
2200
+ one level up. The web version's keyboard interaction (↑/↓ one step, Page
2201
+ Up/Down ten steps, Home/End to bounds) drops as it does everywhere else in
2202
+ this package — `SpinButton` already provides the touch/screen-reader
2203
+ equivalents it was built with (tap the visible −/+ buttons, or the
2204
+ `accessibilityRole="adjustable"` increment/decrement actions).
2205
+
2025
2206
  ### ColorPicker / ColorSwatch
2026
2207
 
2027
2208
  ```tsx
@@ -2222,6 +2403,36 @@ nesting one `Pressable` inside another would create two overlapping tap
2222
2403
  targets. `aria-labelledby` has no RN equivalent, so `accessibilityLabel`
2223
2404
  combines the title and subtitle instead.
2224
2405
 
2406
+ ### SwitchRow
2407
+
2408
+ ```tsx
2409
+ import { BoxedList, SwitchRow } from '@gnome-ui/react-native';
2410
+
2411
+ <BoxedList>
2412
+ <SwitchRow
2413
+ title="Wi-Fi"
2414
+ subtitle="Home Network"
2415
+ checked={wifi}
2416
+ onCheckedChange={setWifi}
2417
+ />
2418
+ </BoxedList>
2419
+ ```
2420
+
2421
+ Activatable row with an integrated switch, for use inside a `BoxedList`
2422
+ — mirrors `@gnome-ui/react`'s `SwitchRow`. The entire row is a single
2423
+ pressable; pressing anywhere toggles the switch, which is why this isn't
2424
+ `ActionRow` + a trailing `Switch` — `AdwSwitchRow` makes the whole row the
2425
+ interactive element, the same shape `CheckRow` already established for its
2426
+ checkbox. Use for a single on/off setting; prefer `CheckRow` for
2427
+ multi-select scenarios. Supports both controlled (`checked`) and
2428
+ uncontrolled (`defaultChecked`) modes, the same `isControlled`/internal-
2429
+ state-fallback shape already used by `Expander`/`ComboRow`/`Popover`/
2430
+ `CheckRow`. The switch visual reuses `Switch`'s exact track/thumb animation
2431
+ recipe, but as plain non-interactive `Animated.View`s rather than the real
2432
+ `Switch` component — nesting one `Pressable` inside another would create
2433
+ two overlapping tap targets. `aria-labelledby` has no RN equivalent, so
2434
+ `accessibilityLabel` combines the title and subtitle instead.
2435
+
2225
2436
  ### ExpanderRow
2226
2437
 
2227
2438
  ```tsx
@@ -2396,6 +2607,157 @@ counter. Six variants (`success`/`warning`/`error`/`new`/`accent`/
2396
2607
  `neutral`) reuse `Badge`'s exact color-mapping shape, plus a `new` (purple)
2397
2608
  variant `Badge` doesn't have.
2398
2609
 
2610
+ ### StepIndicator
2611
+
2612
+ ```tsx
2613
+ import { StepIndicator } from '@gnome-ui/react-native';
2614
+
2615
+ <StepIndicator steps={5} currentStep={1} />
2616
+
2617
+ <StepIndicator
2618
+ steps={['Account', 'Profile', 'Payment', 'Confirm']}
2619
+ currentStep={2}
2620
+ onStepClick={setCurrentStep}
2621
+ />
2622
+
2623
+ <StepIndicator steps={4} currentStep={2} orientation="vertical" />
2624
+ ```
2625
+
2626
+ Numbered "Step X of Y" progress indicator for onboarding/wizard flows —
2627
+ mirrors `@gnome-ui/react`'s `StepIndicator`. Directly portable, no web-only
2628
+ APIs involved: each step's circle derives its state (upcoming/current/
2629
+ completed) purely from `currentStep`. Each circle animates its own
2630
+ border/background color (an independent pair of 0/1 `Animated.Value`s,
2631
+ one for "accented border" and one for "filled background", since they
2632
+ flip on different transitions) — the content swap between number and
2633
+ checkmark and the connector-line recolor are instant, matching the source
2634
+ CSS exactly (only `background-color`/`border-color` transition there).
2635
+
2636
+ The connecting line between circles reuses the CSS trick verbatim —
2637
+ `position: absolute; left: '50%'; width: '100%'` inside each equal-width
2638
+ flex item, so the line runs from one circle's center to the next's;
2639
+ `left`/`width` percentages are valid RN position/dimension values, unlike
2640
+ the `transform: translateX('50%')` trick this package avoids elsewhere.
2641
+ The checkmark uses `tintColor` rather than `color`, since RN icons have no
2642
+ `currentColor` to inherit — the same call `BottomTabBar`'s active-tab icon
2643
+ already made for tracking the *configurable* accent.
2644
+
2645
+ The outer container sets `role="navigation"` **without** `accessible` —
2646
+ the `ToggleGroup`-established pattern for a grouping role over multiple
2647
+ independently-focusable children (each step circle): `accessible` here
2648
+ would collapse the whole indicator into one VoiceOver stop on iOS. Assert
2649
+ `element.props.role` on a `testID` in tests instead of
2650
+ `getByRole('navigation')`. A completed step's circle becomes a `Pressable`
2651
+ only when `onStepClick` is provided — the current and upcoming steps are
2652
+ never pressable, matching the web version.
2653
+
2654
+ ### Timeline
2655
+
2656
+ ```tsx
2657
+ import { Timeline } from '@gnome-ui/react-native';
2658
+
2659
+ <Timeline
2660
+ items={[
2661
+ { leading: <Text color="dim">10:00</Text>, icon: <Icon icon={Check} tintColor={theme.accentFgColor} />, content: <Text>Approved</Text> },
2662
+ { content: <Text>Pending review</Text> },
2663
+ ]}
2664
+ />
2665
+
2666
+ <Timeline orientation="horizontal" variant="dotted" items={steps} />
2667
+ ```
2668
+
2669
+ Ordered sequence of events connected by a visual timeline — mirrors
2670
+ `@gnome-ui/react`'s `Timeline`. An original composition (no direct
2671
+ libadwaita widget), following GNOME HIG activity-feed/stepper patterns.
2672
+
2673
+ The web version aligns every item's `leading` column (vertical) or row
2674
+ (horizontal) via CSS subgrid, so timestamps/labels line up across items
2675
+ regardless of how wide/tall any single one of them is. RN/Yoga has no
2676
+ grid or subgrid at all, so that alignment is reproduced by measurement
2677
+ instead — the same `onLayout` + `Record<index, size>` +
2678
+ "largest-so-far wins" technique `Slider`'s mark labels already
2679
+ established. The node track itself additionally gets a fixed width
2680
+ (vertical, 24 dp, matching the source CSS's literal grid column) or
2681
+ height (horizontal, 28 dp, the larger of the dot/icon node sizes) so
2682
+ `content` starts at the same position across items even when dot and
2683
+ icon nodes are mixed in the same list.
2684
+
2685
+ `orientation="horizontal"` wraps itself in a horizontal `ScrollView`,
2686
+ reimagining the web CSS's `overflow-x: auto`. The web version's
2687
+ `grid-auto-columns: minmax(72px, 1fr)` also grows items to fill leftover
2688
+ space when the row doesn't overflow; that half doesn't port (a
2689
+ `ScrollView`'s content isn't bounded the way a CSS grid track is) — each
2690
+ item gets a flat 72 dp `minWidth` instead, unconditionally scrollable.
2691
+
2692
+ **Real, on-device-confirmed platform bug found while building `variant="dotted"`**:
2693
+ RN's `borderStyle: 'dotted'` renders nothing at all — no error, just
2694
+ invisible — unless *every* side shares the same width and color; the
2695
+ direct 1:1 port of the web CSS's single-side `border-left`/`border-top`
2696
+ dotted line (one bordered side, the other three left unset) silently
2697
+ produced no line whatsoever. Confirmed via the iOS Simulator with
2698
+ saturated debug colors: a uniform four-side `borderWidth`/`borderColor`
2699
+ renders the dotted pattern correctly, but reintroducing even three
2700
+ `transparent` sides (uniform width, per-side color) suppresses it again.
2701
+ Fixed by drawing the dotted connector as a narrow (6 dp) box with a
2702
+ uniform dotted border on all four sides instead of a single bordered
2703
+ edge — visually indistinguishable from a single dotted line at this
2704
+ thickness. **Any future dotted/dashed RN border needs all sides
2705
+ width-and-color-uniform — a single-side border in that style silently
2706
+ renders invisible.**
2707
+
2708
+ `icon`/`leading`/`content` are plain `ReactNode`, the same as `PathBar`'s
2709
+ segment `icon` — the consumer sizes and colors their own icon (e.g.
2710
+ `tintColor={theme.accentFgColor}`), since RN has no `currentColor` for
2711
+ this component to tint an arbitrary child with. `role="list"`/
2712
+ `role="listitem"` are set without `accessible`, the same
2713
+ `ToggleGroup`/`StepIndicator`-established pattern for a grouping role
2714
+ over children that may themselves contain focusable content.
2715
+
2716
+ ### WidgetManager
2717
+
2718
+ ```tsx
2719
+ import { WidgetManager, type WidgetDefinition } from '@gnome-ui/react-native';
2720
+
2721
+ const catalog: WidgetDefinition[] = [
2722
+ { id: 'clock', label: 'Clock', description: 'Shows the current time', render: () => <ClockWidget /> },
2723
+ { id: 'weather', label: 'Weather', render: () => <WeatherWidget /> },
2724
+ ];
2725
+
2726
+ <WidgetManager title="My Dashboard" catalog={catalog} value={widgetIds} onChange={setWidgetIds} />;
2727
+ ```
2728
+
2729
+ Card that manages a controlled collection of "widgets" — pick which ones
2730
+ are visible from a `catalog`, each rendering its own arbitrary content via
2731
+ `render()` — mirrors `@gnome-ui/react`'s own `WidgetManager`. The header's
2732
+ edit button toggles a local `editing` state: in view mode only the added
2733
+ widgets (or an empty-state message) show; in edit mode a dashed "add
2734
+ widget" trigger also appears, opening a catalog picker (`pickerSurface`:
2735
+ `"dialog"`, `"bottomSheet"`, or `"drawer"`). Adding/removing is staged
2736
+ inside the picker and only applied — via `onChange` — when the user
2737
+ confirms; canceling or dismissing discards the staging. Widgets can only be
2738
+ removed through the picker, never inline in the card.
2739
+
2740
+ Every piece this composes already existed: `ActionRow`+`BoxedList` for the
2741
+ catalog rows, `Button`/`IconButton`/`Icon`/`StatusPage` for the rest of the
2742
+ chrome, and `Dialog`/`BottomSheet`/`Drawer` for the three `pickerSurface`
2743
+ options. The web version's option is called `"modal"`, after its own
2744
+ `Modal` component — this package's `Modal` counterpart is `Dialog` (RN's
2745
+ own `Modal` primitive is a different, lower-level thing), so the option is
2746
+ named after what it actually renders here instead of ported verbatim. None
2747
+ of those three overlay components scroll their `children` for you, unlike
2748
+ the web version's `overflow-y: auto` body, so the catalog list gets its own
2749
+ capped `ScrollView` before being handed to whichever surface renders it.
2750
+ `Dialog` already renders its own confirm/cancel row from a `buttons` array;
2751
+ only `bottomSheet`/`drawer` need the hand-rolled footer row the web source
2752
+ itself calls out ("Modal uses its own actions").
2753
+
2754
+ Not ported: `aria-pressed` on the edit toggle — `Button`/`IconButton` set
2755
+ their own internal `accessibilityState` on the underlying `Pressable`, and
2756
+ a second `accessibilityState` prop passed in here would silently replace
2757
+ rather than merge with it (the same `Popover`-trigger clobber `SplitButton`
2758
+ already worked around) — dropped rather than routed around for one
2759
+ decorative toggle-state announcement.
2760
+
2399
2761
  ### BottomTabBar
2400
2762
 
2401
2763
  ```tsx
@@ -2494,6 +2856,55 @@ threshold. Unlike the web version there's no `data-breakpoint` attribute
2494
2856
  to expose for CSS targeting (RN has no attribute selectors) — branch on
2495
2857
  `activeBreakpoint` directly inside the render prop instead.
2496
2858
 
2859
+ ### ScrollToTop
2860
+
2861
+ ```tsx
2862
+ import { ScrollToTop } from '@gnome-ui/react-native';
2863
+
2864
+ const scrollRef = useRef<ScrollView>(null);
2865
+ const [scrollY, setScrollY] = useState(0);
2866
+
2867
+ <View style={{ flex: 1 }}>
2868
+ <ScrollView
2869
+ ref={scrollRef}
2870
+ onScroll={(e) => setScrollY(e.nativeEvent.contentOffset.y)}
2871
+ scrollEventThrottle={16}
2872
+ >
2873
+ {/* ... */}
2874
+ </ScrollView>
2875
+ <ScrollToTop
2876
+ scrollY={scrollY}
2877
+ onPress={() => scrollRef.current?.scrollTo({ y: 0, animated: true })}
2878
+ />
2879
+ </View>;
2880
+ ```
2881
+
2882
+ Absolutely-positioned OSD button that scrolls a `ScrollView`/`FlatList`
2883
+ back to the top — mirrors `@gnome-ui/react`'s `ScrollToTop`, reimagined
2884
+ rather than ported 1:1: RN has no page-level scroll event to observe
2885
+ internally, so the web version's `useScrollToTopVisibility` (a `window`/
2886
+ element `scroll` listener) doesn't port at all. `visible="auto"` (the
2887
+ default) is instead a pure function of a `scrollY` number prop the
2888
+ consumer feeds from their own `ScrollView`'s `onScroll` — no internal
2889
+ state or listener needed. There's likewise no way for this component to
2890
+ scroll a `ScrollView`/`FlatList` on the consumer's behalf the way the web
2891
+ version calls `scrollTarget.scrollTo(...)` itself, so pressing the button
2892
+ calls the required `onPress` instead — typically
2893
+ `scrollRef.current?.scrollTo({ y: 0, animated: true })`.
2894
+
2895
+ Same "no `document.body`/portal target" gap `Toaster` already documents —
2896
+ mount this yourself as the last child of the `View` wrapping your
2897
+ scrollable content so it paints on top; `pointerEvents="box-none"` (the
2898
+ same technique `Toaster` uses) keeps the empty space around the button
2899
+ from intercepting touches meant for the content underneath. The web
2900
+ version's resting `opacity: 0.5`-until-hover/focus is dropped, the same
2901
+ call `PasswordEntryRow`'s reveal button already made — that effect exists
2902
+ purely so the control can brighten on hover, and touch has no hover.
2903
+ `topInset`/`bottomInset` let you thread in your own
2904
+ `useSafeAreaInsets()` values for a `"top-*"`/`"bottom-*"` position, same
2905
+ as `BottomTabBar`'s `bottomInset` — this package takes no dependency on
2906
+ `react-native-safe-area-context` itself.
2907
+
2497
2908
  ## Installation
2498
2909
 
2499
2910
  ```bash
@@ -0,0 +1,44 @@
1
+ import { IconButtonProps } from '../IconButton';
2
+ export interface CopyButtonProps extends Omit<IconButtonProps, 'icon' | 'label' | 'onPress' | 'tooltip'> {
3
+ /** The text copied to the clipboard when the button is activated. */
4
+ value: string;
5
+ /** Accessible label and tooltip shown before copying. Defaults to `"Copy"`. */
6
+ label?: string;
7
+ /** Accessible label and tooltip shown briefly after a successful copy. Defaults to `"Copied!"`. */
8
+ copiedLabel?: string;
9
+ /** How long the "copied" confirmation state is shown, in milliseconds. Defaults to `2000`. */
10
+ resetDelay?: number;
11
+ /**
12
+ * Called after `value` is written to the clipboard.
13
+ * Named `onCopied` (not `onCopy`), mirroring the web version's own naming.
14
+ */
15
+ onCopied?: (value: string) => void;
16
+ /**
17
+ * Called if the copy attempt throws. Kept for API parity with the web
18
+ * version's `onCopyError` (whose `navigator.clipboard.writeText` can
19
+ * reject, e.g. on permission denial) — but `@react-native-clipboard/
20
+ * clipboard`'s `setString` is a synchronous, void-returning native call
21
+ * with no error channel at all, so in normal operation this won't fire
22
+ * the way it can on web. Kept as a defensive catch around the native
23
+ * call (a real, if unlikely, synchronous throw) rather than removed.
24
+ */
25
+ onCopyError?: (error: unknown) => void;
26
+ }
27
+ /**
28
+ * Icon button that copies `value` to the clipboard, swapping to a checkmark
29
+ * and a "Copied!" tooltip for `resetDelay` ms as confirmation — mirrors
30
+ * `@gnome-ui/react`'s `CopyButton`.
31
+ *
32
+ * RN has no `navigator.clipboard`, so this is built on
33
+ * `@react-native-clipboard/clipboard` (the community-standard clipboard
34
+ * module, not `expo-clipboard` — chosen so this package works the same in
35
+ * bare RN and Expo, not just this repo's own Expo example app). Its
36
+ * `setString` is synchronous and void — see `onCopyError`'s doc for what
37
+ * that means for error handling.
38
+ *
39
+ * The live-region announcement uses `role="status"` directly (RN's newer
40
+ * `Role` union does include `"status"`, unlike the older
41
+ * `AccessibilityRole` enum `Toast` had to substitute `"alert"` for) plus
42
+ * `accessibilityLiveRegion="polite"` for Android's announcement mechanism.
43
+ */
44
+ export declare const CopyButton: ({ value, label, copiedLabel, resetDelay, onCopied, onCopyError, ...props }: CopyButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { CopyButtonProps } from './CopyButton';
2
+ export { CopyButton } from './CopyButton';
@@ -0,0 +1,105 @@
1
+ import { StyleProp, View, ViewStyle } from 'react-native';
2
+ /**
3
+ * Controls when the button is rendered.
4
+ *
5
+ * - `"auto"` — hidden until `scrollY` exceeds `threshold` (default).
6
+ * - `"always"` — always rendered regardless of scroll position.
7
+ */
8
+ export type ScrollToTopVisible = 'always' | 'auto';
9
+ /**
10
+ * Anchor corner or edge for the absolutely-positioned container.
11
+ * The button is inset `theme.space4` (24 dp) from each named edge.
12
+ */
13
+ export type ScrollToTopPosition = 'bottom-right' | 'bottom-left' | 'bottom-center' | 'top-right' | 'top-left' | 'top-center';
14
+ export interface ScrollToTopProps {
15
+ /**
16
+ * Controls when the button is visible.
17
+ *
18
+ * - `"auto"` (default) — shown only once `scrollY` exceeds `threshold`.
19
+ * - `"always"` — permanently visible.
20
+ */
21
+ visible?: ScrollToTopVisible;
22
+ /**
23
+ * Corner or edge where the button is anchored. Default: `"bottom-right"`.
24
+ */
25
+ position?: ScrollToTopPosition;
26
+ /**
27
+ * Offset the user must scroll past before the button appears.
28
+ * Only relevant when `visible="auto"`. Default: `300`.
29
+ */
30
+ threshold?: number;
31
+ /**
32
+ * The observed `ScrollView`/`FlatList`'s current vertical scroll offset —
33
+ * pass `nativeEvent.contentOffset.y` from its `onScroll` handler. Only
34
+ * relevant when `visible="auto"`; ignored (and safe to omit) otherwise.
35
+ * Default: `0`.
36
+ */
37
+ scrollY?: number;
38
+ /**
39
+ * Called when the button is pressed. There's no RN equivalent of the web
40
+ * version's own `scrollTarget.scrollTo({ top: 0, behavior: 'smooth' })` —
41
+ * `ScrollView`/`FlatList` are scrolled through a ref's imperative
42
+ * `scrollTo`/`scrollToOffset`, which this component has no way to hold on
43
+ * a consumer's behalf. Typically
44
+ * `() => scrollViewRef.current?.scrollTo({ y: 0, animated: true })`.
45
+ */
46
+ onPress: () => void;
47
+ /**
48
+ * Extra inset added on top of the base edge spacing when anchored to a
49
+ * `"top-*"` position — pass your own `useSafeAreaInsets().top` so the
50
+ * button clears a notch/status bar. Same "no new peer dependency" call
51
+ * `BottomTabBar`'s `bottomInset` already made. Default: `0`.
52
+ */
53
+ topInset?: number;
54
+ /** Same as `topInset`, for a `"bottom-*"` position. Default: `0`. */
55
+ bottomInset?: number;
56
+ style?: StyleProp<ViewStyle>;
57
+ testID?: string;
58
+ }
59
+ /**
60
+ * Absolutely-positioned OSD button that scrolls a `ScrollView`/`FlatList`
61
+ * back to the top — mirrors `@gnome-ui/react`'s `ScrollToTop`, reimagined
62
+ * rather than ported 1:1 per this package's own ROADMAP note: RN has no
63
+ * page-level scroll event to observe internally the way the web version's
64
+ * `useScrollToTopVisibility` attaches a `window`/element `scroll` listener,
65
+ * so that hook doesn't port at all — `visible="auto"` is instead a pure
66
+ * function of a `scrollY` prop the consumer feeds from their own
67
+ * `ScrollView`'s `onScroll`, re-evaluated on every render with no internal
68
+ * state or listener needed.
69
+ *
70
+ * Same "no `document.body`/portal target" gap `Toaster` already
71
+ * documents — mount this yourself as the last child of the `View` wrapping
72
+ * your scrollable content (left at its default relative positioning) so it
73
+ * paints on top; `pointerEvents="box-none"` (the same technique `Toaster`
74
+ * uses) keeps the empty space around the button from intercepting touches
75
+ * meant for the content underneath.
76
+ *
77
+ * The web version's resting `opacity: 0.5`-until-hover/focus is dropped —
78
+ * the same call `PasswordEntryRow`'s reveal button already made: that
79
+ * effect exists purely so the control can brighten on hover, and touch has
80
+ * no hover, so a permanently dimmed control would just be harder to see.
81
+ *
82
+ * Forwards `ref` to the root positioning `View`, not the inner button —
83
+ * matching the web version's own `forwardRef` target.
84
+ *
85
+ * @example
86
+ * // Minimal — appears once `scrollY` exceeds 300, anchored bottom-right
87
+ * const [scrollY, setScrollY] = useState(0);
88
+ * const scrollRef = useRef<ScrollView>(null);
89
+ * <View style={{ flex: 1 }}>
90
+ * <ScrollView
91
+ * ref={scrollRef}
92
+ * onScroll={(e) => setScrollY(e.nativeEvent.contentOffset.y)}
93
+ * scrollEventThrottle={16}
94
+ * >
95
+ * ...
96
+ * </ScrollView>
97
+ * <ScrollToTop
98
+ * scrollY={scrollY}
99
+ * onPress={() => scrollRef.current?.scrollTo({ y: 0, animated: true })}
100
+ * />
101
+ * </View>
102
+ *
103
+ * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.ScrollToTop.html
104
+ */
105
+ export declare const ScrollToTop: import('react').ForwardRefExoticComponent<ScrollToTopProps & import('react').RefAttributes<View>>;
@@ -0,0 +1,2 @@
1
+ export type { ScrollToTopPosition, ScrollToTopProps, ScrollToTopVisible } from './ScrollToTop';
2
+ export { ScrollToTop } from './ScrollToTop';
@@ -0,0 +1,25 @@
1
+ import { StyleProp, View, ViewProps, ViewStyle } from 'react-native';
2
+ export interface SpacerProps extends Omit<ViewProps, 'style'> {
3
+ style?: StyleProp<ViewStyle>;
4
+ }
5
+ /**
6
+ * Invisible `flex: 1` filler for `Toolbar` and `HeaderBar` — mirrors
7
+ * `@gnome-ui/react`'s `Spacer`. Place between leading and trailing groups
8
+ * to push trailing items to the end.
9
+ *
10
+ * `accessible={false}` mirrors the web version's `aria-hidden="true"` —
11
+ * same "purely decorative, exclude from the accessibility tree entirely"
12
+ * call `Separator` already made, rather than reaching for `role`'s newer
13
+ * `"separator"` value (which exists but implies a divider a screen
14
+ * reader user might care about; a plain flex filler has no such meaning).
15
+ *
16
+ * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/style-classes.html#spacer
17
+ *
18
+ * @example
19
+ * <Toolbar>
20
+ * <Button variant="flat">Back</Button>
21
+ * <Spacer />
22
+ * <Button variant="flat">Done</Button>
23
+ * </Toolbar>
24
+ */
25
+ export declare const Spacer: import('react').ForwardRefExoticComponent<SpacerProps & import('react').RefAttributes<View>>;