@gnome-ui/react-native 1.0.0 → 1.1.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
@@ -15,8 +15,8 @@ React Native component library following the [GNOME Human Interface Guidelines](
15
15
  > Containers (`Separator`, `Card`, `BoxedList`, `ActionRow`, `HeaderBar`),
16
16
  > and Tier 3 Navigation (`Tabs`, `ViewSwitcher`, `Sidebar`, `SearchBar`,
17
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
18
+ > `ProgressBar`, `Skeleton`, `Toast`/`Toaster`, `Banner`, and `Dialog`
19
+ > shipped `Tooltip`, `Status Page`, and `AnimatedIcon` remain. Component
20
20
  > ports from `@gnome-ui/react` continue tier by tier. See
21
21
  > [ROADMAP.md](../../ROADMAP.md) Priority 3.
22
22
 
@@ -680,6 +680,116 @@ skips straight to the settled state. RN's `AccessibilityRole` union has no
680
680
  closest available role, paired with `accessibilityLiveRegion="polite"`
681
681
  (Android's live-region API) as the nearest match to `aria-live="polite"`.
682
682
 
683
+ ### Banner
684
+
685
+ ```tsx
686
+ import { Banner } from '@gnome-ui/react-native';
687
+
688
+ <Banner variant="info">A new version is available.</Banner>;
689
+ <Banner variant="error" actionLabel="Retry" onAction={() => {}}>
690
+ Sync failed
691
+ </Banner>;
692
+ <Banner variant="success" dismissible onDismiss={() => {}}>
693
+ Changes saved successfully.
694
+ </Banner>;
695
+ ```
696
+
697
+ Persistent message strip for the top of a view. `variant` is `"info"`
698
+ (default) | `"warning"` | `"error"` | `"success"`, each mapping to the
699
+ matching `theme.<variant>BgColor`/`<variant>FgColor` token pair. Unlike
700
+ `Toast`, it never auto-dismisses — it stays until the user acts or presses
701
+ the optional dismiss button, so there's no `duration` prop at all.
702
+
703
+ Same accessibility substitution as `Toast`: RN's `AccessibilityRole` union
704
+ has no "status" value (the web version's `role="status"`), so `"alert"` +
705
+ `accessibilityLiveRegion="polite"` stands in for it. The banner itself is a
706
+ plain `View`, not `Pressable` (only its action/dismiss buttons are
707
+ interactive), so — per the `BoxedList` lesson that a bare `View` isn't an
708
+ accessibility element by default — `accessible` is set explicitly alongside
709
+ `accessibilityRole`.
710
+
711
+ The web version's per-variant `:hover`/`:active` background tint on the
712
+ action/dismiss buttons (a light overlay on the darker info/error/success
713
+ backgrounds, a dark one on the light warning background) collapses to a
714
+ single `Pressable`-pressed-state overlay, the same simplification `Toast`
715
+ and `Card` already made for their own press states.
716
+
717
+ ### Dialog
718
+
719
+ ```tsx
720
+ import { Dialog } from '@gnome-ui/react-native';
721
+
722
+ // Standard
723
+ <Dialog open={open} title="About Sync" onClose={() => setOpen(false)}>
724
+ Files are synced automatically every 15 minutes.
725
+ </Dialog>;
726
+
727
+ // With buttons
728
+ <Dialog
729
+ open={open}
730
+ title="Discard changes?"
731
+ onClose={() => setOpen(false)}
732
+ buttons={[
733
+ { label: 'Keep editing', onPress: () => setOpen(false) },
734
+ { label: 'Discard', variant: 'destructive', onPress: () => setOpen(false) },
735
+ ]}
736
+ >
737
+ Your changes have not been saved.
738
+ </Dialog>;
739
+
740
+ // Alert — role="alertdialog" + responses/onResponse
741
+ <Dialog
742
+ open={open}
743
+ role="alertdialog"
744
+ title="Delete file?"
745
+ responses={[
746
+ { id: 'cancel', label: 'Cancel' },
747
+ { id: 'delete', label: 'Delete', variant: 'destructive' },
748
+ ]}
749
+ onResponse={(id) => setOpen(false)}
750
+ >
751
+ This action cannot be undone.
752
+ </Dialog>;
753
+ ```
754
+
755
+ Blocking modal dialog. **Standard** takes `title` + `children` + `buttons[]`
756
+ with per-button `onPress`; **Alert** (`role="alertdialog"`) takes
757
+ `responses[]` + a single `onResponse(id)` instead — the same two-API shape
758
+ as `@gnome-ui/react`'s `Dialog`, since `AlertDialog` there is a mode of the
759
+ same component rather than a separate one. `AboutDialog` (a distinct
760
+ `@gnome-ui/react` component, not a `Dialog` variant) has no RN port yet.
761
+
762
+ Built on RN's own `Modal` (`transparent`, `animationType="none"` — the
763
+ entrance is a custom `Animated.timing`) rather than the web version's DOM
764
+ `Portal` + manual focus trap: `Modal` already floats above everything with
765
+ no portal target needed, and already blocks interaction with the screen
766
+ behind it, so there's no `useBodyScrollLock` port. Its `onRequestClose`
767
+ fires on the **Android hardware back button** — the direct analog of the
768
+ web version's document-level Escape listener (iOS has no back button, so
769
+ this is Android-only, matching the platform's own convention). Focus
770
+ trapping (`Tab`/`Shift+Tab` cycling between focusable elements) has no
771
+ port at all — there's no keyboard `Tab` concept in RN's touch-first model,
772
+ the same reasoning that already dropped `TabBar`'s roving-tabindex arrow
773
+ keys.
774
+
775
+ `role` is set via RN's newer, web-aligned `role` prop (not
776
+ `accessibilityRole`) — its `Role` union has real `"dialog"`/`"alertdialog"`
777
+ values, unlike the older `AccessibilityRole` enum `Toast`/`Banner` had to
778
+ substitute `"alert"` into for the web's `role="status"`.
779
+ `accessibilityViewIsModal` (iOS-only) is the closest match to
780
+ `aria-modal="true"`, restricting VoiceOver to the dialog's subtree. The
781
+ dialog card sets `accessible` explicitly (a bare `View` with `role` isn't
782
+ an accessibility element by default — the same `BoxedList` lesson) —
783
+ **this shares the same open, unverified-on-a-real-device accessibility
784
+ question already flagged for `BoxedList`/`TabBar`/`ViewSwitcher`'s
785
+ container-role pattern**: `accessible={true}` on a container may collapse
786
+ its subtree into one opaque VoiceOver stop, which for `Dialog` specifically
787
+ would mean its footer buttons become unreachable via VoiceOver even though
788
+ they're independently `Pressable`. Kept for `getByRole` testability and
789
+ consistency with the established pattern, but this is the component where
790
+ that tradeoff matters most — worth prioritizing for real-device screen
791
+ reader verification before it's treated as settled.
792
+
683
793
  ## Installation
684
794
 
685
795
  ```bash
@@ -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';