@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 +414 -3
- package/dist/components/CopyButton/CopyButton.d.ts +44 -0
- package/dist/components/CopyButton/index.d.ts +2 -0
- package/dist/components/ScrollToTop/ScrollToTop.d.ts +105 -0
- package/dist/components/ScrollToTop/index.d.ts +2 -0
- package/dist/components/Spacer/Spacer.d.ts +25 -0
- package/dist/components/Spacer/index.d.ts +2 -0
- package/dist/components/SpinRow/SpinRow.d.ts +64 -0
- package/dist/components/SpinRow/index.d.ts +2 -0
- package/dist/components/SplitButton/SplitButton.d.ts +46 -0
- package/dist/components/SplitButton/index.d.ts +2 -0
- package/dist/components/StepIndicator/StepIndicator.d.ts +56 -0
- package/dist/components/StepIndicator/index.d.ts +2 -0
- package/dist/components/SwitchRow/SwitchRow.d.ts +43 -0
- package/dist/components/SwitchRow/index.d.ts +2 -0
- package/dist/components/TagInput/TagInput.d.ts +59 -0
- package/dist/components/TagInput/index.d.ts +2 -0
- package/dist/components/Timeline/Timeline.d.ts +87 -0
- package/dist/components/Timeline/index.d.ts +2 -0
- package/dist/components/Toolbar/Toolbar.d.ts +25 -0
- package/dist/components/Toolbar/index.d.ts +2 -0
- package/dist/components/WidgetManager/WidgetManager.d.ts +92 -0
- package/dist/components/WidgetManager/index.d.ts +2 -0
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1611 -729
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
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), `
|
|
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)
|
|
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.
|
|
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,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,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>>;
|