@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 +252 -4
- package/dist/components/AnimatedIcon/AnimatedIcon.d.ts +38 -0
- package/dist/components/AnimatedIcon/index.d.ts +2 -0
- package/dist/components/Banner/Banner.d.ts +50 -0
- package/dist/components/Banner/index.d.ts +2 -0
- package/dist/components/Dialog/Dialog.d.ts +105 -0
- package/dist/components/Dialog/index.d.ts +2 -0
- package/dist/components/Dropdown/Dropdown.d.ts +56 -0
- package/dist/components/Dropdown/index.d.ts +2 -0
- package/dist/components/Icon/Icon.d.ts +56 -0
- package/dist/components/Icon/index.d.ts +2 -0
- package/dist/components/Icon/shared.d.ts +19 -0
- package/dist/components/Tooltip/Tooltip.d.ts +88 -0
- package/dist/components/Tooltip/index.d.ts +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1328 -417
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
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
|
|
18
|
-
> `
|
|
19
|
-
> `
|
|
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,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,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,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,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,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
|
+
};
|