@gnome-ui/react-native 1.4.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 (43) hide show
  1. package/README.md +579 -2
  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/Chip/Chip.d.ts +63 -0
  9. package/dist/components/Chip/index.d.ts +2 -0
  10. package/dist/components/Clamp/Clamp.d.ts +61 -0
  11. package/dist/components/Clamp/index.d.ts +2 -0
  12. package/dist/components/CoachMark/CoachMark.d.ts +92 -0
  13. package/dist/components/CoachMark/CoachMarkTour.d.ts +54 -0
  14. package/dist/components/CoachMark/coachMarkUtils.d.ts +42 -0
  15. package/dist/components/CoachMark/index.d.ts +5 -0
  16. package/dist/components/Drawer/Drawer.d.ts +117 -0
  17. package/dist/components/Drawer/index.d.ts +2 -0
  18. package/dist/components/FileTypeIcon/FileTypeIcon.d.ts +45 -0
  19. package/dist/components/FileTypeIcon/fileType.d.ts +8 -0
  20. package/dist/components/FileTypeIcon/index.d.ts +3 -0
  21. package/dist/components/IconButton/IconButton.d.ts +34 -0
  22. package/dist/components/IconButton/index.d.ts +2 -0
  23. package/dist/components/InlineViewSwitcher/InlineViewSwitcher.d.ts +93 -0
  24. package/dist/components/InlineViewSwitcher/InlineViewSwitcherItem.d.ts +25 -0
  25. package/dist/components/InlineViewSwitcher/index.d.ts +4 -0
  26. package/dist/components/InlineViewSwitcher/variants.d.ts +30 -0
  27. package/dist/components/PreferencesGroup/PreferencesGroup.d.ts +52 -0
  28. package/dist/components/PreferencesGroup/index.d.ts +2 -0
  29. package/dist/components/SegmentedBar/SegmentedBar.d.ts +64 -0
  30. package/dist/components/SegmentedBar/index.d.ts +2 -0
  31. package/dist/components/StatusPage/StatusPage.d.ts +79 -0
  32. package/dist/components/StatusPage/index.d.ts +2 -0
  33. package/dist/components/ToggleGroup/ToggleGroup.d.ts +70 -0
  34. package/dist/components/ToggleGroup/ToggleGroupItem.d.ts +49 -0
  35. package/dist/components/ToggleGroup/index.d.ts +4 -0
  36. package/dist/components/WrapBox/WrapBox.d.ts +67 -0
  37. package/dist/components/WrapBox/index.d.ts +2 -0
  38. package/dist/index.cjs +1 -1
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.ts +15 -0
  41. package/dist/index.js +2147 -577
  42. package/dist/index.js.map +1 -1
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -17,10 +17,14 @@ 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
- > and `Overlay`/`LevelBar`/`Expander`/`Divider`/`Highlight` (Tier 20) also
23
+ > and `Overlay`/`LevelBar`/`Expander`/`Divider`/`Highlight`/`FileTypeIcon`/
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
24
28
  > shipped. Component ports from
25
29
  > `@gnome-ui/react` continue tier by tier — see this package's own
26
30
  > [ROADMAP.md](./ROADMAP.md) for full
@@ -1291,6 +1295,579 @@ dropped, not a behavior gap. `prefers-contrast: more`'s solid-background/
1291
1295
  white-text swap ports via `useResolvedContrast()`, the same hook `Button`
1292
1296
  already uses for its own high-contrast branching.
1293
1297
 
1298
+ ### FileTypeIcon
1299
+
1300
+ ```tsx
1301
+ import { FileTypeIcon } from '@gnome-ui/react-native';
1302
+
1303
+ <FileTypeIcon name="report.pdf" />
1304
+ <FileTypeIcon mimeType="image/png" />
1305
+ <FileTypeIcon name="cover.jpg" thumbnail={thumbnailUrl} />
1306
+ <FileTypeIcon isFolder />
1307
+ ```
1308
+
1309
+ Small icon — optionally a thumbnail — resolved from a file's MIME type or
1310
+ name extension. Useful for file-manager-style listings. Mirrors
1311
+ `@gnome-ui/react`'s `FileTypeIcon`, falling back to the generic file icon
1312
+ (freedesktop's `text-x-generic`) when the type can't be resolved.
1313
+
1314
+ `fileType.ts`'s category-resolution logic (MIME type / extension → one of
1315
+ 13 categories, plus the freedesktop icon and generated label per category)
1316
+ is pure, DOM-free TS — duplicated verbatim from `@gnome-ui/react` rather
1317
+ than imported cross-package, the same `Icon.tsx` precedent already
1318
+ established for logic that isn't worth a shared package for one file's
1319
+ worth of code. `role="img"` + `accessibilityLabel` ports 1:1, and the
1320
+ thumbnail reuses `Avatar`'s own `Image`/`resizeMode="cover"` recipe, sized
1321
+ from `Icon`'s own size map so swapping between the resolved icon and a
1322
+ thumbnail never shifts layout.
1323
+
1324
+ ### Chip
1325
+
1326
+ ```tsx
1327
+ import { Chip } from '@gnome-ui/react-native';
1328
+
1329
+ <Chip label="React" />
1330
+ <Chip label="React" onRemove={() => {}} />
1331
+ <Chip label="React" selectable selected={selected} onToggle={() => setSelected((s) => !s)} />
1332
+ ```
1333
+
1334
+ Compact pill-shaped label for tags, filters, and selection states. Mirrors
1335
+ `@gnome-ui/react`'s `Chip`. Three usage modes: **static** (just a visual
1336
+ label), **removable** (add `onRemove` for a × button), and **selectable**
1337
+ (add `selectable` + `selected` + `onToggle` for toggle behavior — same
1338
+ `isInteractive = selectable && !onRemove` precedence as the web version,
1339
+ so passing both renders the remove button, not a toggle). Pair with
1340
+ `WrapBox` for multi-chip layouts.
1341
+
1342
+ The selected background/border tint
1343
+ (`color-mix(in srgb, accent 15%/50%, transparent)`) resolves to a literal
1344
+ 8-digit `#RRGGBBAA` hex, the same `Highlight` precedent. The web version's
1345
+ `:hover`/`:active` background transitions collapse into a single
1346
+ pressed-state overlay tinted by `theme.activeOverlay` (the same
1347
+ `ActionRow`/`Card` recipe), since touch has no hover. The leading icon and
1348
+ remove (×) icon stay in the default foreground color rather than tracking
1349
+ the selected accent text (`color: inherit` on the web) — RN's `Icon` has
1350
+ no `currentColor` equivalent and only accepts a fixed named-swatch
1351
+ palette, none of which tracks the app's configurable accent color, so
1352
+ this is a decorative nicety dropped, not a behavior gap.
1353
+ `accessibilityRole="checkbox"` on the selectable form ports 1:1, the same
1354
+ `Checkbox` precedent.
1355
+
1356
+ ### SegmentedBar
1357
+
1358
+ ```tsx
1359
+ import { SegmentedBar } from '@gnome-ui/react-native';
1360
+
1361
+ <SegmentedBar
1362
+ values={[
1363
+ { label: 'TypeScript', value: 60, color: '#3178c6' },
1364
+ { label: 'JavaScript', value: 30, color: '#f7df1e' },
1365
+ { label: 'CSS', value: 10, color: '#563d7c' },
1366
+ ]}
1367
+ />
1368
+ ```
1369
+
1370
+ Horizontal bar split into proportional segments, one per category. Mirrors
1371
+ `@gnome-ui/react`'s `SegmentedBar`. Typical use case: repository language
1372
+ distribution. Values are normalized proportionally when they don't sum to
1373
+ 100.
1374
+
1375
+ The web version's hover interaction (dim every segment but the one under
1376
+ the pointer, brighten that one via `filter: brightness()`) is rebuilt for
1377
+ touch rather than dropped: each segment is a `Pressable`, and touching one
1378
+ dims the rest immediately via `onPressIn`/`onPressOut` — deliberately not
1379
+ gated behind `Tooltip`'s own long-press delay, since this feedback is the
1380
+ RN analog of a `Pressable`'s own instant `pressed` state, not the "peek"
1381
+ affordance a tooltip reveal is. Each segment is also wrapped in `Tooltip`
1382
+ (`placement="top"`, `delay={200}`, ported 1:1) for the label/percentage
1383
+ readout — `Tooltip` clones its own handlers onto the child while still
1384
+ calling the child's original ones, so the dim/highlight and the tooltip
1385
+ compose cleanly on the same `Pressable`. `filter: brightness(1.15)` on the
1386
+ actively-touched segment has no RN equivalent — dropped as a decorative
1387
+ nicety, since the touched segment already reads as highlighted by
1388
+ contrast once every other segment dims to 35% opacity.
1389
+
1390
+ ### IconButton
1391
+
1392
+ ```tsx
1393
+ import { IconButton } from '@gnome-ui/react-native';
1394
+ import { Search } from '@gnome-ui/icons';
1395
+
1396
+ <IconButton icon={Search} label="Search" />
1397
+ <IconButton icon={Search} label="Search" tooltip="Search files" />
1398
+ ```
1399
+
1400
+ Icon-only action button composed from `Button`, `Icon`, and optionally
1401
+ `Tooltip` — mirrors `@gnome-ui/react`'s `IconButton`, itself already just a
1402
+ thin composition of those same three pieces. `label` is required since the
1403
+ button has no visible text. Built as a genuine prerequisite for `Drawer`'s
1404
+ `rail`, not scope creep — every piece it composes already existed.
1405
+
1406
+ ### Drawer
1407
+
1408
+ ```tsx
1409
+ import { Drawer } from '@gnome-ui/react-native';
1410
+
1411
+ <Drawer open={open} title="Details" onClose={() => setOpen(false)}>
1412
+ <Text>Drawer content can be any React node passed as children.</Text>
1413
+ </Drawer>
1414
+ ```
1415
+
1416
+ Slide-in panel for supplementary content, anchored to the left or right
1417
+ edge. Mirrors `@gnome-ui/react`'s `Drawer`. Supports a `rail` (an
1418
+ `IconButton` strip on the drawer's inner edge for switching panels without
1419
+ closing it) and nested-drawer width auto-scaling via context — a `Drawer`
1420
+ opened from within another drawer's content automatically renders
1421
+ narrower (`0.85^depth`, floored at 240px), so stacked drawers read as a
1422
+ drill-in hierarchy.
1423
+
1424
+ Floats with a margin on every side and all four corners rounded, matching
1425
+ `@gnome-ui/react`'s own recent CSS update to the same look — positioned
1426
+ within the padded backdrop via `justifyContent` rather than the web CSS's
1427
+ `margin: auto` on the drawer itself, since RN auto-margin support was
1428
+ unverified for this Yoga version (confirmed correct with an on-device
1429
+ debug-color check before trusting it; `BottomSheet` already proves the
1430
+ same `justifyContent: 'flex-end'` mechanism on its own vertical axis).
1431
+ Unlike `BottomSheet`, there's no drag-to-dismiss — the web source defines
1432
+ no exit keyframes at all, so this follows `Dialog`'s simpler animation
1433
+ shape instead. `backdrop-filter: blur(4px)` has no port (no native blur
1434
+ dependency in this package).
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
+
1294
1871
  ## Installation
1295
1872
 
1296
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';