@gnome-ui/react-native 1.0.0 → 1.2.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
@@ -14,9 +14,12 @@ React Native component library following the [GNOME Human Interface Guidelines](
14
14
  > `Link`, `TextField`, `Switch`, `Checkbox`, `RadioButton`), Tier 2 Layout &
15
15
  > Containers (`Separator`, `Card`, `BoxedList`, `ActionRow`, `HeaderBar`),
16
16
  > and Tier 3 Navigation (`Tabs`, `ViewSwitcher`, `Sidebar`, `SearchBar`,
17
- > `PathBar`) fully ported. Tier 4 Feedback in progress: `Spinner`,
18
- > `ProgressBar`, `Skeleton`, and `Toast`/`Toaster` shipped `Banner`,
19
- > `Dialog`, `Tooltip`, `Status Page`, and `AnimatedIcon` remain. Component
17
+ > `PathBar`) fully ported. Tier 4 Feedback: `Spinner`, `ProgressBar`,
18
+ > `Skeleton`, `Toast`/`Toaster`, `Banner`, `Dialog`, `Tooltip`, and
19
+ > `AnimatedIcon` (which brought a new `Icon` component along with it, as its
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
20
23
  > ports from `@gnome-ui/react` continue tier by tier. See
21
24
  > [ROADMAP.md](../../ROADMAP.md) Priority 3.
22
25
 
@@ -680,12 +683,257 @@ skips straight to the settled state. RN's `AccessibilityRole` union has no
680
683
  closest available role, paired with `accessibilityLiveRegion="polite"`
681
684
  (Android's live-region API) as the nearest match to `aria-live="polite"`.
682
685
 
686
+ ### Banner
687
+
688
+ ```tsx
689
+ import { Banner } from '@gnome-ui/react-native';
690
+
691
+ <Banner variant="info">A new version is available.</Banner>;
692
+ <Banner variant="error" actionLabel="Retry" onAction={() => {}}>
693
+ Sync failed
694
+ </Banner>;
695
+ <Banner variant="success" dismissible onDismiss={() => {}}>
696
+ Changes saved successfully.
697
+ </Banner>;
698
+ ```
699
+
700
+ Persistent message strip for the top of a view. `variant` is `"info"`
701
+ (default) | `"warning"` | `"error"` | `"success"`, each mapping to the
702
+ matching `theme.<variant>BgColor`/`<variant>FgColor` token pair. Unlike
703
+ `Toast`, it never auto-dismisses — it stays until the user acts or presses
704
+ the optional dismiss button, so there's no `duration` prop at all.
705
+
706
+ Same accessibility substitution as `Toast`: RN's `AccessibilityRole` union
707
+ has no "status" value (the web version's `role="status"`), so `"alert"` +
708
+ `accessibilityLiveRegion="polite"` stands in for it. The banner itself is a
709
+ plain `View`, not `Pressable` (only its action/dismiss buttons are
710
+ interactive), so — per the `BoxedList` lesson that a bare `View` isn't an
711
+ accessibility element by default — `accessible` is set explicitly alongside
712
+ `accessibilityRole`.
713
+
714
+ The web version's per-variant `:hover`/`:active` background tint on the
715
+ action/dismiss buttons (a light overlay on the darker info/error/success
716
+ backgrounds, a dark one on the light warning background) collapses to a
717
+ single `Pressable`-pressed-state overlay, the same simplification `Toast`
718
+ and `Card` already made for their own press states.
719
+
720
+ ### Dialog
721
+
722
+ ```tsx
723
+ import { Dialog } from '@gnome-ui/react-native';
724
+
725
+ // Standard
726
+ <Dialog open={open} title="About Sync" onClose={() => setOpen(false)}>
727
+ Files are synced automatically every 15 minutes.
728
+ </Dialog>;
729
+
730
+ // With buttons
731
+ <Dialog
732
+ open={open}
733
+ title="Discard changes?"
734
+ onClose={() => setOpen(false)}
735
+ buttons={[
736
+ { label: 'Keep editing', onPress: () => setOpen(false) },
737
+ { label: 'Discard', variant: 'destructive', onPress: () => setOpen(false) },
738
+ ]}
739
+ >
740
+ Your changes have not been saved.
741
+ </Dialog>;
742
+
743
+ // Alert — role="alertdialog" + responses/onResponse
744
+ <Dialog
745
+ open={open}
746
+ role="alertdialog"
747
+ title="Delete file?"
748
+ responses={[
749
+ { id: 'cancel', label: 'Cancel' },
750
+ { id: 'delete', label: 'Delete', variant: 'destructive' },
751
+ ]}
752
+ onResponse={(id) => setOpen(false)}
753
+ >
754
+ This action cannot be undone.
755
+ </Dialog>;
756
+ ```
757
+
758
+ Blocking modal dialog. **Standard** takes `title` + `children` + `buttons[]`
759
+ with per-button `onPress`; **Alert** (`role="alertdialog"`) takes
760
+ `responses[]` + a single `onResponse(id)` instead — the same two-API shape
761
+ as `@gnome-ui/react`'s `Dialog`, since `AlertDialog` there is a mode of the
762
+ same component rather than a separate one. `AboutDialog` (a distinct
763
+ `@gnome-ui/react` component, not a `Dialog` variant) has no RN port yet.
764
+
765
+ Built on RN's own `Modal` (`transparent`, `animationType="none"` — the
766
+ entrance is a custom `Animated.timing`) rather than the web version's DOM
767
+ `Portal` + manual focus trap: `Modal` already floats above everything with
768
+ no portal target needed, and already blocks interaction with the screen
769
+ behind it, so there's no `useBodyScrollLock` port. Its `onRequestClose`
770
+ fires on the **Android hardware back button** — the direct analog of the
771
+ web version's document-level Escape listener (iOS has no back button, so
772
+ this is Android-only, matching the platform's own convention). Focus
773
+ trapping (`Tab`/`Shift+Tab` cycling between focusable elements) has no
774
+ port at all — there's no keyboard `Tab` concept in RN's touch-first model,
775
+ the same reasoning that already dropped `TabBar`'s roving-tabindex arrow
776
+ keys.
777
+
778
+ `role` is set via RN's newer, web-aligned `role` prop (not
779
+ `accessibilityRole`) — its `Role` union has real `"dialog"`/`"alertdialog"`
780
+ values, unlike the older `AccessibilityRole` enum `Toast`/`Banner` had to
781
+ substitute `"alert"` into for the web's `role="status"`.
782
+ `accessibilityViewIsModal` (iOS-only) is the closest match to
783
+ `aria-modal="true"`, restricting VoiceOver to the dialog's subtree. The
784
+ dialog card sets `accessible` explicitly (a bare `View` with `role` isn't
785
+ an accessibility element by default — the same `BoxedList` lesson) —
786
+ **this shares the same open, unverified-on-a-real-device accessibility
787
+ question already flagged for `BoxedList`/`TabBar`/`ViewSwitcher`'s
788
+ container-role pattern**: `accessible={true}` on a container may collapse
789
+ its subtree into one opaque VoiceOver stop, which for `Dialog` specifically
790
+ would mean its footer buttons become unreachable via VoiceOver even though
791
+ they're independently `Pressable`. Kept for `getByRole` testability and
792
+ consistency with the established pattern, but this is the component where
793
+ that tradeoff matters most — worth prioritizing for real-device screen
794
+ reader verification before it's treated as settled.
795
+
796
+ ### Tooltip
797
+
798
+ ```tsx
799
+ import { Button, Tooltip } from '@gnome-ui/react-native';
800
+
801
+ <Tooltip label="Save file" placement="top">
802
+ <Button accessibilityLabel="Save">Save</Button>
803
+ </Tooltip>;
804
+ ```
805
+
806
+ Floating informational label. Positioned automatically and flips to the
807
+ opposite side (then to whichever side actually fits) when the preferred
808
+ placement has no room — the same algorithm as `@gnome-ui/react`'s
809
+ `Tooltip`, measured with `measureInWindow()` instead of
810
+ `getBoundingClientRect()`.
811
+
812
+ **Trigger differs from the web version by necessity**: the web `Tooltip`
813
+ only shows on mouse hover / keyboard focus — touch has no hover state, so
814
+ it explicitly never shows on touch. RN is touch-first, so the primary
815
+ trigger here is **long-press** (`delayLongPress={delay}`, released via
816
+ `onPressOut`) — the standard mobile "peek" idiom. `onHoverIn`/`onHoverOut`
817
+ are also wired for hover-capable input (trackpad/mouse on iPad, or a
818
+ pointer-driven RN target) and `onFocus`/`onBlur` for external-keyboard
819
+ accessibility, both delayed the same way the web version delays hover.
820
+
821
+ Built on RN's own `Modal` (transparent, `pointerEvents="box-none"`), the
822
+ same portal-substitute `Dialog` uses. `role="tooltip"` ports 1:1 — RN's
823
+ `Role` union already has a `"tooltip"` value. `aria-describedby` has no RN
824
+ equivalent, so the label is set as the trigger's `accessibilityHint`
825
+ instead (unless the trigger already provides its own). The bubble's arrow
826
+ reuses the same zero-size / transparent-border-on-three-sides triangle
827
+ trick as the web CSS — RN `View`s support per-side `border*Color` too, the
828
+ same technique `Spinner`'s ring already relies on.
829
+
830
+ Not ported: repositioning on scroll/resize while visible (RN has no global
831
+ scroll event, and a long-press is naturally cancelled by a scroll gesture
832
+ starting). `tooltipBgColor`/`tooltipFgColor` aren't real `@gnome-ui/core`
833
+ tokens (only CSS var fallbacks), so the RN port hardcodes the same literal
834
+ light/dark values, the same workaround `Spinner`'s track color established.
835
+
836
+ ### Icon
837
+
838
+ ```tsx
839
+ import { Search } from '@gnome-ui/icons';
840
+ import { Icon } from '@gnome-ui/react-native';
841
+
842
+ <Icon icon={Search} label="Search" size="lg" color="blue" />;
843
+ ```
844
+
845
+ Renders an icon as an inline SVG via `react-native-svg` (a new peer
846
+ dependency — this package's first). Accepts the same `AnyIconDefinition`
847
+ union as `@gnome-ui/react`'s `Icon`: a structured `paths`-based
848
+ `IconDefinition` from `@gnome-ui/icons`, a `simple-icons` `SimpleIcon`, or a
849
+ plain `{ path }` object. `color` picks a named GNOME palette hue
850
+ (`theme.blue3`, `theme.red3`, …) — RN has no `currentColor` to inherit from
851
+ a parent the way the web version does, so omitting `color` resolves to the
852
+ theme's default foreground color explicitly instead.
853
+
854
+ `animated` icons (`Syncing`, `Recording`, `Downloading`, `Connecting`) carry
855
+ raw `svg` markup instead of `paths` — rendered here through `react-native-
856
+ svg`'s `SvgXml`. It parses the structural elements (`<g>`/`<path>`/
857
+ `<circle>`) but has no CSS engine, so the markup's embedded `<style>`/
858
+ `@keyframes` block is silently dropped and the shapes render at their
859
+ authored rest position — which happens to be exactly the desired inert,
860
+ static-frame behavior for a plain `<Icon>`, no special-casing needed. Wrap
861
+ in `<AnimatedIcon>` to actually play the motion.
862
+
863
+ ### AnimatedIcon
864
+
865
+ ```tsx
866
+ import { Syncing } from '@gnome-ui/icons';
867
+ import { AnimatedIcon } from '@gnome-ui/react-native';
868
+
869
+ <AnimatedIcon icon={Syncing} playing={isSyncing} label="Syncing" />;
870
+ ```
871
+
872
+ Plays the motion for a known `animated` icon (`Syncing`, `Recording`,
873
+ `Downloading`, `Connecting`) — rendered through plain `<Icon>`, these show a
874
+ static frame instead, same as `@gnome-ui/react`'s `AnimatedIcon`.
875
+
876
+ Unlike the web version (which plays a CSS animation embedded in the icon's
877
+ raw `svg` markup via a `--gnome-icon-play-state` custom property), RN has
878
+ no CSS engine to interpret `@keyframes` at all. Each of the 4 known icons'
879
+ motion is instead hand-built with `Animated`, matched by referential
880
+ identity against `@gnome-ui/icons`' own exports (`Syncing` → full-turn
881
+ rotation, `Recording` → opacity pulse, `Downloading` → a translate+opacity
882
+ "drop" on the arrow over a static tray, `Connecting` → three signal dots
883
+ pulsing in a staggered sweep) — an icon `AnimatedIcon` doesn't recognize
884
+ (a future 5th animated icon, or a consumer-authored one) falls back to the
885
+ static `<Icon>` frame rather than throwing. Regardless of `playing`, the
886
+ animation is always paused when the OS reduced-motion setting is on.
887
+
888
+ ### Dropdown
889
+
890
+ ```tsx
891
+ import { Dropdown } from '@gnome-ui/react-native';
892
+
893
+ <Dropdown
894
+ options={[
895
+ { value: 'blue', label: 'Blue' },
896
+ { value: 'green', label: 'Green', description: 'A calm accent' },
897
+ ]}
898
+ value={accentColor}
899
+ onChange={setAccentColor}
900
+ placeholder="Accent color"
901
+ />;
902
+ ```
903
+
904
+ Expandable option list following the Adwaita combo-row pattern, mirroring
905
+ `@gnome-ui/react`'s `Dropdown`.
906
+
907
+ Built on RN's own `Modal` (transparent) — the same portal-substitute
908
+ `Dialog`/`Tooltip` already use — with a full-screen backdrop `Pressable`
909
+ that closes the list on an outside tap (the RN analog of the web version's
910
+ document-level "click outside" listener; unlike `Tooltip`'s backdrop, this
911
+ one isn't `pointerEvents="box-none"`, since it's meant to catch that tap
912
+ rather than pass it through). `open` flips synchronously on trigger press;
913
+ the trigger's on-screen rect and the panel's own rendered height each
914
+ resolve independently into state, combined by a separate effect into the
915
+ final position and flip-up/flip-down direction — the same two-independent-
916
+ async-measurements pattern `Tooltip` established.
917
+
918
+ Keyboard navigation (↑/↓ roving highlight, Home/End, type-ahead) has no
919
+ port — RN's touch-first model has no keyboard focus to drive it, the same
920
+ reasoning that already dropped `TabBar`'s roving-tabindex arrow keys.
921
+ Selection is by direct tap only. `role="combobox"` on the trigger ports
922
+ 1:1; RN's `Role` union has no `"listbox"` value, so the panel uses
923
+ `role="list"` instead — the same closest-available substitution `BoxedList`
924
+ already established for a plain list container.
925
+
683
926
  ## Installation
684
927
 
685
928
  ```bash
686
- npm install @gnome-ui/react-native react-native
929
+ npm install @gnome-ui/react-native react-native react-native-svg
687
930
  ```
688
931
 
932
+ `react-native-svg` is a peer dependency, only needed for `Icon`/
933
+ `AnimatedIcon` — it ships as one of Expo Go's included native modules, so
934
+ Expo projects on `npx expo install react-native-svg` need no extra native
935
+ build step; bare RN projects need it linked as usual for a native module.
936
+
689
937
  ## Example app
690
938
 
691
939
  [`apps/react-native-example`](../../apps/react-native-example) is a
@@ -0,0 +1,38 @@
1
+ import { IconDefinition } from '@gnome-ui/icons';
2
+ import { IconProps } from '../Icon';
3
+ export interface AnimatedIconProps extends Omit<IconProps, 'icon'> {
4
+ /** Animated icon from `@gnome-ui/icons` (`animated: true` — `Syncing`, `Recording`, `Downloading`, `Connecting`). */
5
+ icon: IconDefinition;
6
+ /**
7
+ * Whether the animation plays. Defaults to `true`.
8
+ *
9
+ * Regardless of this prop, the animation is always paused when the OS
10
+ * reduced-motion setting is on — callers don't need to check
11
+ * `useReducedMotion()` themselves.
12
+ */
13
+ playing?: boolean;
14
+ }
15
+ /**
16
+ * Plays the motion for a known `animated` icon from `@gnome-ui/icons`
17
+ * (`Syncing`, `Recording`, `Downloading`, `Connecting`) — rendered through
18
+ * plain `<Icon>`, these show a static frame instead.
19
+ *
20
+ * Unlike `@gnome-ui/react`'s `AnimatedIcon` (which plays a CSS animation
21
+ * embedded in the icon's raw `svg` markup via a `--gnome-icon-play-state`
22
+ * custom property), RN has no CSS engine to interpret `@keyframes` at all —
23
+ * `<Icon>` already drops the `<style>` block silently (see its own doc
24
+ * comment), and this component instead hand-builds each of the 4 known
25
+ * icons' exact motion with `Animated`, matched against `RECIPES` by
26
+ * reference. When playing and reduced motion is off, it renders its own
27
+ * `<Svg>` with the matching `*Motion` recipe as children; otherwise (not
28
+ * playing, reduced motion on, or an icon `RECIPES` doesn't recognize) it
29
+ * defers straight to `<Icon>`.
30
+ *
31
+ * Useful for progress, sync, recording, download, and connection states, or
32
+ * cross-fading between icons by swapping `icon` while `playing` stays true.
33
+ *
34
+ * @example
35
+ * import { Syncing } from "@gnome-ui/icons";
36
+ * <AnimatedIcon icon={Syncing} playing={isSyncing} label="Syncing" />
37
+ */
38
+ export declare const AnimatedIcon: ({ icon, playing, size, width, height, label, color, }: AnimatedIconProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { AnimatedIconProps } from './AnimatedIcon';
2
+ export { AnimatedIcon } from './AnimatedIcon';
@@ -0,0 +1,50 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleProp, ViewProps, ViewStyle } from 'react-native';
3
+ export type BannerVariant = 'info' | 'warning' | 'error' | 'success';
4
+ export interface BannerProps extends Omit<ViewProps, 'style'> {
5
+ /**
6
+ * Visual emphasis level.
7
+ * - `info` (default) — neutral, accent-colored. Use for tips and notices.
8
+ * - `warning` — yellow. Use for recoverable problems.
9
+ * - `error` — red. Use for failures that need attention.
10
+ * - `success` — green. Use for confirmations.
11
+ */
12
+ variant?: BannerVariant;
13
+ /** The message text. Keep it short — one or two sentences. */
14
+ children: ReactNode;
15
+ /**
16
+ * Label for the optional action button placed at the trailing end.
17
+ * When provided, `onAction` should also be supplied.
18
+ */
19
+ actionLabel?: string;
20
+ /** Called when the user presses the action button. */
21
+ onAction?: () => void;
22
+ /**
23
+ * When true a dismiss (×) button is shown at the trailing edge.
24
+ * Provide `onDismiss` to handle removal from the list.
25
+ */
26
+ dismissible?: boolean;
27
+ /** Called when the user presses the dismiss button. */
28
+ onDismiss?: () => void;
29
+ style?: StyleProp<ViewStyle>;
30
+ }
31
+ /**
32
+ * Persistent message strip displayed at the top of a view, following the
33
+ * Adwaita `AdwBanner` pattern. Use for important information that persists
34
+ * until the user acts or explicitly dismisses it — unlike `Toast`, it never
35
+ * auto-dismisses.
36
+ *
37
+ * RN's `AccessibilityRole` union has no "status" value (the web version's
38
+ * `role="status"`); `"alert"` is the closest available role, paired with
39
+ * `accessibilityLiveRegion="polite"` (Android's live-region API) as the
40
+ * nearest match to `aria-live="polite"` — the same substitution `Toast`
41
+ * already established for the identical web role pair. Since the banner
42
+ * itself is a plain `View` (not `Pressable` — only its buttons are
43
+ * interactive), `accessible` is set explicitly alongside `accessibilityRole`,
44
+ * per the `BoxedList` lesson that a bare `View` isn't an accessibility
45
+ * element by default.
46
+ *
47
+ * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Banner.html
48
+ * @see https://developer.gnome.org/hig/patterns/feedback/banners.html
49
+ */
50
+ export declare const Banner: ({ variant, children, actionLabel, onAction, dismissible, onDismiss, style, ...viewProps }: BannerProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { BannerProps, BannerVariant } from './Banner';
2
+ export { Banner } from './Banner';
@@ -0,0 +1,105 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ export type DialogButtonVariant = 'default' | 'suggested' | 'destructive';
4
+ export interface DialogButton {
5
+ /** Button label. */
6
+ label: string;
7
+ /** Visual variant. Defaults to `"default"`. */
8
+ variant?: DialogButtonVariant;
9
+ /** Called when the button is pressed. */
10
+ onPress: () => void;
11
+ /** Whether the button is disabled. */
12
+ disabled?: boolean;
13
+ }
14
+ export type AlertDialogResponseVariant = DialogButtonVariant;
15
+ export interface AlertDialogResponse {
16
+ /**
17
+ * Unique identifier returned via `onResponse`.
18
+ * Use `"cancel"` by convention for the dismissive action.
19
+ */
20
+ id: string;
21
+ /** Button label. */
22
+ label: string;
23
+ /** Visual emphasis. Defaults to `"default"`. */
24
+ variant?: AlertDialogResponseVariant;
25
+ /** Disables the button. */
26
+ disabled?: boolean;
27
+ }
28
+ export interface DialogProps {
29
+ /** Whether the dialog is visible. */
30
+ open: boolean;
31
+ /** Dialog heading. */
32
+ title?: ReactNode;
33
+ /** Body content. */
34
+ children?: ReactNode;
35
+ /** Action buttons (standard dialog API). */
36
+ buttons?: DialogButton[];
37
+ /** Called on Android back button / backdrop press. */
38
+ onClose?: () => void;
39
+ /** Whether pressing the backdrop closes the dialog. Defaults to `true`. */
40
+ closeOnBackdrop?: boolean;
41
+ /**
42
+ * Use `"alertdialog"` for confirmations and destructive warnings — screen
43
+ * readers announce it immediately. Defaults to `"dialog"`.
44
+ *
45
+ * When using `role="alertdialog"`, prefer the `responses`/`onResponse` API
46
+ * over `buttons`/`onPress` for semantic clarity.
47
+ */
48
+ role?: 'dialog' | 'alertdialog';
49
+ /**
50
+ * Response buttons (AlertDialog API). Alternative to `buttons` — each
51
+ * response has a semantic `id` returned via `onResponse`. The Android back
52
+ * button and backdrop press fire the first non-destructive response.
53
+ */
54
+ responses?: AlertDialogResponse[];
55
+ /**
56
+ * Called with the `id` of the response button pressed.
57
+ * Required when `responses` is provided.
58
+ */
59
+ onResponse?: (id: string) => void;
60
+ style?: StyleProp<ViewStyle>;
61
+ /** Forwarded to the backdrop — useful for testing. */
62
+ testID?: string;
63
+ }
64
+ /**
65
+ * Blocking modal dialog following the Adwaita pattern.
66
+ *
67
+ * **Standard** — `title` + `children` + `buttons[]`.
68
+ *
69
+ * **Alert** — add `role="alertdialog"` + `responses[]` + `onResponse`.
70
+ * Uses a semantic response id instead of per-button `onPress`. The Android
71
+ * back button / backdrop press fire the first non-destructive response.
72
+ * Mirrors `AdwAlertDialog`.
73
+ *
74
+ * Built on RN's own `Modal` rather than `@gnome-ui/react`'s DOM `Portal` +
75
+ * manual focus trap: `Modal` already floats above everything with no portal
76
+ * target needed, already blocks interaction with the screen behind it (no
77
+ * `useBodyScrollLock` equivalent needed), and its `onRequestClose` fires on
78
+ * the Android hardware back button — the direct analog of the web version's
79
+ * document-level Escape listener. Focus-trapping (`Tab`/`Shift+Tab` cycling)
80
+ * has no port: there is no keyboard `Tab` concept in RN's touch-first model,
81
+ * the same reasoning that already dropped `TabBar`'s roving-tabindex arrow
82
+ * keys.
83
+ *
84
+ * `role` is passed straight through as RN's own `role` prop (not
85
+ * `accessibilityRole`) — RN's newer, web-aligned `Role` union has real
86
+ * `"dialog"`/`"alertdialog"` values, unlike the older `AccessibilityRole`
87
+ * enum `Toast`/`Banner` had to substitute `"alert"` into for the web's
88
+ * `role="status"`. `accessibilityViewIsModal` (iOS) is the closest match to
89
+ * `aria-modal="true"`, restricting VoiceOver to the dialog's subtree.
90
+ *
91
+ * There is no exit animation on either platform — the source CSS only
92
+ * defines entrance keyframes, and `Modal`'s `visible={false}` unmounts
93
+ * immediately, matching the web version returning `null` outright when
94
+ * `!open`. Entrance is an `Animated.timing` fading + scaling + sliding the
95
+ * card in (mirrors `@keyframes dialog-in`) alongside a plain backdrop fade
96
+ * (`@keyframes backdrop-in`), replayed via a `useEffect` keyed on `open`
97
+ * since — unlike `Toast`, which mounts once per instance — the same
98
+ * `Dialog` element toggles `open` repeatedly while `Modal` itself
99
+ * mounts/unmounts internally. `useReducedMotion()` skips straight to the
100
+ * settled state.
101
+ *
102
+ * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Dialog.html
103
+ * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.AlertDialog.html
104
+ */
105
+ export declare const Dialog: ({ open, title, children, buttons, onClose, closeOnBackdrop, role, responses, onResponse, style, testID, }: DialogProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { AlertDialogResponse, AlertDialogResponseVariant, DialogButton, DialogButtonVariant, DialogProps, } from './Dialog';
2
+ export { Dialog } from './Dialog';
@@ -0,0 +1,56 @@
1
+ import { StyleProp, ViewStyle } from 'react-native';
2
+ export interface DropdownOption<V extends string = string> {
3
+ /** The value submitted / returned on selection. */
4
+ value: V;
5
+ /** Display label shown in the list and trigger. */
6
+ label: string;
7
+ /** Optional descriptive text shown below the label. */
8
+ description?: string;
9
+ /** Whether the option is selectable. */
10
+ disabled?: boolean;
11
+ }
12
+ export interface DropdownProps<V extends string = string> {
13
+ /** The list of selectable options. */
14
+ options: DropdownOption<V>[];
15
+ /** The currently selected value. */
16
+ value?: V;
17
+ /** Called when the user selects an option. */
18
+ onChange?: (value: V) => void;
19
+ /** Placeholder shown when no option is selected. */
20
+ placeholder?: string;
21
+ /** Accessible label for the control. */
22
+ accessibilityLabel?: string;
23
+ /** Disables the entire control. */
24
+ disabled?: boolean;
25
+ style?: StyleProp<ViewStyle>;
26
+ testID?: string;
27
+ }
28
+ /**
29
+ * Expandable option list following the Adwaita combo-row / drop-down
30
+ * pattern, mirroring `@gnome-ui/react`'s `Dropdown`.
31
+ *
32
+ * Built on RN's own `Modal` (transparent) — the same portal-substitute
33
+ * `Dialog`/`Tooltip` already use — with a full-screen backdrop `Pressable`
34
+ * (not `pointerEvents="box-none"` like `Tooltip`'s: unlike a tooltip, this
35
+ * panel is meant to catch and close on an outside tap, the RN analog of the
36
+ * web version's document-level "click outside" listener). `open` flips
37
+ * synchronously on trigger press; the trigger's on-screen rect
38
+ * (`measureInWindow`) and the panel's own rendered height (`onLayout`) each
39
+ * resolve independently into state, combined by a separate effect into the
40
+ * final `top`/`left`/`width`/`flipUp` — the same two-independent-
41
+ * async-measurements pattern `Tooltip` established (and for the same
42
+ * reason: gating `open` itself on the native measurement callback would
43
+ * make the component untestable in this package's Jest environment, where
44
+ * `measureInWindow` never calls back).
45
+ *
46
+ * Keyboard navigation (↑/↓ roving highlight, Home/End, type-ahead) has no
47
+ * port — RN's touch-first model has no keyboard focus to drive it, the
48
+ * same reasoning that already dropped `TabBar`'s roving-tabindex arrow
49
+ * keys. Selection is by direct tap only.
50
+ *
51
+ * `role="combobox"` on the trigger ports 1:1. RN's `Role` union has no
52
+ * `"listbox"` value (unlike `"option"`, which does exist) — the panel uses
53
+ * `role="list"` instead, the same closest-available substitution `BoxedList`
54
+ * already established for a plain list container.
55
+ */
56
+ export declare const Dropdown: <V extends string = string>({ options, value, onChange, placeholder, accessibilityLabel, disabled, style, testID, }: DropdownProps<V>) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { DropdownOption, DropdownProps } from './Dropdown';
2
+ export { Dropdown } from './Dropdown';
@@ -0,0 +1,56 @@
1
+ import { AnyIconDefinition } from '@gnome-ui/icons';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ export type IconSize = 'sm' | 'md' | 'lg';
4
+ /** Named GNOME palette color for the icon. `"default"` (or omitting the prop) resolves to the theme's default foreground color. */
5
+ export type IconColor = 'default' | 'blue' | 'green' | 'yellow' | 'orange' | 'red' | 'purple' | 'brown';
6
+ export interface IconProps {
7
+ /** Icon from `@gnome-ui/icons`, a `simple-icons` icon, or a raw `{ path }` object. */
8
+ icon: AnyIconDefinition;
9
+ /**
10
+ * Rendered size.
11
+ * - `sm` — 12 px
12
+ * - `md` — 16 px (default)
13
+ * - `lg` — 20 px
14
+ *
15
+ * Override with `width`/`height` for non-standard sizes.
16
+ */
17
+ size?: IconSize;
18
+ width?: number;
19
+ height?: number;
20
+ /** Accessible label. Omit for decorative icons — they are hidden from screen readers. */
21
+ label?: string;
22
+ /**
23
+ * Named GNOME palette color. Omit (or pass `"default"`) to use the
24
+ * theme's default foreground color — RN has no `currentColor`
25
+ * equivalent to inherit from a parent, unlike the web version.
26
+ */
27
+ color?: IconColor;
28
+ /** Forwarded to the underlying `Svg` — useful for a `transform` (e.g. a rotated disclosure chevron) or `margin`. */
29
+ style?: StyleProp<ViewStyle>;
30
+ }
31
+ /**
32
+ * Renders an icon as an inline SVG, via `react-native-svg`.
33
+ *
34
+ * Accepts icons from `@gnome-ui/icons`, any `simple-icons` icon, or a plain
35
+ * `{ path }` object — same `AnyIconDefinition` union as `@gnome-ui/react`'s
36
+ * `Icon`. Pass `label` only when the icon conveys meaning on its own.
37
+ *
38
+ * `animated` icons (`Syncing`, `Recording`, …) carry raw `svg` markup
39
+ * instead of `paths` — rendered here through `SvgXml`, which parses the
40
+ * structural elements (`<g>`/`<path>`/`<circle>`) but has no CSS engine, so
41
+ * the embedded `<style>`/`@keyframes` block is silently dropped and the
42
+ * shapes render at their authored rest position. That happens to be exactly
43
+ * the desired "inert, static frame" behavior for a plain `<Icon>` — no
44
+ * special-casing needed here; wrap in `<AnimatedIcon>` to actually play the
45
+ * motion (RN has to hand-build that with `Animated`, since there is no CSS
46
+ * animation engine to interpret the markup's keyframes).
47
+ *
48
+ * @example
49
+ * import { Search } from "@gnome-ui/icons";
50
+ * <Icon icon={Search} label="Search" />
51
+ *
52
+ * @example
53
+ * import { siGithub } from "simple-icons";
54
+ * <Icon icon={siGithub} label="GitHub" />
55
+ */
56
+ export declare const Icon: ({ icon, size, width, height, label, color, style }: IconProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { IconColor, IconProps, IconSize } from './Icon';
2
+ export { Icon } from './Icon';
@@ -0,0 +1,19 @@
1
+ import { useGnomeTheme } from '../../GnomeProvider';
2
+ import { IconColor, IconSize } from './Icon';
3
+ /**
4
+ * Non-component helpers shared between `Icon` and `AnimatedIcon` — kept in
5
+ * their own module (not exported from `Icon.tsx` itself) so both files stay
6
+ * component-only for Fast Refresh, per the project's `react-refresh/
7
+ * only-export-components` lint rule.
8
+ */
9
+ export declare const ICON_SIZE_MAP: Record<IconSize, number>;
10
+ export declare function resolveIconColor(theme: ReturnType<typeof useGnomeTheme>, color: IconColor | undefined): string;
11
+ export declare function iconAccessibilityProps(label: string | undefined): {
12
+ accessible: true;
13
+ accessibilityLabel: string;
14
+ accessibilityRole: "image";
15
+ } | {
16
+ accessible: false;
17
+ accessibilityRole: "none";
18
+ accessibilityLabel?: undefined;
19
+ };