@gnome-ui/react-native 1.5.0 → 1.6.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.
Files changed (32) hide show
  1. package/README.md +441 -3
  2. package/dist/components/AvatarGroup/AvatarGroup.d.ts +47 -0
  3. package/dist/components/AvatarGroup/index.d.ts +2 -0
  4. package/dist/components/AvatarRotator/AvatarRotator.d.ts +65 -0
  5. package/dist/components/AvatarRotator/index.d.ts +2 -0
  6. package/dist/components/Box/Box.d.ts +87 -0
  7. package/dist/components/Box/index.d.ts +2 -0
  8. package/dist/components/Clamp/Clamp.d.ts +61 -0
  9. package/dist/components/Clamp/index.d.ts +2 -0
  10. package/dist/components/CoachMark/CoachMark.d.ts +92 -0
  11. package/dist/components/CoachMark/CoachMarkTour.d.ts +54 -0
  12. package/dist/components/CoachMark/coachMarkUtils.d.ts +42 -0
  13. package/dist/components/CoachMark/index.d.ts +5 -0
  14. package/dist/components/InlineViewSwitcher/InlineViewSwitcher.d.ts +93 -0
  15. package/dist/components/InlineViewSwitcher/InlineViewSwitcherItem.d.ts +25 -0
  16. package/dist/components/InlineViewSwitcher/index.d.ts +4 -0
  17. package/dist/components/InlineViewSwitcher/variants.d.ts +30 -0
  18. package/dist/components/PreferencesGroup/PreferencesGroup.d.ts +52 -0
  19. package/dist/components/PreferencesGroup/index.d.ts +2 -0
  20. package/dist/components/StatusPage/StatusPage.d.ts +79 -0
  21. package/dist/components/StatusPage/index.d.ts +2 -0
  22. package/dist/components/ToggleGroup/ToggleGroup.d.ts +70 -0
  23. package/dist/components/ToggleGroup/ToggleGroupItem.d.ts +49 -0
  24. package/dist/components/ToggleGroup/index.d.ts +4 -0
  25. package/dist/components/WrapBox/WrapBox.d.ts +67 -0
  26. package/dist/components/WrapBox/index.d.ts +2 -0
  27. package/dist/index.cjs +1 -1
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.ts +10 -0
  30. package/dist/index.js +1487 -393
  31. package/dist/index.js.map +1 -1
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -17,12 +17,15 @@ React Native component library following the [GNOME Human Interface Guidelines](
17
17
  > `PathBar`) fully ported. Tier 4 Feedback: `Spinner`, `ProgressBar`,
18
18
  > `Skeleton`, `Toast`/`Toaster`, `Banner`, `Dialog`, `Tooltip`, and
19
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
20
+ > own public component) and `StatusPage` shipped — Tier 4 complete. Tier 5
21
21
  > Advanced Controls fully ported: `Dropdown`, `Slider`, `SpinButton`,
22
22
  > `Avatar`, `Badge`, and `Popover`. Beyond Tier 5, `BottomSheet` (Tier 14)
23
23
  > and `Overlay`/`LevelBar`/`Expander`/`Divider`/`Highlight`/`FileTypeIcon`/
24
- > `SegmentedBar` (Tier 20), `Chip` (Tier 7), and `IconButton`/`Drawer`
25
- > (Tier 8/Tier 20) also shipped. Component ports from
24
+ > `SegmentedBar`/`AvatarGroup`/`AvatarRotator`/`CoachMark`/`CoachMarkTour`
25
+ > (Tier 20), `Chip` (Tier 7), `IconButton`/`Drawer` (Tier 8/Tier 20), and
26
+ > `Clamp` (Tier 6), `Box` (Tier 20), `WrapBox`/`ToggleGroup` (Tier 7), and
27
+ > `InlineViewSwitcher` (Tier 8), and `PreferencesGroup` (Tier 13) also
28
+ > shipped. Component ports from
26
29
  > `@gnome-ui/react` continue tier by tier — see this package's own
27
30
  > [ROADMAP.md](./ROADMAP.md) for full
28
31
  > per-tier status against all 130 `@gnome-ui/react` components, and the
@@ -1430,6 +1433,441 @@ no exit keyframes at all, so this follows `Dialog`'s simpler animation
1430
1433
  shape instead. `backdrop-filter: blur(4px)` has no port (no native blur
1431
1434
  dependency in this package).
1432
1435
 
1436
+ ### AvatarGroup
1437
+
1438
+ ```tsx
1439
+ import { AvatarGroup } from '@gnome-ui/react-native';
1440
+
1441
+ <AvatarGroup
1442
+ avatars={[{ name: 'Alice Martin' }, { name: 'Bob Smith' }, { name: 'Carol White' }]}
1443
+ max={5}
1444
+ />
1445
+ ```
1446
+
1447
+ Overlapping stack of `Avatar`s with a "+N" overflow indicator. Mirrors
1448
+ `@gnome-ui/react`'s `AvatarGroup`. The web version's separating ring
1449
+ around each overlapping avatar is two layered `box-shadow`s (an inset 1px
1450
+ dark/light border plus an outset 2px window-colored ring) — RN can only
1451
+ give a `View` one border, so this keeps just the outer window-colored
1452
+ ring (overriding `Avatar`'s own subtle 1px ring via its `style` prop),
1453
+ since that's the ring doing the actual "stay visually distinct from the
1454
+ avatar behind you" work. The overflow chip reuses `Avatar`'s own per-size
1455
+ box dimensions so it lines up exactly with the avatars beside it.
1456
+ `role="group"` + an auto-generated `accessibilityLabel` (joined names,
1457
+ plus "and N more" when overflowing) port 1:1 from RN's newer web-aligned
1458
+ `Role` union.
1459
+
1460
+ ### AvatarRotator
1461
+
1462
+ ```tsx
1463
+ import { AvatarRotator } from '@gnome-ui/react-native';
1464
+
1465
+ <AvatarRotator name="Alice Martin" avatars={[url1, url2, url3]} />
1466
+ ```
1467
+
1468
+ Single avatar surface that crossfades through multiple image sources.
1469
+ Mirrors `@gnome-ui/react`'s `AvatarRotator`. Keeps `Avatar` focused on
1470
+ rendering one identity, while this component owns timing, crossfade
1471
+ animation, and pause behavior.
1472
+
1473
+ Each source renders as its own absolutely-positioned `Avatar`, crossfaded
1474
+ with `Animated.timing` (`useNativeDriver: true`) — a `RotatorLayer`
1475
+ sub-component owns each layer's own `Animated.Value` rather than the
1476
+ parent tracking an array of them, the same "each item animates itself"
1477
+ shape `Toast`/`Toaster` already established for independently
1478
+ transitioning list items. `prefers-reduced-motion` stops the rotation
1479
+ outright, not just the fade — ported exactly from the web version's own
1480
+ auto-advance effect, which bails out early on both `isPaused` and reduced
1481
+ motion alike. `pauseOnHover` becomes `pauseOnPress`
1482
+ (`onPressIn`/`onPressOut`) — the same touch substitution `Toast`'s own
1483
+ press-and-hold pause already established, kept as a real toggleable prop
1484
+ here (defaults `true`).
1485
+
1486
+ ### CoachMark / CoachMarkTour
1487
+
1488
+ ```tsx
1489
+ import { CoachMark, CoachMarkTour } from '@gnome-ui/react-native';
1490
+
1491
+ <CoachMark
1492
+ open={open}
1493
+ targetRef={target}
1494
+ title="Sync your files"
1495
+ description="Press this to keep every device up to date."
1496
+ primaryAction={{ label: 'Got it', onPress: () => setOpen(false) }}
1497
+ onDismiss={() => setOpen(false)}
1498
+ />
1499
+
1500
+ <CoachMarkTour
1501
+ open={running}
1502
+ steps={[
1503
+ { targetRef: searchRef, title: 'Search', description: 'Find anything fast.' },
1504
+ { targetRef: addRef, title: 'Add', description: 'Create a new item here.', placement: 'left' },
1505
+ ]}
1506
+ onFinish={() => setRunning(false)}
1507
+ onSkip={() => setRunning(false)}
1508
+ />
1509
+ ```
1510
+
1511
+ Spotlights a target element and anchors a callout bubble (title,
1512
+ description, step counter, actions) beside it, guiding a user to one
1513
+ feature. Compose several with `CoachMarkTour`, or drive one directly with
1514
+ `open`. Mirrors `@gnome-ui/react`'s `CoachMark`/`CoachMarkTour` — not a
1515
+ GNOME HIG widget, a pragmatic feature-discovery pattern.
1516
+
1517
+ Positions with the same two-pass viewport-aware flip as the web version
1518
+ (`coachMarkUtils.ts`, duplicated verbatim — pure math, no DOM), resolved
1519
+ from `targetRef.current?.measureInWindow(...)` and the bubble's own
1520
+ `onLayout` size. **The measurement is deliberately delayed (a real
1521
+ `setTimeout`, not just one `requestAnimationFrame`)** — confirmed
1522
+ on-device that measuring too early catches a stale rect when the target
1523
+ sits below sibling content whose own size isn't final on the first commit
1524
+ (e.g. a multi-line description `Text` above it); a single rAF still
1525
+ landed before the follow-up layout pass accounted for it.
1526
+
1527
+ The spotlight cutout has no CSS `box-shadow: 0 0 0 100vmax` port — that
1528
+ trick paints an opaque scrim everywhere except inside a rounded rect via a
1529
+ huge spread shadow, which RN's real OS shadows can't reproduce. Rebuilt as
1530
+ four plain `View` bands around the padded target rect, plus a separate
1531
+ rounded `accentColor`-bordered ring on top — the whole overlay sits inside
1532
+ one full-screen `Pressable`, so a tap anywhere within it (including
1533
+ visually "in the hole") triggers `dismissOnBackdrop`, matching the web
1534
+ version exactly. `dismissOnBackdrop` has no effect when `spotlight` is
1535
+ `false` — ported faithfully, not fixed: the web source only renders a
1536
+ backdrop element at all when `spotlight` is true. The arrow reuses
1537
+ `Popover`/`Tooltip`'s transparent-border-triangle trick rather than the
1538
+ web CSS's rotated-45°-square, offset along the bubble's edge by
1539
+ `arrowOffset` from the position math (unlike `Tooltip`/`Popover`'s simpler
1540
+ always-centered arrow). No focus trap and no scroll/resize
1541
+ re-positioning, the same established gaps for a transient RN floating
1542
+ element. `CoachMarkTour` is pure state orchestration on top of
1543
+ `CoachMark`, ported verbatim.
1544
+
1545
+ ### Clamp
1546
+
1547
+ ```tsx
1548
+ import { Clamp } from '@gnome-ui/react-native';
1549
+
1550
+ <Clamp>
1551
+ <BoxedList>{/* … */}</BoxedList>
1552
+ </Clamp>
1553
+
1554
+ <Clamp maximumSize={480} tighteningThreshold={0.9}>
1555
+ <Text>Never wider than 480 dp, and never edge-to-edge below it</Text>
1556
+ </Clamp>
1557
+ ```
1558
+
1559
+ Constrains its children to a maximum width while letting them shrink freely
1560
+ — mirrors `@gnome-ui/react`'s `Clamp` and the Adwaita `AdwClamp` widget. Use
1561
+ it on settings pages and forms so content never becomes too wide to read
1562
+ comfortably on a tablet or a landscape phone, while still filling the width
1563
+ on a narrow one. `maximumSize` defaults to **600** (the Adwaita recommended
1564
+ narrow-content width) and is in density-independent pixels, not CSS px.
1565
+ Adds no padding of its own.
1566
+
1567
+ The web version's `margin-inline: auto` centering becomes
1568
+ `alignSelf: 'center'` rather than `marginHorizontal: 'auto'` — RN
1569
+ auto-margin support was left unverified for this Yoga version back when
1570
+ `Drawer` needed the same trick, so this follows `Drawer`'s resolution of
1571
+ using flex alignment instead. The one consequence is that `Clamp` expects a
1572
+ column-direction parent (RN's default): `alignSelf` acts on the cross axis,
1573
+ so inside a `flexDirection: 'row'` parent it would centre vertically. Wrap
1574
+ it in a plain `View` there.
1575
+
1576
+ `tighteningThreshold` is a real percentage width here, unlike in
1577
+ `@gnome-ui/react`, where the prop is declared and documented but never
1578
+ reaches the DOM — implementing it exactly as that package documents it (a
1579
+ fraction of the available width, still capped by `maximumSize`) costs
1580
+ nothing on RN and avoids shipping a dead prop.
1581
+
1582
+ ### Box
1583
+
1584
+ ```tsx
1585
+ import { Box } from '@gnome-ui/react-native';
1586
+
1587
+ // Vertical section (heading + content)
1588
+ <Box spacing={12}>
1589
+ <Text variant="caption-heading" color="dim">Devices</Text>
1590
+ <BoxedList>{/* … */}</BoxedList>
1591
+ </Box>
1592
+
1593
+ // Horizontal icon + label
1594
+ <Box orientation="horizontal" spacing={6} align="center">
1595
+ <Icon icon={Folder} size="sm" />
1596
+ <Text>Documents</Text>
1597
+ </Box>
1598
+ ```
1599
+
1600
+ Fundamental flex layout primitive — the RN equivalent of `GtkBox`, and a 1:1
1601
+ mirror of `@gnome-ui/react`'s own `Box`. Arranges children in a row or
1602
+ column with consistent spacing from the GNOME HIG scale: **3** (tight) ·
1603
+ **6** (standard, the default) · **12** (medium) · **18** (large) · **24**
1604
+ (section) · **32** (loose) · **48** (jumbo), all in dp. `align` defaults to
1605
+ `"stretch"` when vertical and `"center"` when horizontal, `justify` to
1606
+ `"start"`.
1607
+
1608
+ `BoxSpacing` keeps the web package's exact seven values rather than being
1609
+ remapped onto this package's own `theme.space1`–`space6` scale — the two
1610
+ overlap at 6/12/18/24/48 but not at 3 or 32/36, and `BoxSpacing` is a
1611
+ published type consumers may already be importing, so it ports verbatim.
1612
+
1613
+ Two things the web version accepts don't survive the platform. `spacing`
1614
+ and `padding` are numbers only — RN's `gap`/`padding` take dp, not CSS
1615
+ strings like `"1rem"`. And `align`/`justify`, which the web hands straight
1616
+ to CSS, are mapped internally from their bare `start`/`end` keywords onto
1617
+ Yoga's `flex-start`/`flex-end`; the prop values stay the web ones, so the
1618
+ API reads identically across both packages. `display: 'flex'` needs no port
1619
+ at all — every RN `View` is already a flex container.
1620
+
1621
+ ### WrapBox
1622
+
1623
+ ```tsx
1624
+ import { WrapBox } from '@gnome-ui/react-native';
1625
+
1626
+ // Tag list
1627
+ <WrapBox>
1628
+ {tags.map((tag) => <Chip key={tag} label={tag} />)}
1629
+ </WrapBox>
1630
+
1631
+ // Tight between items, loose between lines
1632
+ <WrapBox childSpacing={6} lineSpacing={18} justify="center">
1633
+ {filters.map((filter) => <Chip key={filter} label={filter} />)}
1634
+ </WrapBox>
1635
+ ```
1636
+
1637
+ Flexible wrapping layout container — children flow horizontally and wrap to
1638
+ new lines when they don't fit, like words in a paragraph, without locking
1639
+ them into a grid. Mirrors `AdwWrapBox` (libadwaita 1.7 / GNOME 48) and
1640
+ `@gnome-ui/react`'s own `WrapBox`. Pair with `Chip` for tag lists and filter
1641
+ rows.
1642
+
1643
+ `childSpacing` (default **6**) is the gap between items on a line;
1644
+ `lineSpacing` is the gap between lines and falls back to `childSpacing` when
1645
+ omitted — passing `0` really means zero, not "fall back". `justify` defaults
1646
+ to `"start"` and `align` to `"center"`; `wrapReverse` stacks lines bottom to
1647
+ top.
1648
+
1649
+ The web version ships its values as CSS custom properties consumed by a
1650
+ stylesheet (`--wrapbox-gap`, `--wrapbox-justify`, …) because a CSS module
1651
+ can't take runtime values any other way — RN has no such indirection, so
1652
+ they're written straight onto the style object. `flex-flow: row wrap`
1653
+ becomes `flexDirection: 'row'` + `flexWrap`, and the CSS shorthand
1654
+ `gap: <row> <column>` splits into RN's separate `rowGap`/`columnGap`; the
1655
+ single `gap` property would set both, which is precisely what this component
1656
+ has to be able to avoid. As in `Box`, the spacing props are numbers only
1657
+ (dp, not CSS strings) and `align`/`justify` keep the web's bare `start`/`end`
1658
+ keywords while mapping internally onto Yoga's `flex-start`/`flex-end`.
1659
+
1660
+ `alignContent: 'stretch'` is set explicitly even though neither package
1661
+ exposes an `alignContent` prop: **CSS defaults it to `stretch`, Yoga defaults
1662
+ it to `flex-start`**, so without it `align="stretch"` silently does nothing
1663
+ whenever the children have no cross-size of their own — the line collapses to
1664
+ zero height before `alignItems` gets to stretch anything into it. Caught
1665
+ on-device; it's a no-op in the ordinary case where the container hugs its
1666
+ content rather than having a fixed height.
1667
+
1668
+ ### StatusPage
1669
+
1670
+ ```tsx
1671
+ import { StatusPage } from '@gnome-ui/react-native';
1672
+
1673
+ <StatusPage
1674
+ icon={StarOutline}
1675
+ title="No favorites yet"
1676
+ description="Packages you star will show up here."
1677
+ >
1678
+ <Button variant="suggested" onPress={onAdd}>Add a package</Button>
1679
+ </StatusPage>
1680
+
1681
+ // For sidebars, popovers, and small panels
1682
+ <StatusPage compact icon={Search} title="No results" />
1683
+ ```
1684
+
1685
+ Empty-state / status page following the Adwaita `AdwStatusPage` pattern —
1686
+ mirrors `@gnome-ui/react`'s `StatusPage`. Use it to fill a view with no
1687
+ content yet, an error state, or a completion confirmation. Always explain
1688
+ *why* the view is empty and *what the user can do* about it; don't use it
1689
+ for loading states, where `Spinner` or `ProgressBar` belong instead.
1690
+ `compact` scales padding, icon size, title variant, description
1691
+ variant/measure and both action-area gaps down together.
1692
+
1693
+ It centres its content on both axes, but — exactly as in the web version —
1694
+ the vertical centring only does anything once a parent gives it height: put
1695
+ it in a `flex: 1` container to fill the view.
1696
+
1697
+ The title renders through this package's `Text` at `variant="title-1"`
1698
+ (`"title-4"` when `compact`), so it also picks up `Text`'s automatic
1699
+ `header` accessibility role — a deliberate divergence from the web
1700
+ version's `<p class="title">`. That `<p>` exists because HTML forces you to
1701
+ pick a concrete `h1`–`h6` level for a component that can't know where it
1702
+ sits in the document outline; RN's `header` role carries no level, so the
1703
+ dilemma disappears. On a touch device the rotor is the only structural
1704
+ navigation a screen reader user has, which makes the role worth having.
1705
+
1706
+ `max-width: 36ch` on the description has no RN unit to port to. `ch` is the
1707
+ advance width of "0", ≈ 0.5em in the sans faces Adwaita uses, so the cap is
1708
+ resolved against the description's own font size — 288 dp at body size, 216
1709
+ dp at caption size — keeping the measure font-relative the way the CSS is,
1710
+ rather than freezing one pixel value that `compact` would get wrong. The
1711
+ icon is dimmed by its wrapper's opacity (0.55 light / 0.45 dark, the two
1712
+ values the web's own `prefers-color-scheme` block hardcodes) and hidden
1713
+ from assistive tech with the `accessibilityElementsHidden` +
1714
+ `importantForAccessibility="no"` pair used in place of `aria-hidden`. The
1715
+ action area is a `WrapBox` rather than a hand-rolled row — `.actions` is a
1716
+ centred wrapping flex row with a gap and nothing else.
1717
+
1718
+ ### ToggleGroup / ToggleGroupItem
1719
+
1720
+ ```tsx
1721
+ import { ToggleGroup, ToggleGroupItem } from '@gnome-ui/react-native';
1722
+
1723
+ const [align, setAlign] = useState('left');
1724
+
1725
+ <ToggleGroup value={align} onValueChange={setAlign} accessibilityLabel="Alignment">
1726
+ <ToggleGroupItem name="left" icon={FormatJustifyLeft} accessibilityLabel="Left" />
1727
+ <ToggleGroupItem name="center" icon={FormatJustifyCenter} accessibilityLabel="Center" />
1728
+ <ToggleGroupItem name="right" icon={FormatJustifyRight} accessibilityLabel="Right" />
1729
+ </ToggleGroup>
1730
+
1731
+ // Items can be icon-only, label-only, or icon + label
1732
+ <ToggleGroupItem name="grid" icon={Applications} label="Grid" />
1733
+ ```
1734
+
1735
+ Mutually-exclusive group of toggle buttons for in-place option selection —
1736
+ mirrors `AdwToggleGroup` (libadwaita 1.7 / GNOME 48) and
1737
+ `@gnome-ui/react`'s own `ToggleGroup`. Use it for formatting controls,
1738
+ view-mode selectors and toolbar options, wherever a `ViewSwitcher` would be
1739
+ too heavy or doesn't belong in a `HeaderBar`. For icon-only items always
1740
+ pass an `accessibilityLabel`.
1741
+
1742
+ The context and its `value`/`onValueChange` shape port 1:1 — pure React. The
1743
+ keyboard layer doesn't: the web version owns an `onKeyDown` implementing
1744
+ ← / → cycling and Home / End jumps over a roving `tabIndex`, none of which
1745
+ has a touch counterpart, so it drops per this package's standing convention
1746
+ (set by `ViewSwitcher` and `TabBar`). The `radiogroup`/`radio` + `checked`
1747
+ pairing that VoiceOver and TalkBack actually announce carries the semantics
1748
+ instead.
1749
+
1750
+ The group sets `accessibilityRole="radiogroup"` but deliberately **not**
1751
+ `accessible` — on iOS, `accessible` on a container collapses the whole
1752
+ subtree into a single accessibility element, which would make the individual
1753
+ toggles unreachable for VoiceOver. Without it the role still groups on
1754
+ Android while every item stays focusable on its own.
1755
+
1756
+ Three `color-mix(in srgb, accent N%, transparent)` values resolve to 8-digit
1757
+ `#RRGGBBAA` hexes off `theme.accentBgColor` (the `Chip` precedent for the
1758
+ same selected-tint problem), so the tint follows the app's configurable
1759
+ accent color. The CSS paints its active ring as an `inset` box-shadow, which
1760
+ RN has no equivalent for — it becomes a real `borderWidth: 1` that every
1761
+ item carries at all times (transparent when inactive) so selecting one never
1762
+ shifts the row's layout, the substitution `AvatarGroup` already made for its
1763
+ own ring. `box-shadow: var(--gnome-shadow-sm)` on the group is dropped
1764
+ rather than approximated: the theme generator keeps shadow tokens in `raw`
1765
+ only, and `Card` already established that a border carries the same
1766
+ separation here. `:hover` collapses away and `:active` maps to `Pressable`'s
1767
+ `pressed` using `theme.activeOverlay`, whose light/dark values match the
1768
+ CSS's own `:active` colors exactly.
1769
+
1770
+ The icon keeps the default foreground color instead of tracking the active
1771
+ accent text — `Icon` has no `currentColor` equivalent and its `color` prop
1772
+ is a fixed GNOME palette with no `accent` member, which couldn't follow a
1773
+ configurable accent anyway. Same call, same reason, as `Chip`.
1774
+
1775
+ ### InlineViewSwitcher / InlineViewSwitcherItem
1776
+
1777
+ ```tsx
1778
+ import { InlineViewSwitcher, InlineViewSwitcherItem } from '@gnome-ui/react-native';
1779
+
1780
+ const [view, setView] = useState('grid');
1781
+
1782
+ <InlineViewSwitcher value={view} onValueChange={setView} variant="pill">
1783
+ <InlineViewSwitcherItem name="grid" label="Grid" icon={Applications} />
1784
+ <InlineViewSwitcherItem name="list" label="List" icon={ViewSidebar} />
1785
+ </InlineViewSwitcher>
1786
+
1787
+ // Collapse to a BottomSheet picker when the items stop fitting
1788
+ <InlineViewSwitcher value={view} onValueChange={setView} overflow="menu">
1789
+ {/* … */}
1790
+ </InlineViewSwitcher>
1791
+ ```
1792
+
1793
+ Compact inline view switcher for content areas, cards and toolbars —
1794
+ wherever `ViewSwitcher` (header-bar sized) would be too heavy. Mirrors
1795
+ `AdwInlineViewSwitcher` (libadwaita 1.7 / GNOME 48) and `@gnome-ui/react`'s
1796
+ own `InlineViewSwitcher`. Four variants — `default` (card surface + border),
1797
+ `flat` (indicator only), `round` (pill container, solid accent indicator),
1798
+ `pill` (segmented-control look, no accent) — and four overflow strategies:
1799
+ `wrap`, `scroll`, `compact`, `menu`.
1800
+
1801
+ Almost none of the *mechanism* ports, so this is a rebuild rather than a
1802
+ transliteration:
1803
+
1804
+ - **The sliding indicator** is measured, not laid out. The web reads the
1805
+ active button's `offsetLeft`/`offsetWidth`; here each item reports its own
1806
+ `onLayout` up through the context and the indicator animates `translateX` +
1807
+ `width` on **one JS-driven animation** (`useNativeDriver: false`). `width`
1808
+ can't be native-driven and mixing a native with a JS value on one component
1809
+ throws — the trade-off `Expander` already accepted for its animated height.
1810
+ `scaleX` would have been native-driveable but distorts the corner radii the
1811
+ variants are defined by. `useReducedMotion()` snaps it into place instead.
1812
+ - **Overflow detection** replaces `ResizeObserver` + `scrollWidth` vs
1813
+ `clientWidth` with the item measurements already being collected: their
1814
+ summed natural widths (RN leaves `flexShrink` at 0, so an overflowing row
1815
+ still reports each item at full width) against the row's own `onLayout`.
1816
+ The web's `naturalWidthRef` capture and 30 px hysteresis port verbatim —
1817
+ without them, collapsing the labels shrinks the content and immediately
1818
+ re-expands it.
1819
+ - **`overflow="scroll"`** becomes a horizontal `ScrollView` with the
1820
+ scrollbar hidden; `scroll-snap-align: start` has no RN style, but the
1821
+ measured item offsets feed `snapToOffsets`, which reproduces it exactly.
1822
+ - **`overflow="menu"`** reuses the already-shipped `BottomSheet`.
1823
+
1824
+ The ←/→/Home/End keyboard layer drops as everywhere else here, and — as in
1825
+ `ToggleGroup` — the group takes `accessibilityRole="radiogroup"` but
1826
+ deliberately not `accessible`, which on iOS would collapse the items into one
1827
+ unreachable element.
1828
+
1829
+ One divergence is a fix, not a port: the web applies its `.active` class to
1830
+ the menu trigger even though menu mode hides the indicator, which paints
1831
+ `round`'s trigger label in `accent-fg` (#fff) on a plain card — white on
1832
+ white. The RN trigger uses the idle color.
1833
+
1834
+ ### PreferencesGroup
1835
+
1836
+ ```tsx
1837
+ import { PreferencesGroup } from '@gnome-ui/react-native';
1838
+
1839
+ <PreferencesGroup
1840
+ title="Appearance"
1841
+ description="How the app looks on this device."
1842
+ headerSuffix={<Button variant="flat" onPress={reset}>Reset</Button>}
1843
+ >
1844
+ <BoxedList>{rows}</BoxedList>
1845
+ </PreferencesGroup>
1846
+ ```
1847
+
1848
+ Titled section that wraps a `BoxedList` with an optional description —
1849
+ mirrors `AdwPreferencesGroup` and `@gnome-ui/react`'s own
1850
+ `PreferencesGroup`. Use it to group related settings under a named heading.
1851
+ It's purely a layout and labelling wrapper: it doesn't render the
1852
+ `BoxedList` itself, you pass one as `children`. All three header parts are
1853
+ optional; with none of them the header row is omitted entirely.
1854
+
1855
+ The web's empty `.content` wrapper looks like dead markup but is
1856
+ load-bearing, so it's kept. The group is a 12 dp-gap flex column — without
1857
+ that wrapper every child would become a flex item of the group and pick up a
1858
+ 12 dp gap between the rows themselves, instead of one gap between the header
1859
+ and the content as a whole.
1860
+
1861
+ The title renders as `Text variant="body"` with an explicit semibold weight
1862
+ rather than `variant="heading"`, which is body-sized but **bold** and on the
1863
+ tighter heading line-height; the CSS `.title` is specifically semibold at the
1864
+ body line-height. It keeps the `header` accessibility role anyway (passed
1865
+ explicitly), since a settings-group heading is exactly the kind of landmark a
1866
+ screen reader rotor should list — the same call `StatusPage` makes for its
1867
+ own title. `min-width: 0` on the header text has no port and needs none: it's
1868
+ the classic CSS flexbox override for a min-content floor Yoga doesn't apply
1869
+ in the first place.
1870
+
1433
1871
  ## Installation
1434
1872
 
1435
1873
  ```bash
@@ -0,0 +1,47 @@
1
+ import { StyleProp, ViewStyle } from 'react-native';
2
+ import { AvatarColor, AvatarSize } from '../Avatar';
3
+ export interface AvatarGroupItem {
4
+ name?: string;
5
+ src?: string;
6
+ alt?: string;
7
+ color?: AvatarColor;
8
+ }
9
+ export interface AvatarGroupProps {
10
+ avatars: AvatarGroupItem[];
11
+ /** Max visible avatars before showing the overflow chip. Defaults to `5`. */
12
+ max?: number;
13
+ /** Size applied to all avatars and the overflow chip. Defaults to `"md"`. */
14
+ size?: AvatarSize;
15
+ /** Accessible label for the group. Auto-generated from names when omitted. */
16
+ accessibilityLabel?: string;
17
+ style?: StyleProp<ViewStyle>;
18
+ testID?: string;
19
+ }
20
+ /**
21
+ * Overlapping stack of `Avatar`s with a "+N" overflow indicator. Mirrors
22
+ * `@gnome-ui/react`'s `AvatarGroup`.
23
+ *
24
+ * The web version's separating ring around each overlapping avatar is two
25
+ * layered `box-shadow`s (an inset 1px dark/light border plus an outset 2px
26
+ * window-colored ring) — RN can only give a `View` one border, so this
27
+ * keeps just the outer window-colored ring (`borderWidth: 2,
28
+ * borderColor: theme.windowBgColor`, overriding `Avatar`'s own subtle 1px
29
+ * ring via its `style` prop), since that's the ring doing the actual
30
+ * "stay visually distinct from the avatar behind you" work — the inner
31
+ * hairline is a decorative nicety, not a behavior gap. The overflow chip
32
+ * reuses `Avatar`'s own per-size box dimensions (`sm`/`md`/`lg`/`xl` →
33
+ * 24/32/48/64) so it lines up exactly with the avatars beside it.
34
+ *
35
+ * `role="group"` + `accessibilityLabel` ports 1:1 from RN's newer
36
+ * web-aligned `Role` union (the same `Avatar`/`Badge`/`LevelBar`
37
+ * precedent) — the label is auto-generated from `avatars[].name` (joined,
38
+ * plus "and N more" when overflowing) exactly like the web version, unless
39
+ * overridden. The overflow chip's `+N` text is hidden from the
40
+ * accessibility tree (`accessibilityElementsHidden`/
41
+ * `importantForAccessibility="no"`) in favor of the chip's own
42
+ * `accessibilityLabel="+N more"` — the same one-stop-not-two reasoning
43
+ * `SpinButton`/`Avatar` already established for a decorative inner glyph.
44
+ *
45
+ * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Avatar.html
46
+ */
47
+ export declare const AvatarGroup: ({ avatars, max, size, accessibilityLabel, style, testID, }: AvatarGroupProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { AvatarGroupItem, AvatarGroupProps } from './AvatarGroup';
2
+ export { AvatarGroup } from './AvatarGroup';
@@ -0,0 +1,65 @@
1
+ import { StyleProp, ViewStyle } from 'react-native';
2
+ import { AvatarColor, AvatarSize } from '../Avatar';
3
+ export interface AvatarRotatorProps {
4
+ /** Full name used for the accessible label and initials fallback. */
5
+ name?: string;
6
+ /** Image URLs to rotate through. */
7
+ avatars?: string[];
8
+ /** Accessible label. Defaults to `name`. */
9
+ alt?: string;
10
+ /** Size of the avatar. Defaults to `"md"`. */
11
+ size?: AvatarSize;
12
+ /** Fallback initials color when no avatar image is available. */
13
+ color?: AvatarColor;
14
+ /** Time between avatar changes in milliseconds. Defaults to `3000`. */
15
+ interval?: number;
16
+ /** Crossfade duration in milliseconds. Defaults to `240`. */
17
+ transitionDuration?: number;
18
+ /** Pause automatic rotation while pressed and held. Defaults to `true`. */
19
+ pauseOnPress?: boolean;
20
+ /**
21
+ * Controlled active avatar index.
22
+ * When omitted the rotator manages index state internally.
23
+ */
24
+ activeIndex?: number;
25
+ /** Initial active avatar index for uncontrolled usage. Defaults to `0`. */
26
+ defaultActiveIndex?: number;
27
+ /** Called when the active avatar changes. */
28
+ onIndexChange?: (index: number) => void;
29
+ style?: StyleProp<ViewStyle>;
30
+ testID?: string;
31
+ }
32
+ /**
33
+ * Single avatar surface that crossfades through multiple image sources.
34
+ * Mirrors `@gnome-ui/react`'s `AvatarRotator`. Keeps `Avatar` focused on
35
+ * rendering one identity, while this component owns timing, crossfade
36
+ * animation, and pause behavior.
37
+ *
38
+ * Each source renders as its own absolutely-positioned `Avatar`, layered
39
+ * via `StyleSheet.absoluteFill` and crossfaded with `Animated.timing` on
40
+ * `useNativeDriver: true` — an exact reproduction of the web version's
41
+ * stacked-`.layer`-elements-with-opacity-transition technique, just with
42
+ * `RotatorLayer` (see above) owning each layer's own `Animated.Value`
43
+ * instead of a single shared CSS custom property driving them all.
44
+ *
45
+ * **`prefers-reduced-motion` stops the rotation outright, not just the
46
+ * fade** — ported exactly: the web source's own auto-advance `useEffect`
47
+ * bails out early when reduced motion is on, the same as when paused, so
48
+ * this isn't merely an instant-swap-instead-of-crossfade case like
49
+ * `ProgressBar`'s determinate transitions.
50
+ *
51
+ * The web version's `pauseOnHover` (mouseEnter/mouseLeave, focus/blur)
52
+ * becomes `pauseOnPress` (`onPressIn`/`onPressOut`) — the same touch
53
+ * substitution `Toast`'s own press-and-hold pause already established,
54
+ * kept as a real toggleable prop here (unlike `Toast`, where the web
55
+ * source bakes the behavior in without an escape hatch).
56
+ * `usePrefersReducedMotion` (the web version's `@gnome-ui/hooks` import)
57
+ * has no bearing here — this package's own `useReducedMotion()` from
58
+ * `GnomeProvider` is the correct, already-established source for this.
59
+ *
60
+ * `role="img"` + `accessibilityLabel` ports 1:1 from RN's newer
61
+ * web-aligned `Role` union (the same `Avatar`/`AvatarGroup` precedent).
62
+ *
63
+ * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Avatar.html
64
+ */
65
+ export declare const AvatarRotator: ({ name, avatars, alt, size, color, interval, transitionDuration, pauseOnPress, activeIndex, defaultActiveIndex, onIndexChange, style, testID, }: AvatarRotatorProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export type { AvatarRotatorProps } from './AvatarRotator';
2
+ export { AvatarRotator } from './AvatarRotator';
@@ -0,0 +1,87 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleProp, View, ViewProps, ViewStyle } from 'react-native';
3
+ /** GNOME HIG standard spacing values (matches `GtkBox` spacing tokens). */
4
+ export type BoxSpacing = 3 | 6 | 12 | 18 | 24 | 32 | 48;
5
+ /** Alias of `BoxSpacing` for use as a padding scale. */
6
+ export type BoxPadding = BoxSpacing;
7
+ export type BoxOrientation = 'horizontal' | 'vertical';
8
+ export type BoxAlign = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
9
+ export type BoxJustify = 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly';
10
+ export interface BoxProps extends Omit<ViewProps, 'style'> {
11
+ /**
12
+ * Direction children are arranged.
13
+ * `"vertical"` → `flexDirection: 'column'` (default).
14
+ * `"horizontal"` → `flexDirection: 'row'`.
15
+ */
16
+ orientation?: BoxOrientation;
17
+ /**
18
+ * Gap between children, in density-independent pixels.
19
+ * Accepts any of the GNOME HIG standard spacing values or any number.
20
+ * Defaults to `6` (the HIG "standard" inner spacing).
21
+ */
22
+ spacing?: BoxSpacing | number;
23
+ /**
24
+ * Cross-axis alignment (`alignItems`).
25
+ * Defaults to `"stretch"` for vertical, `"center"` for horizontal.
26
+ */
27
+ align?: BoxAlign;
28
+ /**
29
+ * Main-axis distribution (`justifyContent`).
30
+ * Defaults to `"start"`.
31
+ */
32
+ justify?: BoxJustify;
33
+ /** Inner padding applied to all sides, in density-independent pixels. */
34
+ padding?: BoxPadding | number;
35
+ children?: ReactNode;
36
+ style?: StyleProp<ViewStyle>;
37
+ }
38
+ /**
39
+ * Fundamental flex layout primitive — the RN equivalent of `GtkBox`, and a
40
+ * 1:1 mirror of `@gnome-ui/react`'s own `Box`.
41
+ *
42
+ * Arranges children in a single row or column with consistent spacing
43
+ * following the GNOME Human Interface Guidelines spacing scale:
44
+ *
45
+ * | Token | dp | Use |
46
+ * |-------|----|-----|
47
+ * | tight | 3 | Dense UI, icon + label pairs |
48
+ * | standard | 6 | Default inner spacing |
49
+ * | medium | 12 | Between related groups |
50
+ * | large | 18 | Between loosely related sections |
51
+ * | section | 24 | Page-level section gaps |
52
+ * | loose | 32 | Large content separation |
53
+ * | jumbo | 48 | Hero / splash spacing |
54
+ *
55
+ * `BoxSpacing` keeps the web package's exact seven values rather than being
56
+ * remapped onto this package's own `theme.space1`–`space6` scale — the two
57
+ * overlap at 6/12/18/24/48 but not at 3 or 32/36, and `BoxSpacing` is a
58
+ * published type consumers may already be importing, so it ports verbatim.
59
+ *
60
+ * Two things the web version accepts don't survive the platform: `spacing`
61
+ * and `padding` are numbers only (RN's `gap`/`padding` take dp, not CSS
62
+ * strings like `"1rem"`), and `align`/`justify` — which the web passes
63
+ * straight through to CSS — are mapped from their bare `start`/`end`
64
+ * keywords onto Yoga's `flex-start`/`flex-end`. The prop values stay the
65
+ * web ones so the API reads identically across both packages; only the
66
+ * internal translation differs.
67
+ *
68
+ * `display: 'flex'` has no port and needs none — every RN `View` is already
69
+ * a flex container.
70
+ *
71
+ * @example
72
+ * // Vertical section (heading + content)
73
+ * <Box spacing={12}>
74
+ * <Text variant="caption-heading" color="dim">Devices</Text>
75
+ * <BoxedList>…</BoxedList>
76
+ * </Box>
77
+ *
78
+ * @example
79
+ * // Horizontal icon + label
80
+ * <Box orientation="horizontal" spacing={6} align="center">
81
+ * <Icon icon={Folder} />
82
+ * <Text>Documents</Text>
83
+ * </Box>
84
+ *
85
+ * @see https://developer.gnome.org/hig/guidelines/spacing.html
86
+ */
87
+ export declare const Box: import('react').ForwardRefExoticComponent<BoxProps & import('react').RefAttributes<View>>;
@@ -0,0 +1,2 @@
1
+ export type { BoxAlign, BoxJustify, BoxOrientation, BoxPadding, BoxProps, BoxSpacing, } from './Box';
2
+ export { Box } from './Box';