@gnome-ui/react-native 1.2.0 → 1.4.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
@@ -18,10 +18,15 @@ React Native component library following the [GNOME Human Interface Guidelines](
18
18
  > `Skeleton`, `Toast`/`Toaster`, `Banner`, `Dialog`, `Tooltip`, and
19
19
  > `AnimatedIcon` (which brought a new `Icon` component along with it, as its
20
20
  > own public component) shipped — `Status Page` skipped for now. Tier 5
21
- > Advanced Controls in progress: `Dropdown` shipped — `Slider`,
22
- > `Spin Button`, `Avatar`, `Badge`, and `Popover` remain. Component
23
- > ports from `@gnome-ui/react` continue tier by tier. See
24
- > [ROADMAP.md](../../ROADMAP.md) Priority 3.
21
+ > Advanced Controls fully ported: `Dropdown`, `Slider`, `SpinButton`,
22
+ > `Avatar`, `Badge`, and `Popover`. Beyond Tier 5, `BottomSheet` (Tier 14)
23
+ > and `Overlay`/`LevelBar`/`Expander`/`Divider`/`Highlight` (Tier 20) also
24
+ > shipped. Component ports from
25
+ > `@gnome-ui/react` continue tier by tier — see this package's own
26
+ > [ROADMAP.md](./ROADMAP.md) for full
27
+ > per-tier status against all 130 `@gnome-ui/react` components, and the
28
+ > main [ROADMAP.md](../../ROADMAP.md) Priority 3 for the framework
29
+ > expansion this package belongs to.
25
30
 
26
31
  ## How it works
27
32
 
@@ -923,6 +928,369 @@ Selection is by direct tap only. `role="combobox"` on the trigger ports
923
928
  `role="list"` instead — the same closest-available substitution `BoxedList`
924
929
  already established for a plain list container.
925
930
 
931
+ ### Slider
932
+
933
+ ```tsx
934
+ import { Slider } from '@gnome-ui/react-native';
935
+
936
+ <Slider
937
+ value={volume}
938
+ onChange={setVolume}
939
+ accessibilityLabel="Volume"
940
+ marks={[
941
+ { value: 0, label: 'Min' },
942
+ { value: 100, label: 'Max' },
943
+ ]}
944
+ />;
945
+ ```
946
+
947
+ Draggable range control following the Adwaita `GtkScale` pattern, mirroring
948
+ `@gnome-ui/react`'s `Slider`.
949
+
950
+ Touch drag is handled with RN's own `PanResponder` (this package's first use
951
+ of it) reading each touch event's `locationX` — the position relative to the
952
+ track view itself, recalculated by RN on every touch/move — so no
953
+ `measureInWindow` round-trip is needed at all, unlike `Tooltip`/`Dropdown`'s
954
+ trigger-rect measurement. `min`/`max`/`step` clamping and snapping is ported
955
+ verbatim from the web version's pure-JS math.
956
+
957
+ The web version's keyboard interaction (← / → one step, Page Up/Down ten
958
+ steps, Home/End to the bounds) has no RN equivalent — a touch-first device
959
+ has no keyboard driving those keys. Rather than dropping value-adjustment
960
+ accessibility entirely (the reasoning that dropped `Dropdown`'s/`TabBar`'s
961
+ keyboard nav), `accessibilityRole="adjustable"` +
962
+ `onAccessibilityAction`/`accessibilityActions` wires up the "increment"/
963
+ "decrement" actions VoiceOver's swipe-up/down and TalkBack's local-context
964
+ menu generate for an adjustable element — the real native analog of
965
+ keyboard stepping, one step per action. The bigger Page Up/Down and
966
+ Home/End jumps have no equivalent screen-reader gesture on either platform,
967
+ so only single-step adjustment is ported.
968
+
969
+ RN's `transform` only accepts pixel offsets, unlike the CSS `%` units the
970
+ web version's `left: X%; transform: translate(-50%, -50%)` thumb/tick
971
+ centering trick needs — so those are positioned with a plain pixel `left`
972
+ computed from the track's `onLayout`-measured width instead. Mark labels
973
+ use a different trick, since (unlike the thumb/ticks) their own rendered
974
+ width isn't a known constant: a zero-width `View` with
975
+ `alignItems: 'center'` at the mark's percentage `left` lets Yoga center the
976
+ `Text` child around that point regardless of how wide the label renders,
977
+ with no measurement needed.
978
+
979
+ ### SpinButton
980
+
981
+ ```tsx
982
+ import { SpinButton } from '@gnome-ui/react-native';
983
+
984
+ <SpinButton value={quantity} onChange={setQuantity} min={0} max={10} accessibilityLabel="Quantity" />;
985
+ ```
986
+
987
+ Numeric −/+ stepper following the Adwaita `GtkSpinButton` pattern, mirroring
988
+ `@gnome-ui/react`'s `SpinButton`. The `min`/`max`/`step`/`decimals`/`wrap`/
989
+ `format` clamp-and-format math ports verbatim (pure JS, no DOM involved).
990
+
991
+ The primary interaction is tapping the visible −/+ buttons, same as a
992
+ sighted mouse user on the web version. The web version's keyboard
993
+ interaction (↑/↓ one step, Page Up/Down ten steps, Home/End to bounds) has
994
+ no RN equivalent — a touch-first device has no keyboard to drive it, the
995
+ same reasoning `Slider` already applied. Rather than dropping value
996
+ adjustment accessibility entirely, single-step increment/decrement reuses
997
+ `Slider`'s exact `accessibilityRole="adjustable"` +
998
+ `onAccessibilityAction`/`accessibilityActions` recipe (VoiceOver's
999
+ swipe-up/down, TalkBack's local-context menu) — the bigger Page Up/Down and
1000
+ Home/End jumps have no equivalent screen-reader gesture on either platform,
1001
+ so those alone are dropped, same as `Slider`. The visible −/+ buttons and
1002
+ value text are hidden from the accessibility tree
1003
+ (`accessibilityElementsHidden`/`importantForAccessibility="no"`, mirroring
1004
+ the web version's `aria-hidden`/`tabIndex={-1}` on both `<button>`s and the
1005
+ value `<span>`) so a screen reader user gets one adjustable stop, not three.
1006
+
1007
+ ### Avatar
1008
+
1009
+ ```tsx
1010
+ import { Avatar } from '@gnome-ui/react-native';
1011
+
1012
+ <Avatar name="Grace Hopper" size="lg" />
1013
+ <Avatar src="https://example.com/alice.jpg" alt="Alice's profile photo" />;
1014
+ ```
1015
+
1016
+ Circular avatar with image or initials fallback, mirroring `@gnome-ui/react`'s
1017
+ `Avatar`. The color-hash and initials-extraction math ports verbatim (pure
1018
+ JS, no DOM involved).
1019
+
1020
+ The outer container carries `role="img"` + `accessibilityLabel` — RN's newer
1021
+ web-aligned `Role` union has an `"img"` value, a direct 1:1 port of the web
1022
+ version's `role="img"`, no substitution needed (same as `ProgressBar`'s
1023
+ `role="progressbar"`). The image/initials underneath are hidden from the
1024
+ accessibility tree, mirroring the web version's `aria-hidden` on both, so a
1025
+ screen reader gets one stop, not two — same reasoning as `SpinButton`'s
1026
+ hidden −/+ buttons.
1027
+
1028
+ The web CSS's `box-shadow: inset 0 0 0 1px …` ring becomes a real 1px
1029
+ `borderWidth`/`borderColor` here (RN has no inset shadow) — the same
1030
+ substitution `Slider`'s thumb border already used for a ring effect.
1031
+
1032
+ ### Badge
1033
+
1034
+ ```tsx
1035
+ import { Avatar, Badge } from '@gnome-ui/react-native';
1036
+
1037
+ <Badge variant="error" anchor={<Avatar name="Alice Bob" />}>3</Badge>
1038
+ <Badge dot variant="success" />;
1039
+ ```
1040
+
1041
+ Counter or status indicator, optionally overlaid on another element,
1042
+ mirroring `@gnome-ui/react`'s `Badge`. `children` renders as a themed `Text`
1043
+ label when it's a string or number (the common case — counts and short
1044
+ text); any other node renders as-is, the same convention `Button`'s
1045
+ `children` already established.
1046
+
1047
+ The web CSS's `box-shadow: 0 0 0 2px var(--gnome-window-bg-color)` ring
1048
+ (always present, separating the badge from whatever's behind it) has no RN
1049
+ equivalent that avoids affecting layout — RN's `border*` shrinks the content
1050
+ box instead of drawing outside it. Reproduced instead with an outer wrapping
1051
+ `View` (2px padding, `theme.windowBgColor` background, pill radius) around
1052
+ the actual colored badge, so the ring appears to spread outward exactly like
1053
+ the web version's non-blurred shadow, without eating into the badge's own
1054
+ text padding.
1055
+
1056
+ ### Popover
1057
+
1058
+ ```tsx
1059
+ import { Button, Popover, Text } from '@gnome-ui/react-native';
1060
+
1061
+ <Popover content={<Text>Rich content here</Text>}>
1062
+ <Button>Open</Button>
1063
+ </Popover>;
1064
+ ```
1065
+
1066
+ Floating panel anchored to a trigger element, following the Adwaita
1067
+ `GtkPopover` pattern, mirroring `@gnome-ui/react`'s `Popover`. Unlike
1068
+ `Tooltip`, it can hold rich interactive content (buttons, links, forms).
1069
+
1070
+ Reuses this package's own established pieces rather than re-deriving them:
1071
+ `Tooltip`'s `cloneElement`-onto-an-arbitrary-trigger architecture and
1072
+ 4-placement fallback-cascade positioning (no arrow-offset-shift-when-clamped
1073
+ — same simplification `Tooltip` already accepted), and `Dropdown`'s
1074
+ toggle-on-press + full-screen backdrop `Pressable` that closes on an outside
1075
+ tap plus reduced-motion fade-in.
1076
+
1077
+ **Deliberate divergence from `Dropdown`'s backdrop structure**: `Dropdown`
1078
+ nests its panel directly inside the backdrop `Pressable` and gets away with
1079
+ it because almost every pixel of its panel is itself a `Pressable` option
1080
+ row, which claims the touch responder before it can bubble to the backdrop.
1081
+ A popover's `content` is arbitrary — likely to have inert padding/whitespace
1082
+ with no `Pressable` of its own — so nesting the same way would let a tap on
1083
+ inert panel space fall through to the backdrop and close the popover, unlike
1084
+ the web version's `.contains()` check (which never closes on *any* tap
1085
+ inside the panel). Fixed with `onStartShouldSetResponder={() => true}` on
1086
+ the panel itself: it claims the touch responder for any touch RN's
1087
+ negotiation hasn't already given to a deeper `Pressable` inside `content`,
1088
+ without making the panel itself behave like a button.
1089
+
1090
+ `BackHandler`'s `hardwareBackPress` (wired the same way `Dialog` already
1091
+ does) is the Android analog of the web version's document-level Escape
1092
+ listener. Focus-trapping and focus-restore-on-close have no port — no DOM
1093
+ `document.activeElement`/`querySelector` equivalent exists in RN, the same
1094
+ gap already present in `Dialog`/`Tooltip`/`Dropdown`.
1095
+
1096
+ The web version's rotated-square-with-matching-background arrow is replaced
1097
+ with `Tooltip`'s simpler transparent-border-triangle technique — the same
1098
+ visual affordance, a much simpler RN-native primitive.
1099
+
1100
+ ### BottomSheet
1101
+
1102
+ ```tsx
1103
+ import { BottomSheet, Button } from '@gnome-ui/react-native';
1104
+
1105
+ <Button onPress={() => setOpen(true)}>Open</Button>
1106
+ <BottomSheet open={open} title="Options" onClose={() => setOpen(false)}>
1107
+ <Text>Rich content here</Text>
1108
+ </BottomSheet>;
1109
+ ```
1110
+
1111
+ Slide-up panel that overlays content from the bottom edge, mirroring
1112
+ `AdwBottomSheet` (libadwaita 1.6+) and `@gnome-ui/react`'s `BottomSheet`.
1113
+ Reuses `Dialog`'s backdrop-opacity-on-an-`AnimatedPressable` +
1114
+ no-op-`Pressable`-around-the-card recipe, and `BackHandler`'s
1115
+ `hardwareBackPress` as the Android analog of the web version's Escape
1116
+ listener.
1117
+
1118
+ **Real drag-to-dismiss**, not a fixed-panel simplification: `PanResponder`
1119
+ (the same core API `Slider` already proved handles a threshold gesture)
1120
+ drives a single `Animated.Value` shared with the entrance/exit animation —
1121
+ dragging the handle bar past 150 px (same constant as the web version)
1122
+ requests a close; releasing short of that springs back to `0`. A real
1123
+ slide-up needs the sheet's own height first (RN's `transform` has no
1124
+ percentage-of-self units, the same `Slider`/`Avatar` pitfall) — the sheet
1125
+ renders once off-screen, measured via `onLayout`, before animating in.
1126
+
1127
+ **A real, timed exit animation, unlike `Dialog`**: `Dialog`'s web source has
1128
+ no exit keyframes at all, but `BottomSheet`'s does — ported with a local
1129
+ `visible` state that lags one animation behind the `open` prop, flipping to
1130
+ `false` only in the exit `Animated.timing`'s own completion callback.
1131
+
1132
+ The web version's `backdrop-filter: blur(4px)` has no port (no native blur
1133
+ view dependency, same reasoning that dropped `Sidebar`'s blurred variant),
1134
+ and `useBodyScrollLock` needs no RN equivalent (`Modal` already blocks all
1135
+ background interaction). `children`, when a plain string, is wrapped in
1136
+ `Text` before rendering — RN throws if a raw string is a `View`'s child,
1137
+ unlike the web version's plain `<div>{children}</div>`.
1138
+
1139
+ ### Overlay
1140
+
1141
+ ```tsx
1142
+ import { Overlay, Button } from '@gnome-ui/react-native';
1143
+
1144
+ <Button onPress={() => setOpen(true)}>Open</Button>
1145
+ <Overlay open={open} onDismiss={() => setOpen(false)}>
1146
+ <YourOwnCard />
1147
+ </Overlay>;
1148
+ ```
1149
+
1150
+ Standalone backdrop/scrim layer with a fade transition and
1151
+ press-to-dismiss — the shared building block behind `Dialog`, `Dropdown`,
1152
+ `Popover`, and `BottomSheet`'s own backdrops, extracted here for building
1153
+ custom overlay UI, mirroring `@gnome-ui/react`'s `Overlay`.
1154
+
1155
+ Deliberately minimal, same as the web version: no focus trap, no
1156
+ `BackHandler`/Escape handling, no `role` — use `Dialog`/`Popover`/
1157
+ `BottomSheet` directly when you need those. Reuses `Dialog`'s exact
1158
+ backdrop recipe (`AnimatedPressable` + a no-op `Pressable` wrapping
1159
+ `children`, so a tap on your own content never bubbles to the backdrop and
1160
+ dismisses it) and `BottomSheet`'s real, timed exit animation technique — a
1161
+ local `visible` state that lags the `open` prop by one `Animated.timing`,
1162
+ flipping to `false` only in that animation's own completion callback.
1163
+
1164
+ **Not retrofitted into `Dialog`/`Dropdown`/`Popover`/`BottomSheet`** — each
1165
+ already ships and is fully tested with its own inline copy of this same
1166
+ backdrop pattern (with small per-component differences: `Popover` claims
1167
+ the touch responder differently than the no-op-`Pressable` wrapper the
1168
+ others use). Extracting `Overlay` as a new standalone primitive was the
1169
+ scoped ask; retrofitting four already-shipped components to share it is a
1170
+ separate, riskier refactor this turn didn't take on.
1171
+
1172
+ ### LevelBar
1173
+
1174
+ ```tsx
1175
+ import { LevelBar } from '@gnome-ui/react-native';
1176
+
1177
+ <LevelBar value={0.15} low={0.25} high={0.75} accessibilityLabel="Battery" />
1178
+ <LevelBar value={0.6} discrete numBlocks={5} accessibilityLabel="Signal strength" />;
1179
+ ```
1180
+
1181
+ Discrete level indicator with color-coded low/high offset zones, mirroring
1182
+ `GtkLevelBar` and `@gnome-ui/react`'s `LevelBar`. Use for a gauge/
1183
+ measurement display (disk usage, battery, signal strength) — not for task
1184
+ progress (`ProgressBar`) or a proportional category breakdown
1185
+ (`SegmentedBar`).
1186
+
1187
+ The continuous fill reuses `ProgressBar`'s exact animation technique rather
1188
+ than animating `width` directly: a fixed `width: '100%'` fill with
1189
+ `transformOrigin: 'left'` and an animated `transform: [{ scaleX }]`, so the
1190
+ whole thing runs on `useNativeDriver: true` — a JS-driven `width` animation
1191
+ schedules its next frame via a plain `setTimeout` that routinely fires
1192
+ after a test's `render()` returns but before unmount, producing a spurious
1193
+ "update not wrapped in act()" warning, the same reasoning `ProgressBar`'s
1194
+ own docstring documents. `useReducedMotion()` mirrors `ProgressBar`'s
1195
+ determinate behavior (duration drops to `0`, an immediate jump).
1196
+
1197
+ Discrete mode's per-block color transition has no port — a value change is
1198
+ a plain, unanimated color swap per block, a decorative nicety rather than a
1199
+ behavior gap. `role="meter"` ports 1:1 from RN's newer web-aligned `Role`
1200
+ union (unlike `AccessibilityRole`, which has no `"meter"` value at all).
1201
+
1202
+ ### Expander
1203
+
1204
+ ```tsx
1205
+ import { Expander } from '@gnome-ui/react-native';
1206
+
1207
+ <Expander label="Show advanced options">
1208
+ <TextField label="Custom endpoint" />
1209
+ </Expander>
1210
+ ```
1211
+
1212
+ Standalone disclosure triangle + collapsible content, mirroring `GtkExpander`
1213
+ and `@gnome-ui/react`'s `Expander`. A bare, unstyled counterpart to
1214
+ `ExpanderRow`; use it outside a settings-row context (e.g. "Show advanced
1215
+ options" in a form, or "Show details" under an error message).
1216
+
1217
+ The web version clips the panel with a CSS grid-height animation and rides
1218
+ the content's `padding-top` on a second, separate transition, so a collapsed
1219
+ expander doesn't reserve blank space for hidden padding. RN has no CSS grid
1220
+ to lean on, so the panel is a single `Animated.View` whose numeric `height`
1221
+ is driven directly (`useNativeDriver: false`, the same accepted trade-off
1222
+ `Checkbox`/`RadioButton`/`Switch`/`AnimatedIcon` already make for
1223
+ non-transform properties) — since the content's own `onLayout` measurement
1224
+ already includes its `paddingTop`, one animated height reproduces the web
1225
+ version's two-transition result. Content stays mounted while collapsed
1226
+ (`accessibilityElementsHidden`/`importantForAccessibility="no"`, the same
1227
+ substitution for the web's `inert` used elsewhere in this package), and on
1228
+ first mount with `defaultExpanded` the panel briefly renders at its natural,
1229
+ unmeasured height so the initial reveal doesn't pop once layout resolves.
1230
+
1231
+ The chevron is `PanEnd` (GNOME's own `pan-end-symbolic` disclosure triangle)
1232
+ rotating 0deg → 90deg on an `Animated.Value`, the same `interpolate`-to-
1233
+ `rotate` recipe `Spinner` uses for its own spin.
1234
+
1235
+ ### Divider
1236
+
1237
+ ```tsx
1238
+ import { Divider } from '@gnome-ui/react-native';
1239
+
1240
+ <Divider>OR</Divider>
1241
+ <Divider>Continue with</Divider>
1242
+ <Divider />
1243
+ ```
1244
+
1245
+ Horizontal rule with an optional centered label — the common auth/login-form
1246
+ pattern ("Sign in" / **OR** / "Continue with Google"). Mirrors
1247
+ `@gnome-ui/react`'s `Divider`. For a bare dividing line with no label, use
1248
+ `Separator` instead — it also supports a vertical orientation, which
1249
+ `Divider` does not.
1250
+
1251
+ `role="separator"` ports 1:1 from RN's newer web-aligned `Role` union (the
1252
+ same one `Avatar`/`Badge`/`LevelBar` already reach for) — unlike
1253
+ `Separator`'s own `accessible={false}`, since a labelled `Divider` ("OR") is
1254
+ exactly the kind of content a screen reader user needs read aloud, rather
1255
+ than a purely decorative line. The label reuses `Text`'s
1256
+ `variant="caption" color="dim"` verbatim, which already resolves to the same
1257
+ font-size/weight/dim-opacity the web version's `.label` class hard-codes.
1258
+
1259
+ ### Highlight
1260
+
1261
+ ```tsx
1262
+ import { Highlight } from '@gnome-ui/react-native';
1263
+
1264
+ <Highlight text="Preferences for accessibility" query="access" />
1265
+ <Highlight text="The quick brown fox" query={['quick', 'fox']} />
1266
+ ```
1267
+
1268
+ Wraps every occurrence of `query` within `text` in a highlighted inline run
1269
+ — mirrors `@gnome-ui/react`'s `Highlight`, which wraps matches in a `<mark>`.
1270
+ Pairs with `SearchBar`'s suggestion list and any filterable list to show
1271
+ users which part of a result matched what they typed.
1272
+
1273
+ The outer span is the themed `Text` component (so callers get the same
1274
+ `variant`/`color` API as everywhere else), but each matched run is a plain,
1275
+ unthemed RN `Text` carrying only the highlight's own overrides — RN's `Text`
1276
+ is the one primitive that inherits ambient `fontSize`/`color`/`fontFamily`
1277
+ from a parent `Text` when nested, the same way the web version's `<mark>`
1278
+ inherits from its surrounding text and only overrides
1279
+ `background-color`/`font-weight`. Reaching for the themed `Text` for the
1280
+ marked runs too would reset them to its own default `variant="body"` sizing
1281
+ instead of inheriting whatever variant the caller chose for the whole
1282
+ string.
1283
+
1284
+ The web version's translucent `color-mix(in srgb, accent 30%, transparent)`
1285
+ background has no RN equivalent (`color-mix` is CSS-only) — resolved to a
1286
+ literal 8-digit `#RRGGBBAA` hex instead, since `accentBgColor` is always a
1287
+ plain 6-digit hex across all four theme variants. `border-radius` on the
1288
+ `<mark>` has no reliable port either: RN only paints `backgroundColor` on an
1289
+ inline (nested) `Text` run, not `borderRadius` — a decorative nicety
1290
+ dropped, not a behavior gap. `prefers-contrast: more`'s solid-background/
1291
+ white-text swap ports via `useResolvedContrast()`, the same hook `Button`
1292
+ already uses for its own high-contrast branching.
1293
+
926
1294
  ## Installation
927
1295
 
928
1296
  ```bash
@@ -0,0 +1,53 @@
1
+ import { StyleProp, ViewStyle } from 'react-native';
2
+ export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl';
3
+ /**
4
+ * Named color palette for the initials fallback.
5
+ * Mirrors libadwaita's avatar color set.
6
+ */
7
+ export type AvatarColor = 'blue' | 'green' | 'yellow' | 'orange' | 'red' | 'purple' | 'brown' | 'teal' | 'slate';
8
+ export interface AvatarProps {
9
+ /**
10
+ * Full name used to generate initials and — when `color` is omitted —
11
+ * to deterministically pick a background color.
12
+ */
13
+ name?: string;
14
+ /** Image URL. When provided the initials fallback is hidden. */
15
+ src?: string;
16
+ /** Accessible label. Defaults to `name`, then `"Avatar"`. */
17
+ alt?: string;
18
+ /** Size of the avatar. Defaults to `"md"`. */
19
+ size?: AvatarSize;
20
+ /**
21
+ * Override the auto-derived background color for the initials fallback.
22
+ * When omitted a color is derived from `name` via a stable hash.
23
+ */
24
+ color?: AvatarColor;
25
+ style?: StyleProp<ViewStyle>;
26
+ }
27
+ /**
28
+ * Circular avatar with image or initials fallback.
29
+ *
30
+ * Follows the Adwaita `AdwAvatar` pattern — deterministic color from name,
31
+ * up to two initials when no image is supplied. Rebuilt with `View`/`Image`/
32
+ * `Text` rather than ported from `@gnome-ui/react`'s DOM-based JSX, but
33
+ * mirrors its prop API and color-hash/initials math (pure JS, ported
34
+ * verbatim).
35
+ *
36
+ * The outer container carries `role="img"` + `accessibilityLabel` — RN's
37
+ * newer web-aligned `Role` union has an `"img"` value, a direct 1:1 port of
38
+ * the web version's `role="img"`, no substitution needed (same as
39
+ * `ProgressBar`'s `role="progressbar"`). The image/initials underneath are
40
+ * hidden from the accessibility tree
41
+ * (`accessibilityElementsHidden`/`importantForAccessibility="no"`, mirroring
42
+ * the web version's `aria-hidden` on both), so a screen reader gets one
43
+ * stop, not two — same reasoning as `SpinButton`'s hidden −/+ buttons.
44
+ *
45
+ * The web CSS's `box-shadow: inset 0 0 0 1px …` ring (drawn on top, doesn't
46
+ * affect layout) becomes a real 1px `borderWidth`/`borderColor` here (RN has
47
+ * no inset shadow) — the same substitution `Slider`'s thumb border already
48
+ * used for a ring effect, at the cost of a negligible 1px content-box inset
49
+ * the web version doesn't have.
50
+ *
51
+ * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Avatar.html
52
+ */
53
+ export declare const Avatar: ({ name, src, alt, size, color, style }: AvatarProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { AvatarColor, AvatarProps, AvatarSize } from './Avatar';
2
+ export { Avatar } from './Avatar';
@@ -0,0 +1,48 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ export type BadgeVariant = 'accent' | 'success' | 'warning' | 'error' | 'neutral';
4
+ export interface BadgeProps {
5
+ /**
6
+ * Visual style. Defaults to `"accent"`.
7
+ * - `accent` — blue, for counts and highlights.
8
+ * - `success` — green, for positive status.
9
+ * - `warning` — yellow, for cautionary status.
10
+ * - `error` — red, for failures or urgent counts.
11
+ * - `neutral` — gray, for inactive or secondary counts.
12
+ */
13
+ variant?: BadgeVariant;
14
+ /**
15
+ * When true, renders a small dot with no label — used for unread/online indicators.
16
+ * The `children` are ignored in dot mode.
17
+ */
18
+ dot?: boolean;
19
+ /** Number or short text to display. Keep to 1–3 characters. String/number render as a themed label; other nodes render as-is. */
20
+ children?: ReactNode;
21
+ /**
22
+ * When provided, the badge is positioned over this child element at the
23
+ * top-right corner.
24
+ */
25
+ anchor?: ReactNode;
26
+ style?: StyleProp<ViewStyle>;
27
+ }
28
+ /**
29
+ * Counter or status indicator, optionally overlaid on another element.
30
+ *
31
+ * Rebuilt with `View`/`Text` rather than ported from `@gnome-ui/react`'s
32
+ * DOM-based JSX, but mirrors its prop API. `children` renders as a themed
33
+ * `Text` label when it's a string or number (the common case — counts and
34
+ * short text); any other node renders as-is, the same convention `Button`'s
35
+ * `children` already established.
36
+ *
37
+ * The web CSS's `box-shadow: 0 0 0 2px var(--gnome-window-bg-color)` ring
38
+ * (always present, separating the badge from whatever's behind it) has no
39
+ * RN equivalent that avoids affecting layout — RN's `border*` shrinks the
40
+ * content box instead of drawing outside it. Reproduced instead with an
41
+ * outer wrapping `View` (2px padding, `theme.windowBgColor` background,
42
+ * pill radius) around the actual colored badge, so the ring appears to
43
+ * spread outward exactly like the web version's non-blurred shadow, without
44
+ * eating into the badge's own text padding.
45
+ *
46
+ * @see https://developer.gnome.org/hig/patterns/feedback/badges.html
47
+ */
48
+ export declare const Badge: ({ variant, dot, children, anchor, style }: BadgeProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { BadgeProps, BadgeVariant } from './Badge';
2
+ export { Badge } from './Badge';
@@ -0,0 +1,82 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ export interface BottomSheetProps {
4
+ /** Whether the sheet is visible. */
5
+ open: boolean;
6
+ /** Optional heading shown below the drag handle. */
7
+ title?: ReactNode;
8
+ children?: ReactNode;
9
+ /** Called when the user dismisses the sheet (backdrop press, Android back button, or drag down). */
10
+ onClose?: () => void;
11
+ /** Whether pressing the backdrop closes the sheet. Defaults to `true`. */
12
+ closeOnBackdrop?: boolean;
13
+ style?: StyleProp<ViewStyle>;
14
+ /** Applied to the backdrop, mirroring `Dialog`'s own `testID`. */
15
+ testID?: string;
16
+ }
17
+ /**
18
+ * Slide-up panel that overlays content from the bottom edge, mirroring
19
+ * `AdwBottomSheet` (libadwaita 1.6+) and `@gnome-ui/react`'s `BottomSheet`.
20
+ *
21
+ * Rebuilt with `View`/`Modal`/`PanResponder` rather than ported from the web
22
+ * version's DOM `Portal` + manual focus trap + raw pointer events —
23
+ * reusing this package's own established pieces: `Dialog`'s backdrop-
24
+ * opacity-on-an-`AnimatedPressable` + no-op-`Pressable`-around-the-card
25
+ * (so a tap on the card never bubbles to the backdrop) recipe, and
26
+ * `BackHandler`'s `hardwareBackPress` as the Android analog of the web
27
+ * version's Escape listener (same pattern `Dialog`/`Popover` already use).
28
+ * Focus-trapping and focus-restore-on-close have no port — no DOM
29
+ * `document.activeElement`/`querySelector` equivalent exists in RN, the
30
+ * same gap already present in every other floating component here.
31
+ *
32
+ * **Real drag-to-dismiss, not a fixed-panel simplification**: unlike the
33
+ * web version's raw `PointerEvent` handlers directly mutating
34
+ * `style.transform`, this uses `PanResponder` (the same core RN API
35
+ * `Slider` already proved handles a threshold-based drag gesture) driving
36
+ * a single `Animated.Value` — the *same* value used for the slide-in/
37
+ * slide-out entrance animation. `PanResponder`'s `gestureState.dy` is the
38
+ * cumulative vertical delta since the gesture started, so a plain
39
+ * `translateY.setValue(Math.max(0, gestureState.dy))` during
40
+ * `onPanResponderMove` reproduces the web version's own
41
+ * `Math.max(0, e.clientY - dragStartY.current)` clamp exactly, and a
42
+ * release past `DRAG_CLOSE_THRESHOLD` (150 px, same constant as the web
43
+ * version) animates the rest of the way down before calling `onClose`,
44
+ * otherwise it springs back to `0`. The gesture responder is attached only
45
+ * to the handle bar `View`, not the whole sheet, mirroring the web
46
+ * version's `onPointerDown` being scoped to `.handle` alone.
47
+ *
48
+ * **A real slide-up needs the sheet's own height first** — RN's
49
+ * `transform` has no percentage-of-self units (the same `Slider`/`Avatar`
50
+ * pitfall), unlike the web version's `translateY(100%)`. The sheet renders
51
+ * once off-screen (measured via `onLayout`, starting `translateY` at that
52
+ * measured height) before animating to `0` — the same "resolve own
53
+ * rendered size, then animate" two-step every other sized-on-open
54
+ * component in this package (`Dialog`, `Dropdown`, `Popover`) already
55
+ * needs, just for a transform offset instead of a floating position.
56
+ *
57
+ * **Real, timed exit animation, unlike `Dialog`**: `Dialog`'s web source
58
+ * has no exit keyframes at all (`Modal`'s `visible={false}` unmounts
59
+ * immediately, matching the web version's plain `return null`) — but
60
+ * `BottomSheet`'s web source explicitly animates both the backdrop fade
61
+ * and the sheet's slide-out before removing it. Ported with a local
62
+ * `visible` state that lags one animation behind the `open` prop: closing
63
+ * starts the exit `Animated.timing`s and only flips `visible` to `false`
64
+ * (unmounting the `Modal`'s content) in the animation's own completion
65
+ * callback — not a `setTimeout` racing a hardcoded duration like the web
66
+ * version, since `Animated`'s callback already fires exactly when the
67
+ * animation actually finishes.
68
+ *
69
+ * The web version's `backdrop-filter: blur(4px)` has no port — this
70
+ * package has no native blur view dependency, the same reasoning that
71
+ * already dropped `Sidebar`'s blurred `variant`. `useBodyScrollLock` has no
72
+ * RN equivalent needed: `Modal` already blocks all interaction with
73
+ * whatever's behind it, there is no scrollable "body" to lock separately.
74
+ *
75
+ * `role="dialog"` + `accessibilityViewIsModal` port 1:1 from `Dialog`'s own
76
+ * precedent. `children`, when a plain string, must be wrapped in `Text`
77
+ * before rendering — RN throws if a raw string is a `View`'s child, unlike
78
+ * the web version's `<div>{children}</div>`, which needed no such check.
79
+ *
80
+ * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.BottomSheet.html
81
+ */
82
+ export declare const BottomSheet: ({ open, title, children, onClose, closeOnBackdrop, style, testID, }: BottomSheetProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,2 @@
1
+ export type { BottomSheetProps } from './BottomSheet';
2
+ export { BottomSheet } from './BottomSheet';
@@ -0,0 +1,36 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ export interface DividerProps {
4
+ /** Optional centered label (e.g. `"OR"`, `"Continue with"`). */
5
+ children?: ReactNode;
6
+ /** Accessible name. Defaults to `children` when it's a string. */
7
+ accessibilityLabel?: string;
8
+ style?: StyleProp<ViewStyle>;
9
+ testID?: string;
10
+ }
11
+ /**
12
+ * Horizontal rule with an optional centered label — common auth/login-form
13
+ * pattern ("Sign in" / **OR** / "Continue with Google"). Mirrors
14
+ * `@gnome-ui/react`'s `Divider`.
15
+ *
16
+ * For a bare dividing line with no label, use `Separator` instead — it also
17
+ * supports a vertical orientation, which `Divider` does not.
18
+ *
19
+ * `role="separator"` ports 1:1 from RN's newer web-aligned `Role` union
20
+ * (the same one `Avatar`/`Badge`/`LevelBar` already reach for), unlike
21
+ * `Separator`'s own choice to render as `accessible={false}` — that
22
+ * component is purely decorative with nothing for a screen reader to
23
+ * announce, while a labelled `Divider` ("OR") is exactly the kind of
24
+ * content a screen reader user needs read aloud, so it stays in the
25
+ * accessibility tree instead. The label reuses `Text`'s `variant="caption"
26
+ * color="dim"` verbatim rather than hand-rolled styles, since that
27
+ * combination already resolves to the same font-size/weight/dim-opacity
28
+ * the web version's `.label` class hard-codes.
29
+ *
30
+ * The web version's `aria-orientation="horizontal"` has no port — `Divider`
31
+ * has no `orientation` prop at all (unlike `Separator`), so there is only
32
+ * ever one orientation to announce.
33
+ *
34
+ * @see https://developer.gnome.org/hig/patterns/containers.html
35
+ */
36
+ export declare const Divider: ({ children, accessibilityLabel, style, testID }: DividerProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { DividerProps } from './Divider';
2
+ export { Divider } from './Divider';