react-native-gtkx 0.2.0-alpha.1 → 0.2.0-alpha.2

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 (144) hide show
  1. package/README.md +11 -2
  2. package/dist/adw/widgets.generated.d.ts +4 -4
  3. package/dist/adw/widgets.generated.js +51 -51
  4. package/dist/adw/widgets.generated.js.map +1 -1
  5. package/dist/animated/create-animated.d.ts +2 -0
  6. package/dist/animated/create-animated.js +2 -0
  7. package/dist/animated/create-animated.js.map +1 -1
  8. package/dist/animated/index.d.ts +2 -0
  9. package/dist/animated/index.js +1 -0
  10. package/dist/animated/index.js.map +1 -1
  11. package/dist/animated/value-xy.d.ts +41 -0
  12. package/dist/animated/value-xy.js +90 -0
  13. package/dist/animated/value-xy.js.map +1 -0
  14. package/dist/animated/value.d.ts +9 -0
  15. package/dist/animated/value.js +30 -2
  16. package/dist/animated/value.js.map +1 -1
  17. package/dist/common/icon.d.ts +23 -0
  18. package/dist/common/icon.js +14 -0
  19. package/dist/common/icon.js.map +1 -0
  20. package/dist/common/index.d.ts +2 -0
  21. package/dist/common/index.js +2 -0
  22. package/dist/common/index.js.map +1 -1
  23. package/dist/common/list.d.ts +82 -0
  24. package/dist/common/list.js +166 -0
  25. package/dist/common/list.js.map +1 -0
  26. package/dist/common/widget.d.ts +1 -22
  27. package/dist/common/widget.js +75 -8
  28. package/dist/common/widget.js.map +1 -1
  29. package/dist/components/animated.d.ts +4 -2
  30. package/dist/components/animated.js +3 -1
  31. package/dist/components/animated.js.map +1 -1
  32. package/dist/components/app-registry.d.ts +26 -0
  33. package/dist/components/app-registry.js.map +1 -1
  34. package/dist/components/host-node.d.ts +7 -0
  35. package/dist/components/host-node.js +26 -1
  36. package/dist/components/host-node.js.map +1 -1
  37. package/dist/components/index.d.ts +6 -2
  38. package/dist/components/index.js +2 -1
  39. package/dist/components/index.js.map +1 -1
  40. package/dist/components/measure.d.ts +13 -0
  41. package/dist/components/measure.js +51 -0
  42. package/dist/components/measure.js.map +1 -0
  43. package/dist/components/modal.d.ts +1 -1
  44. package/dist/components/press-event.d.ts +28 -0
  45. package/dist/components/press-event.js +46 -0
  46. package/dist/components/press-event.js.map +1 -0
  47. package/dist/components/pressable.d.ts +2 -6
  48. package/dist/components/pressable.js +14 -8
  49. package/dist/components/pressable.js.map +1 -1
  50. package/dist/components/scroll-view.d.ts +4 -0
  51. package/dist/components/scroll-view.js +8 -2
  52. package/dist/components/scroll-view.js.map +1 -1
  53. package/dist/components/text-input.js +1 -1
  54. package/dist/components/text-input.js.map +1 -1
  55. package/dist/components/text.js +25 -1
  56. package/dist/components/text.js.map +1 -1
  57. package/dist/components/use-layout-child.js +17 -14
  58. package/dist/components/use-layout-child.js.map +1 -1
  59. package/dist/components/view.d.ts +9 -4
  60. package/dist/components/view.js +8 -2
  61. package/dist/components/view.js.map +1 -1
  62. package/dist/contracts.d.ts +19 -3
  63. package/dist/contracts.js.map +1 -1
  64. package/dist/gtk/index.d.ts +2 -1
  65. package/dist/gtk/index.js +8 -1
  66. package/dist/gtk/index.js.map +1 -1
  67. package/dist/gtk/widgets.generated.d.ts +14 -14
  68. package/dist/gtk/widgets.generated.js +101 -102
  69. package/dist/gtk/widgets.generated.js.map +1 -1
  70. package/dist/gtk/window-actions.d.ts +77 -0
  71. package/dist/gtk/window-actions.js +193 -0
  72. package/dist/gtk/window-actions.js.map +1 -0
  73. package/dist/gtkx/bridge/geometry.d.ts +15 -0
  74. package/dist/gtkx/bridge/geometry.js +38 -0
  75. package/dist/gtkx/bridge/geometry.js.map +1 -0
  76. package/dist/gtkx/bridge/index.d.ts +2 -0
  77. package/dist/gtkx/bridge/index.js +7 -2
  78. package/dist/gtkx/bridge/index.js.map +1 -1
  79. package/dist/gtkx/bridge/slot-portal.d.ts +12 -0
  80. package/dist/gtkx/bridge/slot-portal.js +17 -0
  81. package/dist/gtkx/bridge/slot-portal.js.map +1 -0
  82. package/dist/gtkx/bridge/svg-node.js +10 -54
  83. package/dist/gtkx/bridge/svg-node.js.map +1 -1
  84. package/dist/gtkx/bridge/use-signal.js +2 -2
  85. package/dist/gtkx/bridge/use-signal.js.map +1 -1
  86. package/dist/index.d.ts +2 -2
  87. package/dist/index.js +1 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/mcp/data/generated.d.ts +99 -77
  90. package/dist/mcp/data/generated.js +107 -76
  91. package/dist/mcp/data/generated.js.map +1 -1
  92. package/dist/mcp/server.js +1 -1
  93. package/dist/mcp/server.js.map +1 -1
  94. package/dist/navigation/sidebar.d.ts +31 -1
  95. package/dist/navigation/sidebar.js +12 -2
  96. package/dist/navigation/sidebar.js.map +1 -1
  97. package/dist/responder/system.d.ts +27 -0
  98. package/dist/responder/system.js +184 -0
  99. package/dist/responder/system.js.map +1 -0
  100. package/dist/responder/touch-history.d.ts +10 -0
  101. package/dist/responder/touch-history.js +78 -0
  102. package/dist/responder/touch-history.js.map +1 -0
  103. package/dist/responder/types.d.ts +63 -0
  104. package/dist/responder/types.js +29 -0
  105. package/dist/responder/types.js.map +1 -0
  106. package/dist/responder/use-responder.d.ts +12 -0
  107. package/dist/responder/use-responder.js +98 -0
  108. package/dist/responder/use-responder.js.map +1 -0
  109. package/dist/runner/host-dev.js +3 -1
  110. package/dist/runner/host-dev.js.map +1 -1
  111. package/dist/runner/host.js +5 -1
  112. package/dist/runner/host.js.map +1 -1
  113. package/dist/runner/index.js +1 -1
  114. package/dist/runner/index.js.map +1 -1
  115. package/dist/sea/gtkx-config-module.js +1 -1
  116. package/dist/sea/gtkx-config-module.js.map +1 -1
  117. package/dist/style/box-shadow.d.ts +7 -0
  118. package/dist/style/box-shadow.js +197 -0
  119. package/dist/style/box-shadow.js.map +1 -0
  120. package/dist/style/index.d.ts +2 -0
  121. package/dist/style/index.js +2 -0
  122. package/dist/style/index.js.map +1 -1
  123. package/dist/style/split-style.d.ts +5 -2
  124. package/dist/style/split-style.js +31 -3
  125. package/dist/style/split-style.js.map +1 -1
  126. package/dist/style/text-decoration.d.ts +15 -0
  127. package/dist/style/text-decoration.js +26 -0
  128. package/dist/style/text-decoration.js.map +1 -0
  129. package/dist/style/visual-css.js +37 -1
  130. package/dist/style/visual-css.js.map +1 -1
  131. package/dist/svg/gradient-geometry.js +2 -5
  132. package/dist/svg/gradient-geometry.js.map +1 -1
  133. package/dist/testing/index.js.map +1 -1
  134. package/dist/vendor/react-native/pan-responder.d.ts +102 -0
  135. package/dist/vendor/react-native/pan-responder.js +196 -0
  136. package/dist/vendor/react-native/pan-responder.js.map +1 -0
  137. package/dist/vendor/react-native/touch-history-math.d.ts +18 -0
  138. package/dist/vendor/react-native/touch-history-math.js +102 -0
  139. package/dist/vendor/react-native/touch-history-math.js.map +1 -0
  140. package/dist/vite/index.js +1 -1
  141. package/dist/vite/index.js.map +1 -1
  142. package/dist/vitest/index.d.ts +1 -1
  143. package/dist/vitest/index.js.map +1 -1
  144. package/package.json +9 -9
@@ -1,6 +1,6 @@
1
1
  // GENERATED FILE — do not edit by hand.
2
2
  // Produced by scripts/generate-mcp-data.mjs from docs/api.md,
3
- // docs/platform-layer.md, docs/gtkx-rc2-notes.md, docs/getting-started.md,
3
+ // docs/platform-layer.md, docs/gtkx-rc3-notes.md, docs/getting-started.md,
4
4
  // docs/research/navigation-extensibility.md and
5
5
  // scripts/widget-surface/classification.json.
6
6
  //
@@ -13,8 +13,8 @@ export const PORTABLE_COMPONENTS = [
13
13
  name: "View",
14
14
  subpath: "react-native",
15
15
  gtkImplementation: "GtkBox (RnGtkxViewBox)",
16
- supported: "`style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins)",
17
- differences: "nesting another pointerEvents inside a box-only view is not supported",
16
+ supported: "`style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here",
17
+ differences: "nesting another pointerEvents inside a box-only view is not supported; responder negotiation is capture-then-bubble as in RN, but there is **no transfer yet**: once a view is granted, `onResponderTerminationRequest`/`onResponderReject` are never called and a second view cannot steal (GTK's `CLAIMED` is irrevocable, so a transfer could not be enforced against native widgets — see docs/research/gestures.md). The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one",
18
18
  },
19
19
  {
20
20
  name: "Text",
@@ -41,7 +41,7 @@ export const PORTABLE_COMPONENTS = [
41
41
  name: "Pressable",
42
42
  subpath: "react-native",
43
43
  gtkImplementation: "GtkFixed + GestureClick/Motion",
44
- supported: "`onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered}`",
44
+ supported: "`onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered}`; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` — a desktop pointer is one fabricated touch)",
45
45
  differences: "—",
46
46
  },
47
47
  {
@@ -55,7 +55,7 @@ export const PORTABLE_COMPONENTS = [
55
55
  name: "ScrollView",
56
56
  subpath: "react-native",
57
57
  gtkImplementation: "GtkScrolledWindow",
58
- supported: "vertical/`horizontal`, `contentContainerStyle`, `onScroll`, `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` (`ScrollViewHandle`)",
58
+ supported: "vertical/`horizontal`, `contentContainerStyle`, `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` (`ScrollViewHandle`)",
59
59
  differences: "`animated` in scrollTo is ignored",
60
60
  },
61
61
  {
@@ -97,7 +97,7 @@ export const PORTABLE_COMPONENTS = [
97
97
  name: "Animated.View",
98
98
  subpath: "react-native",
99
99
  gtkImplementation: "direct widget calls",
100
- supported: "`opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange)",
100
+ supported: "`opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange), plus the same responder and touch props `View` takes — this is where an idiomatic `PanResponder` drag lands",
101
101
  differences: "`rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`)",
102
102
  },
103
103
  {
@@ -152,8 +152,8 @@ export const PORTABLE_APIS = [
152
152
  {
153
153
  name: "AppRegistry",
154
154
  subpath: "react-native",
155
- supported: "`registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,applicationActions,actionAccels,windowActions,windowControllers,breakpoints})`, `getAppKeys`",
156
- differences: 'desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar — the app\'s HeaderBars (navigation) become the chrome. `applicationActions`/`actionAccels` reach the underlying `GtkApplication` (`app.*` actions — what a `Gio.Notification` action button targets); `windowActions`/`windowControllers` reach the window (`win.*` actions, a window-scoped `GtkShortcutController`); `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` (a dev warning fires otherwise)',
155
+ supported: "`registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,actionAccels,breakpoints,applicationActions,windowActions,windowControllers})`, `getAppKeys`",
156
+ differences: 'desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar — the app\'s HeaderBars (navigation) become the chrome. `actionAccels` binds accelerators to action names on the `GtkApplication`; `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` (a dev warning fires otherwise). **`applicationActions`/`windowActions`/`windowControllers` are deprecated** — reach for [`<ApplicationActions>`/`<WindowActions>`/`<WindowControllers>`](platform-layer.md#actions-and-shortcuts-declared-in-the-app-tree) instead; they still work unchanged',
157
157
  },
158
158
  {
159
159
  name: "Platform",
@@ -227,11 +227,17 @@ export const PORTABLE_APIS = [
227
227
  supported: '`addEventListener("hardwareBackPress")`, `exitApp`',
228
228
  differences: "no hardware back key on desktop — subscriptions are honored but nothing fires them yet",
229
229
  },
230
+ {
231
+ name: "PanResponder",
232
+ subpath: "react-native",
233
+ supported: "`create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native's own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN's `touchHistory` store",
234
+ differences: "multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`'s return value is not consumed yet",
235
+ },
230
236
  {
231
237
  name: "Animated",
232
238
  subpath: "react-native",
233
- supported: "`Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity)",
234
- differences: "`useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway",
239
+ supported: "`Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity), `ValueXY` (`setValue`/`setOffset`/`flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) — the value a `PanResponder` drag writes to",
240
+ differences: "`useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway; `Animated.event` is not implemented — write the value directly (`pan.setValue({x: g.dx, y: g.dy})`), which is what it would do",
235
241
  },
236
242
  {
237
243
  name: "Easing",
@@ -301,7 +307,6 @@ export const GTK_WIDGETS = [
301
307
  { name: "GtkColumnView", subpath: "react-native-gtkx/gtk", wrapped: true },
302
308
  { name: "GtkComboBox", subpath: "react-native-gtkx/gtk", wrapped: true },
303
309
  { name: "GtkComboBoxText", subpath: "react-native-gtkx/gtk", wrapped: true },
304
- { name: "GtkDragIcon", subpath: "react-native-gtkx/gtk", wrapped: true },
305
310
  { name: "GtkDrawingArea", subpath: "react-native-gtkx/gtk", wrapped: true },
306
311
  { name: "GtkDropDown", subpath: "react-native-gtkx/gtk", wrapped: true },
307
312
  { name: "GtkEditableLabel", subpath: "react-native-gtkx/gtk", wrapped: true },
@@ -416,43 +421,49 @@ export const GTK_WIDGETS = [
416
421
  name: "GtkAboutDialog",
417
422
  subpath: "react-native-gtkx/gtk",
418
423
  wrapped: false,
419
- reason: "toplevel (derives Gtk.Window)",
424
+ reason: "toplevel (implements GtkRoot)",
420
425
  },
421
426
  {
422
427
  name: "GtkAppChooserDialog",
423
428
  subpath: "react-native-gtkx/gtk",
424
429
  wrapped: false,
425
- reason: "toplevel (derives Gtk.Window)",
430
+ reason: "toplevel (implements GtkRoot)",
426
431
  },
427
432
  {
428
433
  name: "GtkApplicationWindow",
429
434
  subpath: "react-native-gtkx/gtk",
430
435
  wrapped: false,
431
- reason: "toplevel (derives Gtk.Window)",
436
+ reason: "toplevel (implements GtkRoot)",
432
437
  },
433
438
  {
434
439
  name: "GtkAssistant",
435
440
  subpath: "react-native-gtkx/gtk",
436
441
  wrapped: false,
437
- reason: "toplevel (derives Gtk.Window)",
442
+ reason: "toplevel (implements GtkRoot)",
438
443
  },
439
444
  {
440
445
  name: "GtkColorChooserDialog",
441
446
  subpath: "react-native-gtkx/gtk",
442
447
  wrapped: false,
443
- reason: "toplevel (derives Gtk.Window)",
448
+ reason: "toplevel (implements GtkRoot)",
444
449
  },
445
450
  {
446
451
  name: "GtkDialog",
447
452
  subpath: "react-native-gtkx/gtk",
448
453
  wrapped: false,
449
- reason: "toplevel (derives Gtk.Window)",
454
+ reason: "toplevel (implements GtkRoot)",
455
+ },
456
+ {
457
+ name: "GtkDragIcon",
458
+ subpath: "react-native-gtkx/gtk",
459
+ wrapped: false,
460
+ reason: "toplevel (implements GtkRoot)",
450
461
  },
451
462
  {
452
463
  name: "GtkFileChooserDialog",
453
464
  subpath: "react-native-gtkx/gtk",
454
465
  wrapped: false,
455
- reason: "toplevel (derives Gtk.Window)",
466
+ reason: "toplevel (implements GtkRoot)",
456
467
  },
457
468
  {
458
469
  name: "GtkFlowBoxChild",
@@ -464,7 +475,7 @@ export const GTK_WIDGETS = [
464
475
  name: "GtkFontChooserDialog",
465
476
  subpath: "react-native-gtkx/gtk",
466
477
  wrapped: false,
467
- reason: "toplevel (derives Gtk.Window)",
478
+ reason: "toplevel (implements GtkRoot)",
468
479
  },
469
480
  {
470
481
  name: "GtkListBoxRow",
@@ -476,31 +487,31 @@ export const GTK_WIDGETS = [
476
487
  name: "GtkMessageDialog",
477
488
  subpath: "react-native-gtkx/gtk",
478
489
  wrapped: false,
479
- reason: "toplevel (derives Gtk.Window)",
490
+ reason: "toplevel (implements GtkRoot)",
480
491
  },
481
492
  {
482
493
  name: "GtkPageSetupUnixDialog",
483
494
  subpath: "react-native-gtkx/gtk",
484
495
  wrapped: false,
485
- reason: "toplevel (derives Gtk.Window)",
496
+ reason: "toplevel (implements GtkRoot)",
486
497
  },
487
498
  {
488
499
  name: "GtkPrintUnixDialog",
489
500
  subpath: "react-native-gtkx/gtk",
490
501
  wrapped: false,
491
- reason: "toplevel (derives Gtk.Window)",
502
+ reason: "toplevel (implements GtkRoot)",
492
503
  },
493
504
  {
494
505
  name: "GtkShortcutsWindow",
495
506
  subpath: "react-native-gtkx/gtk",
496
507
  wrapped: false,
497
- reason: "toplevel (derives Gtk.Window)",
508
+ reason: "toplevel (implements GtkRoot)",
498
509
  },
499
510
  {
500
511
  name: "GtkWindow",
501
512
  subpath: "react-native-gtkx/gtk",
502
513
  wrapped: false,
503
- reason: "toplevel (derives Gtk.Window)",
514
+ reason: "toplevel (implements GtkRoot)",
504
515
  },
505
516
  ];
506
517
  export const ADW_WIDGETS = [
@@ -610,7 +621,7 @@ export const ADW_WIDGETS = [
610
621
  name: "AdwAboutWindow",
611
622
  subpath: "react-native-gtkx/adw",
612
623
  wrapped: false,
613
- reason: "toplevel (derives Gtk.Window)",
624
+ reason: "toplevel (implements GtkRoot)",
614
625
  },
615
626
  {
616
627
  name: "AdwActionRow",
@@ -622,7 +633,7 @@ export const ADW_WIDGETS = [
622
633
  name: "AdwApplicationWindow",
623
634
  subpath: "react-native-gtkx/adw",
624
635
  wrapped: false,
625
- reason: "toplevel (derives Gtk.Window)",
636
+ reason: "toplevel (implements GtkRoot)",
626
637
  },
627
638
  {
628
639
  name: "AdwButtonRow",
@@ -652,7 +663,7 @@ export const ADW_WIDGETS = [
652
663
  name: "AdwMessageDialog",
653
664
  subpath: "react-native-gtkx/adw",
654
665
  wrapped: false,
655
- reason: "toplevel (derives Gtk.Window)",
666
+ reason: "toplevel (implements GtkRoot)",
656
667
  },
657
668
  {
658
669
  name: "AdwNavigationPage",
@@ -682,7 +693,7 @@ export const ADW_WIDGETS = [
682
693
  name: "AdwPreferencesWindow",
683
694
  subpath: "react-native-gtkx/adw",
684
695
  wrapped: false,
685
- reason: "toplevel (derives Gtk.Window)",
696
+ reason: "toplevel (implements GtkRoot)",
686
697
  },
687
698
  {
688
699
  name: "AdwSpinRow",
@@ -700,19 +711,19 @@ export const ADW_WIDGETS = [
700
711
  name: "AdwWindow",
701
712
  subpath: "react-native-gtkx/adw",
702
713
  wrapped: false,
703
- reason: "toplevel (derives Gtk.Window)",
714
+ reason: "toplevel (implements GtkRoot)",
704
715
  },
705
716
  ];
706
717
  export const DOC_CHUNKS = [
707
718
  {
708
719
  doc: "docs/api.md",
709
720
  heading: "Components",
710
- text: "| Export | GTK implementation | Supported | Differences from RN |\n| ------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `View` | GtkBox (RnGtkxViewBox) | `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins) | nesting another pointerEvents inside a box-only view is not supported |\n| `Text` | GtkLabel (Pango) | wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID` | nested `Text` elements are concatenated without per-span styles; text is always ellipsizable (shrinkable in narrow windows) |\n| `Image` | GtkPicture | `source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch → disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the \"Svg\" section below — a separate import, not part of this table) | no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet |\n| `TextInput` | GtkEntry / GtkTextView | controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics) | multiline needs a height in the style (as RN recommends) |\n| `Pressable` | GtkFixed + GestureClick/Motion | `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered}` | — |\n| `TouchableOpacity` | on top of Pressable | `activeOpacity` | — |\n| `ScrollView` | GtkScrolledWindow | vertical/`horizontal`, `contentContainerStyle`, `onScroll`, `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` (`ScrollViewHandle`) | `animated` in scrollTo is ignored |\n| `FlatList` | windowed core on ScrollView | virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + ScrollView methods (`FlatListHandle`) | 1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s → 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom |\n| `SectionList` | on top of FlatList | `sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`) | viewability props are not exposed (section-aware ViewTokens pending) |\n| `Switch` | GtkSwitch | `value`/`onValueChange`, `disabled` | sized by the GTK theme, not iOS metrics |\n| `ActivityIndicator` | GtkSpinner | `animating`, `size` (small/large/number) | no `color` yet |\n| `Modal` | modal GtkWindow (portal) | `visible`, `onRequestClose` (Escape/close button), `title`, `width`/`height`; independently resizable with relayout | desktop semantics: a separate window, not an overlay; `transparent`/`animationType` are no-ops |\n| `Animated.View` | direct widget calls | `opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange) | `rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`) |\n| `SafeAreaView` | = View | — | no notches on desktop |\n| `StatusBar` | null | — | no status bar |\n| `Root` | internal root | `width`/`height` | extension: required by the test harness |\n| `NestedRoot` | internal root | — | extension: a Yoga root inside any GTK container slot (navigation pages, custom containers); the slot allocation is the viewport |\n| `IntrinsicRoot` | internal root | — | extension: a content-sized Yoga root for chrome slots (HeaderBar start/end) — reports its content size to GTK |",
721
+ text: "| Export | GTK implementation | Supported | Differences from RN |\n| ------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `View` | GtkBox (RnGtkxViewBox) | `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here | nesting another pointerEvents inside a box-only view is not supported; responder negotiation is capture-then-bubble as in RN, but there is **no transfer yet**: once a view is granted, `onResponderTerminationRequest`/`onResponderReject` are never called and a second view cannot steal (GTK's `CLAIMED` is irrevocable, so a transfer could not be enforced against native widgets — see docs/research/gestures.md). The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one |\n| `Text` | GtkLabel (Pango) | wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID` | nested `Text` elements are concatenated without per-span styles; text is always ellipsizable (shrinkable in narrow windows) |\n| `Image` | GtkPicture | `source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch → disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the \"Svg\" section below — a separate import, not part of this table) | no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet |\n| `TextInput` | GtkEntry / GtkTextView | controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics) | multiline needs a height in the style (as RN recommends) |\n| `Pressable` | GtkFixed + GestureClick/Motion | `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered}`; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` — a desktop pointer is one fabricated touch) | — |\n| `TouchableOpacity` | on top of Pressable | `activeOpacity` | — |\n| `ScrollView` | GtkScrolledWindow | vertical/`horizontal`, `contentContainerStyle`, `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` (`ScrollViewHandle`) | `animated` in scrollTo is ignored |\n| `FlatList` | windowed core on ScrollView | virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + ScrollView methods (`FlatListHandle`) | 1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s → 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom |\n| `SectionList` | on top of FlatList | `sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`) | viewability props are not exposed (section-aware ViewTokens pending) |\n| `Switch` | GtkSwitch | `value`/`onValueChange`, `disabled` | sized by the GTK theme, not iOS metrics |\n| `ActivityIndicator` | GtkSpinner | `animating`, `size` (small/large/number) | no `color` yet |\n| `Modal` | modal GtkWindow (portal) | `visible`, `onRequestClose` (Escape/close button), `title`, `width`/`height`; independently resizable with relayout | desktop semantics: a separate window, not an overlay; `transparent`/`animationType` are no-ops |\n| `Animated.View` | direct widget calls | `opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange), plus the same responder and touch props `View` takes — this is where an idiomatic `PanResponder` drag lands | `rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`) |\n| `SafeAreaView` | = View | — | no notches on desktop |\n| `StatusBar` | null | — | no status bar |\n| `Root` | internal root | `width`/`height` | extension: required by the test harness |\n| `NestedRoot` | internal root | — | extension: a Yoga root inside any GTK container slot (navigation pages, custom containers); the slot allocation is the viewport |\n| `IntrinsicRoot` | internal root | — | extension: a content-sized Yoga root for chrome slots (HeaderBar start/end) — reports its content size to GTK |",
711
722
  },
712
723
  {
713
724
  doc: "docs/api.md",
714
725
  heading: "API modules",
715
- text: '| Export | Supported | Differences |\n| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `StyleSheet` | `create`, `flatten`, `compose`, `absoluteFill(Object)`, `hairlineWidth` | — |\n| `PlatformColor` | Adwaita variables: `PlatformColor("accent-bg-color")` → `var(--...)`, `@named` | names are Adwaita, not iOS/Android |\n| `AppRegistry` | `registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,applicationActions,actionAccels,windowActions,windowControllers,breakpoints})`, `getAppKeys` | desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar — the app\'s HeaderBars (navigation) become the chrome. `applicationActions`/`actionAccels` reach the underlying `GtkApplication` (`app.*` actions — what a `Gio.Notification` action button targets); `windowActions`/`windowControllers` reach the window (`win.*` actions, a window-scoped `GtkShortcutController`); `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` (a dev warning fires otherwise) |\n| `Platform` | `OS: "linux"`, `Version` (GTK), `select` (linux → native → default), `isTV`, `isTesting` | — |\n| `Dimensions` | `get("window"/"screen")`, `addEventListener("change")` | main window only (transient windows are ignored) |\n| `useWindowDimensions` | reactive main-window dimensions | — |\n| `Appearance` | `getColorScheme`, `setColorScheme` (AdwStyleManager), `addChangeListener` | — |\n| `useColorScheme` | reactive theme | — |\n| `AppState` | `currentState` active/background, `addEventListener` | driven by the window\'s `is-active` |\n| `Alert` | `alert(title, message, buttons, options)` → Adw.AlertDialog | `cancel`/`destructive`/`isPreferred` styles |\n| `Linking` | `openURL`, `canOpenURL` (http/https/mailto/file), `getInitialURL` (null), `addEventListener("url")` | system launcher; no deep-link delivery on desktop yet — "url" subscriptions never fire |\n| `InteractionManager` | `runAfterInteractions(task?)` (cancellable, then-able), `createInteractionHandle`/`clearInteractionHandle`, `addListener` | navigation transitions register interactions, so screen work deferred with `runAfterInteractions` waits for the push/pop slide |\n| `DevSettings` | `addMenuItem(title, handler)` (entries in the Dev Menu — Ctrl+Shift+D in `run-linux --dev`, the react-native-windows shortcut), `reload(reason?)` | silent no-ops in release builds, like RN |\n| `I18nManager` | `isRTL` (live: GTK\'s read of the locale text direction), `doLeftAndRightSwapInRTL`, `getConstants` | `allowRTL`/`forceRTL`/`swapLeftAndRightInRTL` are accepted no-ops (mobile persistence has no desktop store) |\n| `BackHandler` | `addEventListener("hardwareBackPress")`, `exitApp` | no hardware back key on desktop — subscriptions are honored but nothing fires them yet |\n| `Animated` | `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity) | `useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway |\n| `Easing` | linear/ease/quad/cubic/in/out/inOut/bezier | — |\n| `version` | package version | extension |\n\nStyles (which keys go where and what is unsupported) — [style system table](../packages/react-native-gtkx/src/style/README.md).',
726
+ text: '| Export | Supported | Differences |\n| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `StyleSheet` | `create`, `flatten`, `compose`, `absoluteFill(Object)`, `hairlineWidth` | — |\n| `PlatformColor` | Adwaita variables: `PlatformColor("accent-bg-color")` → `var(--...)`, `@named` | names are Adwaita, not iOS/Android |\n| `AppRegistry` | `registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,actionAccels,breakpoints,applicationActions,windowActions,windowControllers})`, `getAppKeys` | desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar — the app\'s HeaderBars (navigation) become the chrome. `actionAccels` binds accelerators to action names on the `GtkApplication`; `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` (a dev warning fires otherwise). **`applicationActions`/`windowActions`/`windowControllers` are deprecated** — reach for [`<ApplicationActions>`/`<WindowActions>`/`<WindowControllers>`](platform-layer.md#actions-and-shortcuts-declared-in-the-app-tree) instead; they still work unchanged |\n| `Platform` | `OS: "linux"`, `Version` (GTK), `select` (linux → native → default), `isTV`, `isTesting` | — |\n| `Dimensions` | `get("window"/"screen")`, `addEventListener("change")` | main window only (transient windows are ignored) |\n| `useWindowDimensions` | reactive main-window dimensions | — |\n| `Appearance` | `getColorScheme`, `setColorScheme` (AdwStyleManager), `addChangeListener` | — |\n| `useColorScheme` | reactive theme | — |\n| `AppState` | `currentState` active/background, `addEventListener` | driven by the window\'s `is-active` |\n| `Alert` | `alert(title, message, buttons, options)` → Adw.AlertDialog | `cancel`/`destructive`/`isPreferred` styles |\n| `Linking` | `openURL`, `canOpenURL` (http/https/mailto/file), `getInitialURL` (null), `addEventListener("url")` | system launcher; no deep-link delivery on desktop yet — "url" subscriptions never fire |\n| `InteractionManager` | `runAfterInteractions(task?)` (cancellable, then-able), `createInteractionHandle`/`clearInteractionHandle`, `addListener` | navigation transitions register interactions, so screen work deferred with `runAfterInteractions` waits for the push/pop slide |\n| `DevSettings` | `addMenuItem(title, handler)` (entries in the Dev Menu — Ctrl+Shift+D in `run-linux --dev`, the react-native-windows shortcut), `reload(reason?)` | silent no-ops in release builds, like RN |\n| `I18nManager` | `isRTL` (live: GTK\'s read of the locale text direction), `doLeftAndRightSwapInRTL`, `getConstants` | `allowRTL`/`forceRTL`/`swapLeftAndRightInRTL` are accepted no-ops (mobile persistence has no desktop store) |\n| `BackHandler` | `addEventListener("hardwareBackPress")`, `exitApp` | no hardware back key on desktop — subscriptions are honored but nothing fires them yet |\n| `PanResponder` | `create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native\'s own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN\'s `touchHistory` store | multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`\'s return value is not consumed yet |\n| `Animated` | `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity), `ValueXY` (`setValue`/`setOffset`/`flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) — the value a `PanResponder` drag writes to | `useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway; `Animated.event` is not implemented — write the value directly (`pan.setValue({x: g.dx, y: g.dy})`), which is what it would do |\n| `Easing` | linear/ease/quad/cubic/in/out/inOut/bezier | — |\n| `version` | package version | extension |\n\nStyles (which keys go where and what is unsupported) — [style system table](../packages/react-native-gtkx/src/style/README.md). Includes `boxShadow` (RN 0.76) and `outlineColor`/`outlineOffset`/`outlineStyle`/`outlineWidth` (RN 0.77): both are what Adwaita\'s own theme uses for the `.card`/`.boxed-list` frame and for every focus ring, so they are the difference between a React Native style approximating the platform look and reproducing it — see [research/react-native-first-showcase.md](research/react-native-first-showcase.md).',
716
727
  },
717
728
  {
718
729
  doc: "docs/api.md",
@@ -722,12 +733,12 @@ export const DOC_CHUNKS = [
722
733
  {
723
734
  doc: "docs/api.md",
724
735
  heading: "Navigation (`react-native-gtkx/navigation`)",
725
- text: 'A [react-navigation](https://reactnavigation.org) stack navigator backed by\n`Adw.NavigationView` — native Adwaita page transitions, the HeaderBar back\nbutton and back gestures stay in sync with react-navigation state (the\nreact-native-windows / native-stack model). Requires the optional peer\n`@react-navigation/native` (v8).\n\n`@react-navigation/native@8` itself peers on `react-native: "*"` (unlike\n`@react-navigation/core@8`, which has no react-native peer at all). If your\napp has no `react-native` package anywhere in its tree — a vite+gtkx app\nwith no Metro side, exactly what `examples/gallery` demonstrates —\n`npm install` will print an unmet-peer-dependency warning for it. This is\nharmless: react-native-gtkx never imports anything from the `react-native`\npackage, so nothing actually needs it at runtime; the warning is npm being\nstrict about a peer range upstream declared loosely (`"*"` — any version\nsatisfies it, npm just wants the package present at all).\n\n```tsx\nimport { NavigationContainer } from "@react-navigation/native"\nimport { createStackNavigator } from "react-native-gtkx/navigation"\n\n// Run the app with chrome: "content" — the navigator\'s HeaderBars ARE the\n// window chrome (the default system chrome would add a second titlebar):\n// AppRegistry.runApplication(name, { ..., chrome: "content" })\n\nconst Stack = createStackNavigator()\n\nconst App = () => (\n <NavigationContainer>\n <Stack.Navigator>\n <Stack.Screen\n name="Home"\n component={HomeScreen}\n />\n <Stack.Screen\n name="Details"\n component={DetailsScreen}\n options={{ title: "Details page" }}\n />\n </Stack.Navigator>\n </NavigationContainer>\n)\n```\n\n- Screen `options`: `title` (HeaderBar title, defaults to the route name),\n `headerShown` (default true).\n- `createSidebarNavigator` — the desktop drawer equivalent on\n `Adw.NavigationSplitView`: a persistent native sidebar (`AdwActionRow`\n per screen, in a GtkListBox with Adwaita `navigation-sidebar` styling)\n selects between parallel screens (TabRouter semantics). Navigator prop\n `sidebarTitle`; screen `options`: `title`, `icon` (Adwaita symbolic icon\n name for the row\'s prefix), `color` (a CSS color for a colored-dot\n prefix instead of `icon` — the two are mutually exclusive per row,\n `color` wins if both are set), `count` (a badge suffix, hidden when 0 or\n unset). Run the app with `chrome: "content"` so the split view\'s\n HeaderBars are the window chrome (`examples/gallery` is built on it).\n Navigator prop `headerButtons` packs declarative native buttons into the\n content HeaderBar end (`{id, icon, tooltip, onPress}`, `icon` is an\n Adwaita symbolic name) — the gallery\'s color-scheme toggle uses it.\n Navigator prop `collapseWidth` (sp): below this width the split view\n collapses to the sidebar or the content pane alone, through a native\n `Adw.Breakpoint` wrapping the view in an `AdwBreakpointBin` — NOT a\n `useWindowDimensions` conditional (see docs/platform-layer.md, "Two ways\n to react to size"); the property flip happens inside GTK\'s own\n allocation pass, costing no React render for the resize itself. Unset by\n default — no `AdwBreakpointBin` is mounted at all, so existing consumers\n see no behavior change. Any route becoming active while collapsed\n reveals content (`AdwNavigationSplitView.showContent`, a plain native\n property write, not React state) — a row click OR a programmatic\n `navigate()`/`jumpTo()`; the native back button that then appears\n reverses it. Re-selecting the same, already-active row after that also\n reveals content again — GTK\'s `row-selected` does not refire for a\n re-click with no selection change, so this is driven by `row-activated`\n (fires on every click) in addition. The reverse direction — the split\n view\'s own back button, Escape or back gesture hiding content again — is\n observed too: it fires a `sidebarShown` event\n (`navigation.addListener("sidebarShown", …)`) on the currently active\n route, the same event-map protocol `createStackNavigator`\'s\n `transitionStart`/`transitionEnd` use. Nothing in react-navigation state\n changes when this fires — TabRouter has no "closed" concept, the same\n route stays focused, only the pane did — so it exists purely for an app\n that wants to react (`examples/tasks-nav`\'s `ContentScreen` resets its\n own in-screen "open task" state on it). Never fired for content being\n revealed (that direction is already an ordinary state change) or when\n `collapseWidth` is unset. Resizing back above `collapseWidth` and then\n back below it again does NOT reset `showContent` or the selection —\n confirmed empirically, not assumed — both simply persist across the\n round trip, the same size-class behavior a mobile master-detail app\n relies on; see docs/research/navigation-extensibility.md for the\n evidence.\n- Sidebar navigator props `minWidth` / `minHeight` (px, default 360×294 —\n GNOME\'s own adaptive floor): the narrowest size this navigator\'s UI\n supports, applied to the `AdwBreakpointBin` that `collapseWidth` mounts.\n Ignored when `collapseWidth` is unset, since no bin exists then. Adwaita\n cannot measure a breakpoint bin — what it contains changes with the\n breakpoints — so the bin reports a minimum of ZERO and warns that\n `width-request`/`height-request` must be set. Under `chrome: "content"`\n the bin is the window\'s own child, so that zero IS the window\'s floor:\n the window resizes straight past what the pane inside can draw, and\n Adwaita clips the pane instead of adapting it ("AdwNavigationSplitView\n exceeds AdwBreakpointBin width: requested 469 px, 360 px available" in\n the journal, felt as a list running off the right edge with its trailing\n controls cut away). An app whose content HeaderBar needs more than the\n default must raise it — measure the pane rather than guessing: a\n segmented control as `headerTitle` costs ~110px on its own and, unlike a\n title label, cannot ellipsize. `examples/tasks-nav` passes `480` for\n exactly that reason; the value stays below its `collapseWidth`, so the\n collapsed layout is still fully reachable.\n- Sidebar screen options `headerLeft` / `headerRight` / `headerTitle`:\n `() => ReactNode` — the content HeaderBar\'s own start/end/title, per\n screen, on top of the one navigator-wide default. This is what lets one\n screen\'s header change shape with ITS OWN selection (a filter toggle\n group for a list, a back button plus star/trash for an open item):\n call `navigation.setOptions({ headerLeft, headerRight, headerTitle })`\n from inside the screen, in an effect keyed on whatever local state\n decides its shape — no stack involved, and no new navigator API beyond\n the options themselves (`useNavigationBuilder` already re-resolves\n descriptor options on every `setOptions` call). `headerTitle` replaces\n the HeaderBar\'s title widget outright (unset, the page\'s own title\n shows automatically, as before). A screen\'s own `headerButtons`\n (`HeaderButton[]`, same shape as the navigator prop) replaces the\n navigator-level default entirely for that screen. **Caveat, found\n while testing this**: `setOptions` MERGES into the previously resolved\n options rather than replacing them — a call that omits `headerRight`\n does not clear a `headerRight` a PREVIOUS call set, it leaves it in\n place. A screen that flips between shapes must give every one of these\n four keys an explicit value (`undefined` counts as a real overwrite; an\n absent key does not) on every call, not just the ones currently in use.\n- Sidebar navigator prop `sidebarContent`:\n `(props: SidebarContentProps) => ReactNode` — replaces the ENTIRE sidebar\n pane\'s body, for a sidebar that needs sections, a search field, a footer,\n or anything a flat list of rows cannot express. The sidebar\'s children\n stop being "one row per screen": you draw what you like, and navigation\n is just the `jumpTo` you were handed. `SidebarContentProps` carries\n `routes` (key, name, resolved options, title, `focused`), `focusedIndex`\n and `jumpTo(name)` — use those rather than dispatching yourself, so\n selection cannot drift from navigation state. The pane\'s AdwHeaderBar and\n `sidebarTitle` still belong to the navigator: this is the body under it,\n not the chrome. Mounted as React Native content (a layout root filling\n the pane); a sidebar built from GTK widgets wraps its own tree in\n `WidgetContent`, the same escape hatch `contentLayout: "widget"` is for a\n screen body. Reach for `sidebarRow` first if you only want a different\n ROW — it keeps the navigator\'s list and everything attached to it.\n- Sidebar screen option `sidebarRow`: `() => ReactNode` — draw the row\n yourself instead of letting `title`/`icon`/`color`/`count` compose one.\n Those four are a convenience, not the ceiling: they build an\n `AdwActionRow`, which brings Adwaita\'s own row metrics with it, so an app\n wanting a different shape, density or height had nothing to reach for.\n Return anything a `GtkListBoxRow` can hold — React Native content, GTK\n widgets, a differently-configured Adwaita row. The navigator keeps owning\n row BEHAVIOUR (selection, click → `jumpTo`, staying in step with\n navigation state, the collapsed reveal), so a custom row cannot drift out\n of sync with the router; only what is drawn changes. A screen that passes\n none of `icon`/`color`/`count` gets a compact `GtkListBoxRow` + label\n automatically — `AdwActionRow`\'s height is right when there IS a prefix\n and a count to lay out and pure cost when there is not.\n- Sidebar screen option `contentLayout`: `"react-native"` (default) or\n `"widget"` — what the screen\'s body IS. The default mounts it in a Yoga\n layout root that fills the pane, so `<View style={{ flex: 1 }}>` behaves\n the way it does anywhere else. `"widget"` packs the body into the page\n directly, with no layout root in between, for a screen whose body is a\n GTK widget tree (a `GtkScrolledWindow` around an `AdwClamp` around a\n `.boxed-list` `GtkListBox`, say): GTK\'s own sizing — `vexpand`, a list\'s\n natural height — then applies normally. **Under the default a widget tree\n collapses instead**, and quietly: every widget becomes a single Yoga LEAF\n measured for its own natural size, so a container renders its first child,\n drops the rest, and reports the ~1px it can shrink to, with no error\n anywhere. `examples/tasks-nav` is built this way. Mixing is per screen,\n not per subtree — a `"widget"` screen that wants React Native content\n somewhere inside it wraps that part in `SlotContent` itself.\n- Stack screen options `headerLeft` / `headerRight`: `() => ReactNode` —\n real RN content in the HeaderBar (inputs included), hosted by an\n intrinsic-size root; `headerButtons` render after `headerRight`\n (hn-app\'s header search filter is the demo).\n- Stack screen option `gestureEnabled: false` disables the native back\n button, Escape and the back gesture for that screen (the page\'s\n Adwaita `can-pop`); a programmatic `goBack` still pops. `usePreventRemove`\n works through the same mechanism — a prevented route reports\n `can-pop: false`, so no native pop can race react-navigation state; the\n route pops once the app lifts the guard (e.g. after its own\n confirmation dialog).\n- Stack screen option `animation` maps onto `Adw.NavigationView`\'s\n `animate-transitions` — GTK has exactly one transition style, not a\n choice of styles like iOS/Android, so the option collapses to a\n boolean: `"none"` turns transitions off, any other value (including\n native-stack\'s own style names, e.g. `"slide_from_bottom"`, `"fade"`)\n turns them on, with the standard Adwaita transition rather than the\n one asked for. Requesting a specific type still animates — it is not\n silently treated as `"none"` — and warns once in development.\n `animate-transitions` is a property of the whole view, not a per-page\n one, so there is no per-screen granularity to offer: the value used is\n read from whichever screen is currently on top of the visible stack,\n recomputed on every navigation. Setting it once via `screenOptions`\n (the same value for every screen) is the reliable way to use this —\n the per-screen case only matters if different screens genuinely\n disagree, and even then only the active one\'s value is observed.\n Interactive swipe-back gestures always animate regardless of this\n setting — Adwaita\'s own behavior, not overridable here.\n- The factories are typed: `createStackNavigator<ParamList>()` gives\n typed `Screen` configs and `StackScreenProps<ParamList, Route>` for\n screen components (`SidebarScreenProps` likewise).\n- The stack navigator emits `transitionStart` / `transitionEnd` on a\n screen\'s `navigation` object, matching `@react-navigation/stack` and\n `@react-navigation/native-stack` exactly: `{ data: { closing: boolean } }`,\n `closing: false` for the screen being pushed in, `closing: true` for the\n screen being popped out. A screen that stays mounted without actually\n entering or leaving (e.g. the screen underneath a push) gets neither\n event, same as upstream. Two things worth knowing before relying on\n timing:\n - **`transitionEnd` is tied to `AdwNavigationPage`\'s own `shown`/`hidden`\n signals** — contrary to an earlier version of this page, Adwaita DOES\n expose a transition-finished signal (four of them, in fact: `showing`,\n `shown`, `hiding`, `hidden`, all per-page). `transitionEnd` on the\n entering screen fires on that screen\'s `shown`; on the leaving screen\n it fires on `hidden`. `transitionDuration` (default 400 ms) is a\n fallback only, used when a page\'s own signal never arrives — a\n signal-less environment, or a page skipped entirely by a multi-hop\n pop (popping past an intermediate screen never fires anything on it,\n since it was never the one actually on screen during the transition).\n When transitions are not animated, the real signals still fire —\n immediately — so `transitionEnd` is not delayed by the fallback\n window either.\n - **Native pops do not fire these events at all today.** A user-driven\n pop (the Adwaita back button, Escape, the back gesture) is handled by\n the widget itself before this package\'s code is told about it, so\n there is nothing to hook a `transitionStart` into. Only\n programmatic navigation (`navigate`, `goBack`, `dispatch`, …) fires\n `transitionStart`/`transitionEnd`.\n- The sidebar navigator emits `sidebarShown` (`{ data: undefined }`) on a\n screen\'s `navigation` object — the collapsed-mode counterpart of a native\n pop, and the one case where a native, user-driven interaction (the split\n view\'s own back button, Escape, the back gesture) DOES get an event: the\n widget-level property that changes (`showContent`) has no\n react-navigation state behind it at all, so there is no state change for\n an app to observe any other way. Fired on the active route only when\n `showContent` goes from shown back to hidden, and only while\n `collapseWidth` is set; never fired for content being revealed (that\n already shows up as an ordinary focused-route change).\n- The rest of the react-navigation surface — `useNavigation`, `useRoute`,\n `useFocusEffect`, `useIsFocused`, `useNavigationContainerRef`,\n `CommonActions`, `StackActions`, `usePreventRemove`, `NavigationContainer`\n and everything else — comes from `@react-navigation/native` directly, not\n from this package. **Breaking change**: earlier versions re-exported a\n subset of these names from `react-native-gtkx/navigation`; the re-export\n was removed because it was never complete (anything beyond the subset\n still required importing from `@react-navigation/native`, so it was one\n more place to look rather than a convenience). This package\'s navigation\n entry point now exports exactly its own surface: `createStackNavigator`,\n `createSidebarNavigator`, and the option/prop types around them.\n- Each screen mounts its own layout root inside the page: the page\'s\n content allocation is that screen\'s viewport.\n- Differences from `@react-navigation/native-stack`: `headerRight`/custom\n header widgets are not supported yet; deep-link "url" events never fire\n on desktop (see `Linking`).',
736
+ text: 'A [react-navigation](https://reactnavigation.org) stack navigator backed by\n`Adw.NavigationView` — native Adwaita page transitions, the HeaderBar back\nbutton and back gestures stay in sync with react-navigation state (the\nreact-native-windows / native-stack model). Requires the optional peer\n`@react-navigation/native` (v8).\n\n`@react-navigation/native@8` itself peers on `react-native: "*"` (unlike\n`@react-navigation/core@8`, which has no react-native peer at all). If your\napp has no `react-native` package anywhere in its tree — a vite+gtkx app\nwith no Metro side, exactly what `examples/gallery` demonstrates —\n`npm install` will print an unmet-peer-dependency warning for it. This is\nharmless: react-native-gtkx never imports anything from the `react-native`\npackage, so nothing actually needs it at runtime; the warning is npm being\nstrict about a peer range upstream declared loosely (`"*"` — any version\nsatisfies it, npm just wants the package present at all).\n\n```tsx\nimport { NavigationContainer } from "@react-navigation/native"\nimport { createStackNavigator } from "react-native-gtkx/navigation"\n\n// Run the app with chrome: "content" — the navigator\'s HeaderBars ARE the\n// window chrome (the default system chrome would add a second titlebar):\n// AppRegistry.runApplication(name, { ..., chrome: "content" })\n\nconst Stack = createStackNavigator()\n\nconst App = () => (\n <NavigationContainer>\n <Stack.Navigator>\n <Stack.Screen\n name="Home"\n component={HomeScreen}\n />\n <Stack.Screen\n name="Details"\n component={DetailsScreen}\n options={{ title: "Details page" }}\n />\n </Stack.Navigator>\n </NavigationContainer>\n)\n```\n\n- Screen `options`: `title` (HeaderBar title, defaults to the route name),\n `headerShown` (default true).\n- `createSidebarNavigator` — the desktop drawer equivalent on\n `Adw.NavigationSplitView`: a persistent native sidebar (`AdwActionRow`\n per screen, in a GtkListBox with Adwaita `navigation-sidebar` styling)\n selects between parallel screens (TabRouter semantics). Navigator prop\n `sidebarTitle`; screen `options`: `title`, `icon` (Adwaita symbolic icon\n name for the row\'s prefix), `color` (a CSS color for a colored-dot\n prefix instead of `icon` — the two are mutually exclusive per row,\n `color` wins if both are set), `count` (a badge suffix, hidden when 0 or\n unset). Run the app with `chrome: "content"` so the split view\'s\n HeaderBars are the window chrome (`examples/gallery` is built on it).\n Navigator prop `headerButtons` packs declarative native buttons into the\n content HeaderBar end (`{id, icon, tooltip, onPress}`, `icon` is an\n Adwaita symbolic name) — the gallery\'s color-scheme toggle uses it.\n Navigator prop `collapseWidth` (sp): below this width the split view\n collapses to the sidebar or the content pane alone, through a native\n `Adw.Breakpoint` wrapping the view in an `AdwBreakpointBin` — NOT a\n `useWindowDimensions` conditional (see docs/platform-layer.md, "Two ways\n to react to size"); the property flip happens inside GTK\'s own\n allocation pass, costing no React render for the resize itself. Unset by\n default — no `AdwBreakpointBin` is mounted at all, so existing consumers\n see no behavior change. Any route becoming active while collapsed\n reveals content (`AdwNavigationSplitView.showContent`, a plain native\n property write, not React state) — a row click OR a programmatic\n `navigate()`/`jumpTo()`; the native back button that then appears\n reverses it. Re-selecting the same, already-active row after that also\n reveals content again — GTK\'s `row-selected` does not refire for a\n re-click with no selection change, so this is driven by `row-activated`\n (fires on every click) in addition. The reverse direction — the split\n view\'s own back button, Escape or back gesture hiding content again — is\n observed too: it fires a `sidebarShown` event\n (`navigation.addListener("sidebarShown", …)`) on the currently active\n route, the same event-map protocol `createStackNavigator`\'s\n `transitionStart`/`transitionEnd` use. Nothing in react-navigation state\n changes when this fires — TabRouter has no "closed" concept, the same\n route stays focused, only the pane did — so it exists purely for an app\n that wants to react (`examples/tasks-nav`\'s `ContentScreen` resets its\n own in-screen "open task" state on it). Never fired for content being\n revealed (that direction is already an ordinary state change) or when\n `collapseWidth` is unset. Resizing back above `collapseWidth` and then\n back below it again does NOT reset `showContent` or the selection —\n confirmed empirically, not assumed — both simply persist across the\n round trip, the same size-class behavior a mobile master-detail app\n relies on; see docs/research/navigation-extensibility.md for the\n evidence.\n- **Which rung to reach for.** Three ways to put content in the sidebar,\n cheapest first — the same ladder react-navigation\'s own `tabBarIcon` →\n `drawerLabel` → `drawerContent` climbs: (1) `title`/`icon`/`color`/`count`\n above — the convenience; composes an `AdwActionRow`. (2) `sidebarRow`\n (screen option, below) — draw one row yourself; the navigator keeps the\n list and everything attached to it (selection, click → `jumpTo`, staying\n in step with navigation state, the collapsed reveal). (3) `sidebarContent`\n (navigator prop, below) — draw the whole pane, routing surface included.\n The reason rungs 2 and 3 exist at all, plainly: **`AdwActionRow` carries\n Adwaita\'s OWN row metrics, not a default this package picked** — measured\n at roughly 104px per row (with a prefix and/or count laid out) against\n ~40px for a plain title-only row — and nothing passed to\n `title`/`icon`/`color`/`count` changes that height. A screen on rung 1\n has no lever for it; wanting a different height or density means climbing\n to `sidebarRow` or `sidebarContent` instead.\n- Sidebar navigator props `minWidth` / `minHeight` (px, default 360×294 —\n GNOME\'s own adaptive floor): the narrowest size this navigator\'s UI\n supports, applied to the `AdwBreakpointBin` that `collapseWidth` mounts.\n Ignored when `collapseWidth` is unset, since no bin exists then. Adwaita\n cannot measure a breakpoint bin — what it contains changes with the\n breakpoints — so the bin reports a minimum of ZERO and warns that\n `width-request`/`height-request` must be set. Under `chrome: "content"`\n the bin is the window\'s own child, so that zero IS the window\'s floor:\n the window resizes straight past what the pane inside can draw, and\n Adwaita clips the pane instead of adapting it ("AdwNavigationSplitView\n exceeds AdwBreakpointBin width: requested 469 px, 360 px available" in\n the journal, felt as a list running off the right edge with its trailing\n controls cut away). An app whose content HeaderBar needs more than the\n default must raise it — measure the pane rather than guessing: a\n segmented control as `headerTitle` costs ~110px on its own and, unlike a\n title label, cannot ellipsize. `examples/tasks-nav` passes `480` for\n exactly that reason; the value stays below its `collapseWidth`, so the\n collapsed layout is still fully reachable.\n- Sidebar screen options `headerLeft` / `headerRight` / `headerTitle`:\n `() => ReactNode` — the content HeaderBar\'s own start/end/title, per\n screen, on top of the one navigator-wide default. This is what lets one\n screen\'s header change shape with ITS OWN selection (a filter toggle\n group for a list, a back button plus star/trash for an open item):\n call `navigation.setOptions({ headerLeft, headerRight, headerTitle })`\n from inside the screen, in an effect keyed on whatever local state\n decides its shape — no stack involved, and no new navigator API beyond\n the options themselves (`useNavigationBuilder` already re-resolves\n descriptor options on every `setOptions` call). `headerTitle` replaces\n the HeaderBar\'s title widget outright (unset, the page\'s own title\n shows automatically, as before). A screen\'s own `headerButtons`\n (`HeaderButton[]`, same shape as the navigator prop) replaces the\n navigator-level default entirely for that screen. **Caveat, found\n while testing this**: `setOptions` MERGES into the previously resolved\n options rather than replacing them — a call that omits `headerRight`\n does not clear a `headerRight` a PREVIOUS call set, it leaves it in\n place. A screen that flips between shapes must give every one of these\n four keys an explicit value (`undefined` counts as a real overwrite; an\n absent key does not) on every call, not just the ones currently in use.\n- Sidebar screen option `sidebarRow`: `() => ReactNode` — draw the row\n yourself instead of letting `title`/`icon`/`color`/`count` compose one.\n Those four are a convenience, not the ceiling: they build an\n `AdwActionRow`, which brings Adwaita\'s own row metrics with it, so an app\n wanting a different shape, density or height had nothing to reach for.\n Return anything a `GtkListBoxRow` can hold — React Native content, GTK\n widgets, a differently-configured Adwaita row. The navigator keeps owning\n row BEHAVIOUR (selection, click → `jumpTo`, staying in step with\n navigation state, the collapsed reveal), so a custom row cannot drift out\n of sync with the router; only what is drawn changes. A screen that passes\n none of `icon`/`color`/`count` gets a compact `GtkListBoxRow` + label\n automatically — `AdwActionRow`\'s height is right when there IS a prefix\n and a count to lay out and pure cost when there is not. The next rung up\n is `sidebarContent`, below, for replacing the whole pane rather than one\n row.\n- Sidebar navigator prop `sidebarContent`:\n `(props: SidebarContentProps) => ReactNode` — replaces the ENTIRE sidebar\n pane\'s body, for a sidebar that needs sections, a search field, a footer,\n or anything a flat list of rows cannot express. The sidebar\'s children\n stop being "one row per screen": you draw what you like, and navigation\n is just the `jumpTo` you were handed. `SidebarContentProps` carries\n `routes` (key, name, resolved options, title, `focused`), `focusedIndex`\n and `jumpTo(name)` — use those rather than dispatching yourself, so\n selection cannot drift from navigation state. The pane\'s AdwHeaderBar and\n `sidebarTitle` still belong to the navigator: this is the body under it,\n not the chrome. Mounted as React Native content (a layout root filling\n the pane); a sidebar built from GTK widgets wraps its own tree in\n `WidgetContent`, the same escape hatch `contentLayout: "widget"` is for a\n screen body. Reach for `sidebarRow` (above) first if you only want a\n different ROW — it keeps the navigator\'s list and everything attached to\n it; this one hands over the whole pane, routing included. A sidebar with\n a search field above the list and a footer below it, still driven by the\n navigator\'s own routing:\n\n ```tsx\n <Sidebar.Navigator\n sidebarContent={({ routes, focusedIndex, jumpTo }) => (\n <View style={{ flex: 1 }}>\n <SearchField onSubmit={filterRoutes} />\n <ScrollView style={{ flex: 1 }}>\n {routes.map((route, index) => (\n <Pressable\n key={route.key}\n onPress={() => jumpTo(route.name)}\n >\n <Text\n style={{\n padding: 8,\n fontWeight: index === focusedIndex ? "700" : "400",\n }}\n >\n {route.title}\n </Text>\n </Pressable>\n ))}\n </ScrollView>\n <StorageUsageFooter />\n </View>\n )}\n >\n <Sidebar.Screen\n name="Inbox"\n component={InboxScreen}\n />\n <Sidebar.Screen\n name="Trash"\n component={TrashScreen}\n />\n </Sidebar.Navigator>\n ```\n\n `route.title` is already resolved (`options.title`, falling back to the\n route name) — no need to read `options.title` yourself. `jumpTo` reveals\n the content pane when collapsed, same as a native row click; the\n navigator, not this callback, decides that.\n\n- Sidebar navigator props `sidebarHeaderLeft` / `sidebarHeaderRight` /\n `sidebarHeaderTitle`: `() => ReactNode` — the SIDEBAR pane\'s own\n AdwHeaderBar start/end/title, the exact counterparts of the content\n header\'s `headerLeft`/`headerRight`/`headerTitle`. Until these existed the\n sidebar header was a hard-coded `<AdwHeaderBar />` and `sidebarTitle` (a\n plain string) was the only thing an app could set on it at all, so a\n sidebar\'s own "new item" action — where GNOME puts it, next to the pane\n title — had nowhere to go and ended up on the content header instead\n (`examples/tasks-nav` shipped with two indistinguishable `+` buttons for\n exactly this reason). `sidebarHeaderTitle` replaces the title widget the\n same way a screen\'s `headerTitle` does; unset, `sidebarTitle` renders as\n before. Content is mounted through the same `HeaderSlotContent` root the\n content header uses, so React Native content lays out as a horizontal,\n content-hugging cluster flush with natively packed buttons — do not\n hand-roll an `IntrinsicContent` here, a bare Yoga root defaults to\n `column` and pushes the window controls onto a second row. These are\n navigator PROPS rather than screen options on purpose: there is one\n sidebar pane shared by every screen, so its chrome sits at the level\n `sidebarTitle`/`sidebarContent` already do, and the `sidebar` prefix marks\n which header a name refers to. There is deliberately no\n `sidebarHeaderButtons` convenience mirroring `headerButtons` — arbitrary\n content is the primitive, and a one-button call site reads no better as a\n `{id, icon, tooltip, onPress}` record than as the `GtkButton` it already\n is; add it only if a real call site is worse without it.\n\n- Sidebar screen option `contentLayout`: `"react-native"` (default) or\n `"widget"` — what the screen\'s body IS. The default mounts it in a Yoga\n layout root that fills the pane, so `<View style={{ flex: 1 }}>` behaves\n the way it does anywhere else. `"widget"` packs the body into the page\n directly, with no layout root in between, for a screen whose body is a\n GTK widget tree (a `GtkScrolledWindow` around an `AdwClamp` around a\n `.boxed-list` `GtkListBox`, say): GTK\'s own sizing — `vexpand`, a list\'s\n natural height — then applies normally. **Under the default a widget tree\n collapses instead**, and quietly: every widget becomes a single Yoga LEAF\n measured for its own natural size, so a container renders its first child,\n drops the rest, and reports the ~1px it can shrink to, with no error\n anywhere. `examples/tasks-nav` is built this way. Mixing is per screen,\n not per subtree — a `"widget"` screen that wants React Native content\n somewhere inside it wraps that part in `SlotContent` itself.\n- Stack screen options `headerLeft` / `headerRight`: `() => ReactNode` —\n real RN content in the HeaderBar (inputs included), hosted by an\n intrinsic-size root; `headerButtons` render after `headerRight`\n (hn-app\'s header search filter is the demo).\n- Stack screen option `gestureEnabled: false` disables the native back\n button, Escape and the back gesture for that screen (the page\'s\n Adwaita `can-pop`); a programmatic `goBack` still pops. `usePreventRemove`\n works through the same mechanism — a prevented route reports\n `can-pop: false`, so no native pop can race react-navigation state; the\n route pops once the app lifts the guard (e.g. after its own\n confirmation dialog).\n- Stack screen option `animation` maps onto `Adw.NavigationView`\'s\n `animate-transitions` — GTK has exactly one transition style, not a\n choice of styles like iOS/Android, so the option collapses to a\n boolean: `"none"` turns transitions off, any other value (including\n native-stack\'s own style names, e.g. `"slide_from_bottom"`, `"fade"`)\n turns them on, with the standard Adwaita transition rather than the\n one asked for. Requesting a specific type still animates — it is not\n silently treated as `"none"` — and warns once in development.\n `animate-transitions` is a property of the whole view, not a per-page\n one, so there is no per-screen granularity to offer: the value used is\n read from whichever screen is currently on top of the visible stack,\n recomputed on every navigation. Setting it once via `screenOptions`\n (the same value for every screen) is the reliable way to use this —\n the per-screen case only matters if different screens genuinely\n disagree, and even then only the active one\'s value is observed.\n Interactive swipe-back gestures always animate regardless of this\n setting — Adwaita\'s own behavior, not overridable here.\n- The factories are typed: `createStackNavigator<ParamList>()` gives\n typed `Screen` configs and `StackScreenProps<ParamList, Route>` for\n screen components (`SidebarScreenProps` likewise).\n- The stack navigator emits `transitionStart` / `transitionEnd` on a\n screen\'s `navigation` object, matching `@react-navigation/stack` and\n `@react-navigation/native-stack` exactly: `{ data: { closing: boolean } }`,\n `closing: false` for the screen being pushed in, `closing: true` for the\n screen being popped out. A screen that stays mounted without actually\n entering or leaving (e.g. the screen underneath a push) gets neither\n event, same as upstream. Two things worth knowing before relying on\n timing:\n - **`transitionEnd` is tied to `AdwNavigationPage`\'s own `shown`/`hidden`\n signals** — contrary to an earlier version of this page, Adwaita DOES\n expose a transition-finished signal (four of them, in fact: `showing`,\n `shown`, `hiding`, `hidden`, all per-page). `transitionEnd` on the\n entering screen fires on that screen\'s `shown`; on the leaving screen\n it fires on `hidden`. `transitionDuration` (default 400 ms) is a\n fallback only, used when a page\'s own signal never arrives — a\n signal-less environment, or a page skipped entirely by a multi-hop\n pop (popping past an intermediate screen never fires anything on it,\n since it was never the one actually on screen during the transition).\n When transitions are not animated, the real signals still fire —\n immediately — so `transitionEnd` is not delayed by the fallback\n window either.\n - **Native pops do not fire these events at all today.** A user-driven\n pop (the Adwaita back button, Escape, the back gesture) is handled by\n the widget itself before this package\'s code is told about it, so\n there is nothing to hook a `transitionStart` into. Only\n programmatic navigation (`navigate`, `goBack`, `dispatch`, …) fires\n `transitionStart`/`transitionEnd`.\n- The sidebar navigator emits `sidebarShown` (`{ data: undefined }`) on a\n screen\'s `navigation` object — the collapsed-mode counterpart of a native\n pop, and the one case where a native, user-driven interaction (the split\n view\'s own back button, Escape, the back gesture) DOES get an event: the\n widget-level property that changes (`showContent`) has no\n react-navigation state behind it at all, so there is no state change for\n an app to observe any other way. Fired on the active route only when\n `showContent` goes from shown back to hidden, and only while\n `collapseWidth` is set; never fired for content being revealed (that\n already shows up as an ordinary focused-route change).\n- The rest of the react-navigation surface — `useNavigation`, `useRoute`,\n `useFocusEffect`, `useIsFocused`, `useNavigationContainerRef`,\n `CommonActions`, `StackActions`, `usePreventRemove`, `NavigationContainer`\n and everything else — comes from `@react-navigation/native` directly, not\n from this package. **Breaking change**: earlier versions re-exported a\n subset of these names from `react-native-gtkx/navigation`; the re-export\n was removed because it was never complete (anything beyond the subset\n still required importing from `@react-navigation/native`, so it was one\n more place to look rather than a convenience). This package\'s navigation\n entry point now exports exactly its own surface: `createStackNavigator`,\n `createSidebarNavigator`, and the option/prop types around them.\n- Each screen mounts its own layout root inside the page: the page\'s\n content allocation is that screen\'s viewport.\n- Differences from `@react-navigation/native-stack`: `headerRight`/custom\n header widgets are not supported yet; deep-link "url" events never fire\n on desktop (see `Linking`).',
726
737
  },
727
738
  {
728
739
  doc: "docs/api.md",
729
740
  heading: "Svg",
730
- text: 'Vector graphics built from state, modeled on\n[react-native-svg](https://github.com/software-mansion/react-native-svg) (the\nde-facto standard RN mirrors) rather than invented from scratch — portable\ncode costs nothing to bring over. Drawing goes through `Gsk.Path`/\n`Gtk.Snapshot` on a single custom widget (`RnGtkxSvgNode`, `registerClass` +\nan overridden `snapshot()` vfunc — the same mechanism `RnGtkxLayout` and\n`RnGtkxViewBox` already use), not a rasterized image: for that, `Image`\nalready loads `.svg` files today (see the `Image` row above).\n\n**Not part of the main `react-native-gtkx` export surface** — unlike every\ncomponent in the table above, `Svg` and everything below are exported only\nfrom `react-native-gtkx/svg`, in the shape of the `react-native-svg` package\nitself. `react-native-svg` is a separate package on every other platform (RN\nhas no built-in `Svg`), so this project mirrors that split instead of adding\n`Svg` to the main entry, which would make code written against it fail to\ncompile anywhere else. See "`react-native-svg` compatibility" below for the\nexact import and how the alias resolves it.\n\n```tsx\nimport Svg, { Circle, G, Path, Rect } from "react-native-svg"\n\nconst Icon = () => (\n <Svg\n width={24}\n height={24}\n viewBox="0 0 24 24"\n >\n <Circle\n cx={12}\n cy={12}\n r={10}\n fill="#1c71d8"\n />\n <Path\n d="M8 12 l3 3 l5 -6"\n stroke="white"\n strokeWidth={2}\n fill="none"\n />\n </Svg>\n)\n```\n\n- **`Svg`**: `width`/`height` (or `style`) size it — a Yoga leaf like\n `Image`, sized entirely by style/flex, never by measuring the widget\n (nothing here is intrinsic-sized). `viewBox="minX minY width height"` and\n `preserveAspectRatio` (`xMin/xMid/xMax` × `YMin/YMid/YMax`, `meet`/`slice`,\n `none`; default `xMidYMid meet`) reshape the internal coordinate system\n exactly like real SVG — Yoga never sees them. Content always clips to the\n allocated bounds (no `overflow: visible` opt-out).\n- **`Path`**: `d` is handed straight to `Gsk.Path.parse()`, which understands\n SVG path syntax natively — there is no path parser of our own.\n- **`Rect`** (`x`/`y`/`width`/`height`/`rx`/`ry`), **`Circle`**\n (`cx`/`cy`/`r`), **`Ellipse`** (`cx`/`cy`/`rx`/`ry`), **`Line`**\n (`x1`/`y1`/`x2`/`y2`, stroke-only — no `fill` prop at all, not even\n ignored), **`Polygon`**/**`Polyline`** (`points`, `"x,y x,y …"` or\n space-separated, closed/open respectively): each is a small geometry\n helper away from the same `d` syntax, so every shape ends up drawn through\n that one `Gsk.Path.parse()` call.\n- Every shape accepts `fill`/`stroke` (a static CSS color — hex/`rgb()`/\n `hsl()`/named/`transparent`/`none`, or `"url(#id)"` referencing a\n gradient; default `fill="black"`, `stroke="none"`, matching SVG),\n `fillRule` (`nonzero` | `evenodd`), `fillOpacity`/`strokeOpacity`/\n `opacity`, `strokeWidth`, `strokeLinecap`/`strokeLinejoin`,\n `strokeDasharray`, `strokeDashoffset`.\n- **`G`** groups children under an `opacity` and/or a `transform` string —\n `translate()`/`scale()`/`rotate()`/`rotate(a,cx,cy)`/`matrix()`, the plain\n SVG transform-list syntax (`matrix()` maps directly onto\n `Gsk.Transform.matrix2d()`); `skewX`/`skewY` and the structured\n `transform={[{translateX:...}]}` array form `Animated.View` accepts are\n not supported here.\n- **Gradients**: `<Defs>` holds `<LinearGradient id x1 y1 x2 y2>` /\n `<RadialGradient id cx cy r>` (fractions 0–1 by default —\n `gradientUnits="objectBoundingBox"`, mapped against the shape\'s own\n `Gsk.Path.getBounds()`; `gradientUnits="userSpaceOnUse"` uses the\n coordinates as-is instead), each with `<Stop offset stopColor\nstopOpacity>` children (`offset` accepts `0.5` or `"50%"`). `Defs` must be\n a direct child of `Svg` (nested `Defs` are not scanned). No\n `gradientTransform`, no `spreadMethod` beyond the default pad behavior.\n **Known limitation**: constructing a `Gsk.ColorStop` currently crashes in\n gtkx-rc2\'s native addon — verified through three independent construction\n paths (the generated constructor, its property setters, and a bypass that\n skips `ColorStop` entirely), all failing in the same compiled native code,\n so this is not fixable from application code. A gradient reference\n degrades to painting nothing for that fill/stroke (the same safe path as\n an unresolvable `url(#id)`) rather than crashing the app; the coordinate\n math itself is unaffected and unit-tested\n (`packages/react-native-gtkx/tests/unit/svg/gradient-geometry.test.ts`) —\n gradients will render as soon as this is fixed upstream, with no changes\n needed on either side.\n- **Animated**: the numeric props above (shape geometry, `opacity`,\n `strokeWidth`, `strokeDashoffset`) accept an `Animated.Value`/\n interpolation in place of a number. A tick mutates the widget\'s paint\n state directly and calls `queueDraw()` — the same bypass-React pattern\n `Animated.View` uses for `transform` (`setStoredTransform` +\n `queueAllocate`), just on its own invalidation channel since none of this\n touches Yoga. `G`\'s `transform` string and `d`/`points` are not\n Animated-aware (they are strings, not numbers).\n- Not in scope: `<Text>`/`<TSpan>` on a path, `<Mask>`, `<ClipPath>`, SVG\n filters, `<Use>`/`<Symbol>`/`<Pattern>`, and rasterizing arbitrary SVG\n strings at runtime (`SvgXml` — `Image` already covers SVG **files**). None\n of these have a real consumer yet; `Path`/`Rect`/`Circle`/`Ellipse`/\n `Line`/`Polygon`/`Polyline`/`G` cover icons, charts and indicators, the\n overwhelming majority of real usage.',
741
+ text: 'Vector graphics built from state, modeled on\n[react-native-svg](https://github.com/software-mansion/react-native-svg) (the\nde-facto standard RN mirrors) rather than invented from scratch — portable\ncode costs nothing to bring over. Drawing goes through `Gsk.Path`/\n`Gtk.Snapshot` on a single custom widget (`RnGtkxSvgNode`, `registerClass` +\nan overridden `snapshot()` vfunc — the same mechanism `RnGtkxLayout` and\n`RnGtkxViewBox` already use), not a rasterized image: for that, `Image`\nalready loads `.svg` files today (see the `Image` row above).\n\n**Not part of the main `react-native-gtkx` export surface** — unlike every\ncomponent in the table above, `Svg` and everything below are exported only\nfrom `react-native-gtkx/svg`, in the shape of the `react-native-svg` package\nitself. `react-native-svg` is a separate package on every other platform (RN\nhas no built-in `Svg`), so this project mirrors that split instead of adding\n`Svg` to the main entry, which would make code written against it fail to\ncompile anywhere else. See "`react-native-svg` compatibility" below for the\nexact import and how the alias resolves it.\n\n```tsx\nimport Svg, { Circle, G, Path, Rect } from "react-native-svg"\n\nconst Icon = () => (\n <Svg\n width={24}\n height={24}\n viewBox="0 0 24 24"\n >\n <Circle\n cx={12}\n cy={12}\n r={10}\n fill="#1c71d8"\n />\n <Path\n d="M8 12 l3 3 l5 -6"\n stroke="white"\n strokeWidth={2}\n fill="none"\n />\n </Svg>\n)\n```\n\n- **`Svg`**: `width`/`height` (or `style`) size it — a Yoga leaf like\n `Image`, sized entirely by style/flex, never by measuring the widget\n (nothing here is intrinsic-sized). `viewBox="minX minY width height"` and\n `preserveAspectRatio` (`xMin/xMid/xMax` × `YMin/YMid/YMax`, `meet`/`slice`,\n `none`; default `xMidYMid meet`) reshape the internal coordinate system\n exactly like real SVG — Yoga never sees them. Content always clips to the\n allocated bounds (no `overflow: visible` opt-out).\n- **`Path`**: `d` is handed straight to `Gsk.Path.parse()`, which understands\n SVG path syntax natively — there is no path parser of our own.\n- **`Rect`** (`x`/`y`/`width`/`height`/`rx`/`ry`), **`Circle`**\n (`cx`/`cy`/`r`), **`Ellipse`** (`cx`/`cy`/`rx`/`ry`), **`Line`**\n (`x1`/`y1`/`x2`/`y2`, stroke-only — no `fill` prop at all, not even\n ignored), **`Polygon`**/**`Polyline`** (`points`, `"x,y x,y …"` or\n space-separated, closed/open respectively): each is a small geometry\n helper away from the same `d` syntax, so every shape ends up drawn through\n that one `Gsk.Path.parse()` call.\n- Every shape accepts `fill`/`stroke` (a static CSS color — hex/`rgb()`/\n `hsl()`/named/`transparent`/`none`, or `"url(#id)"` referencing a\n gradient; default `fill="black"`, `stroke="none"`, matching SVG),\n `fillRule` (`nonzero` | `evenodd`), `fillOpacity`/`strokeOpacity`/\n `opacity`, `strokeWidth`, `strokeLinecap`/`strokeLinejoin`,\n `strokeDasharray`, `strokeDashoffset`.\n- **`G`** groups children under an `opacity` and/or a `transform` string —\n `translate()`/`scale()`/`rotate()`/`rotate(a,cx,cy)`/`matrix()`, the plain\n SVG transform-list syntax (`matrix()` maps directly onto\n `Gsk.Transform.matrix2d()`); `skewX`/`skewY` and the structured\n `transform={[{translateX:...}]}` array form `Animated.View` accepts are\n not supported here.\n- **Gradients**: `<Defs>` holds `<LinearGradient id x1 y1 x2 y2>` /\n `<RadialGradient id cx cy r>` (fractions 0–1 by default —\n `gradientUnits="objectBoundingBox"`, mapped against the shape\'s own\n `Gsk.Path.getBounds()`; `gradientUnits="userSpaceOnUse"` uses the\n coordinates as-is instead), each with `<Stop offset stopColor\nstopOpacity>` children (`offset` accepts `0.5` or `"50%"`). `Defs` must be\n a direct child of `Svg` (nested `Defs` are not scanned). No\n `gradientTransform`, no `spreadMethod` beyond the default pad behavior.\n An unresolvable `url(#id)` paints nothing for that fill/stroke rather\n than throwing.\n- **Animated**: the numeric props above (shape geometry, `opacity`,\n `strokeWidth`, `strokeDashoffset`) accept an `Animated.Value`/\n interpolation in place of a number. A tick mutates the widget\'s paint\n state directly and calls `queueDraw()` — the same bypass-React pattern\n `Animated.View` uses for `transform` (`setStoredTransform` +\n `queueAllocate`), just on its own invalidation channel since none of this\n touches Yoga. `G`\'s `transform` string and `d`/`points` are not\n Animated-aware (they are strings, not numbers).\n- Not in scope: `<Text>`/`<TSpan>` on a path, `<Mask>`, `<ClipPath>`, SVG\n filters, `<Use>`/`<Symbol>`/`<Pattern>`, and rasterizing arbitrary SVG\n strings at runtime (`SvgXml` — `Image` already covers SVG **files**). None\n of these have a real consumer yet; `Path`/`Rect`/`Circle`/`Ellipse`/\n `Line`/`Polygon`/`Polyline`/`G` cover icons, charts and indicators, the\n overwhelming majority of real usage.',
731
742
  },
732
743
  {
733
744
  doc: "docs/api.md",
@@ -747,17 +758,22 @@ export const DOC_CHUNKS = [
747
758
  {
748
759
  doc: "docs/platform-layer.md",
749
760
  heading: "React Native content inside GTK slots",
750
- text: "| Export | Sizing | Use for |\n| ------------------ | ---------------------------- | ------------------------------------------------ |\n| `SlotContent` | fills the slot | a page body, a pane, a dialog body |\n| `IntrinsicContent` | sized by its own Yoga layout | an AdwHeaderBar slot, a toolbar area, a list row |",
761
+ text: "| Export | Sizing | Use for |\n| ------------------ | ---------------------------- | ------------------------------------------------ |\n| `SlotContent` | fills the slot | a page body, a pane, a dialog body |\n| `IntrinsicContent` | sized by its own Yoga layout | an AdwHeaderBar slot, a toolbar area, a list row |\n\n`createSidebarNavigator`'s `sidebarRow` screen option (docs/api.md) wraps\nits content in exactly `IntrinsicContent` for this reason — a row is sized\nby what it holds, not stretched to fill the list.\n\n**Every content area inside a widget needs one of them.** A widget hands out\nrectangles two ways: as ordinary CHILDREN (a content area) and as SLOTS —\nproperties that take a widget, `topBar={…}`, `titleWidget={…}`, `sheet={…}`.\nWhich way a given area arrives is gtkx's business and moves between releases\n(rc.3 took the `content`/`child` props off single-child widgets and made that\ncontent a child), and it has never had anything to do with layout. Both are\nGTK's territory: the layout root is cleared on the way in, so a widget lands\nbare (what `WidgetContent` does by hand) and React Native content has to bring\nits own root.\n\n```tsx\n<AdwBottomSheet\n style={{ flex: 1 }}\n sheet={\n <IntrinsicContent>\n <View style={{ padding: 20, gap: 10 }}>…</View>\n </IntrinsicContent>\n }\n bottomBar={\n <IntrinsicContent>\n <View style={{ flexDirection: \"row\", gap: 8 }}>…</View>\n </IntrinsicContent>\n }\n>\n {/* the content area — a child under rc.3, and just as much a boundary */}\n <SlotContent>\n <View style={{ flex: 1, justifyContent: \"center\" }}>…</View>\n </SlotContent>\n</AdwBottomSheet>\n```\n\nForget the wrapper and you get an error naming the widget and where the\ncontent landed, not a wrong-looking window: without a root, content inside a\nwidget would join the ENCLOSING Yoga tree — laid out against the window's\nviewport while GTK hands it the widget's own rectangle.\n`examples/bottom-sheet` is that whole story in one screen.\n\nWhich of the two is yours to choose, and the platform deliberately does not\nguess: `AdwBottomSheet` alone FILLS in its content child but HUGS in both\n`sheet` and `bottomBar`. One widget, three content areas, two answers, with\nnothing in the name or the GIR type to tell them apart — the answer lives in\nthe widget's own layout code. Swapping them is visible immediately:\n`SlotContent` in a bottom bar collapses it to nothing (a filling root reports\na zero minimum, so a size-to-content area is told \"nothing\"), and\n`IntrinsicContent` around a content area leaves `flex: 1` with no viewport to\nfill.\n\nNote the two independent sizes here. `style={{ flex: 1 }}` on the widget is\nthe WIDGET's size in the surrounding React Native layout (a wrapped widget is\na Yoga leaf at its natural size until the style says otherwise); the wrapper\ninside each content area is the CONTENT's size inside the rectangle that\nwidget then hands out.",
762
+ },
763
+ {
764
+ doc: "docs/platform-layer.md",
765
+ heading: "Adwaita chrome, written in React Native",
766
+ text: "The opposite direction from everything above: these create no widget an app\ncould not have created itself, because what they encode is a **look**, not a\nbehaviour. `docs/research/react-native-first-showcase.md` measured Adwaita's\n`.boxed-list` out of libadwaita's own compiled stylesheet and found it is a\nrounded, shadowed card whose rows carry a hairline separator, a hover tint\nand a press tint — every one of which is a React Native style prop. What an\napp should not have to do is rediscover the numbers, which are not obvious\n(the frame is a three-part `box-shadow`, not a border; the corner radii live\non the first and last ROW, not on the container) and which move when\nlibadwaita moves.\n\n| Export | What it is |\n| --------------- | ------------------------------------------------------------------------------------------- |\n| `List` | the `.boxed-list` frame — a `View` with the card background, radius and shadow |\n| `ListRow` | `AdwActionRow`'s layout and states (`title`/`subtitle`/`prefix`/`suffix`), on a `Pressable` |\n| `ListSeparator` | the hairline, for a `FlatList`'s `ItemSeparatorComponent` |\n| `rowPosition` | `(index, count)` → `\"first\" | \"middle\" | \"last\" | \"only\"`, since RN has no `:first-child` |\n| `Icon` | a **named** icon from the desktop icon theme |\n\n`Icon` is not `Image`: RN's `Image` takes a file path or URI, because on iOS\nand Android an icon is a bundled asset. Here it is a _name_ resolved against\nthe current icon theme at paint time, which recolours itself with the label\ncolour and follows the user's theme — nothing in `Image`'s contract can\nexpress that. The shape is the one RN apps already use\n(`<Icon name size />`), with the desktop icon theme behind it instead of a\nbundled font.\n\n`ListRow` does **not** yet do keyboard navigation between rows or draw a\nfocus ring. `GtkListBox` implements both as widget behaviour, and RN has no\nfocus model for `View` to hang them on — `Pressable`'s state callback is\n`{pressed, hovered}` with no `focused`. The ring itself is drawable\n(`outlineWidth`/`outlineColor`/`outlineStyle`/`outlineOffset`, see the style\ntable); what is missing is the state, not the paint.",
751
767
  },
752
768
  {
753
769
  doc: "docs/platform-layer.md",
754
770
  heading: "GTK widgets, driven by React Native",
755
- text: 'Every `GtkWidget` subclass gtkx binds — 87 of them at last count, from\n`GtkBox` and `GtkButton` to `GtkColumnView` and `GtkEmojiChooser`. The list is\ngenerated, not hand-picked: `scripts/generate-widget-surface.ts` classifies\ngtkx\'s full binding by real GObject inheritance (see\n`scripts/widget-surface/classification.json` for the exact list gtkx binds\ntoday) and `src/gtk/widgets.generated.ts` is the committed result. Re-run the\ngenerator after a gtkx upgrade to pick up new widgets — it diffs against its\nown previous output and prints what changed.\n\nThey keep **every prop gtkx binds** and gain `style` and `onLayout`. Position\nand appearance both come from the style prop, exactly like anywhere else in\nReact Native:\n\n```tsx\n<View style={{ flexDirection: "row", gap: 8, alignItems: "center" }}>\n <GtkEntry\n style={{ flex: 1 }}\n placeholderText="Filter"\n />\n <GtkButton\n style={{ width: 72, backgroundColor: "#3584e4", borderRadius: 6 }}\n label="Go"\n />\n</View>\n```\n\nThe entry flexes, the button takes its width and its colour. The layout half\nof the style drives Yoga; the visual half becomes a GTK CSS class **on the\nwidget itself**, so the button really is blue, not a blue box behind a button.\nSet no size and the widget\'s own natural size wins.\n\n**Outside React Native layout they step aside.** The same `GtkButton` dropped\ninto a `AdwHeaderBar`\'s `start` or a `AdwToolbarView`\'s `topBar` — where there is no\nYoga tree to join — renders as the bare widget. One symbol, both worlds, no\nflag to remember.',
771
+ text: 'Every `GtkWidget` subclass gtkx binds — 86 of them at last count, from\n`GtkBox` and `GtkButton` to `GtkColumnView` and `GtkEmojiChooser`. The list is\ngenerated, not hand-picked: `scripts/generate-widget-surface.ts` classifies\ngtkx\'s full binding by real GObject inheritance (see\n`scripts/widget-surface/classification.json` for the exact list gtkx binds\ntoday) and `src/gtk/widgets.generated.ts` is the committed result. Re-run the\ngenerator after a gtkx upgrade to pick up new widgets — it diffs against its\nown previous output and prints what changed.\n\nThey keep **every prop gtkx binds** and gain `style` and `onLayout`. Position\nand appearance both come from the style prop, exactly like anywhere else in\nReact Native:\n\n```tsx\n<View style={{ flexDirection: "row", gap: 8, alignItems: "center" }}>\n <GtkEntry\n style={{ flex: 1 }}\n placeholderText="Filter"\n />\n <GtkButton\n style={{ width: 72, backgroundColor: "#3584e4", borderRadius: 6 }}\n label="Go"\n />\n</View>\n```\n\nThe entry flexes, the button takes its width and its colour. The layout half\nof the style drives Yoga; the visual half becomes a GTK CSS class **on the\nwidget itself**, so the button really is blue, not a blue box behind a button.\nSet no size and the widget\'s own natural size wins.\n\n**Outside React Native layout they step aside.** The same `GtkButton` dropped\ninto a `AdwHeaderBar`\'s `start` or a `AdwToolbarView`\'s `topBar` — where there is no\nYoga tree to join — renders as the bare widget. One symbol, both worlds, no\nflag to remember.',
756
772
  },
757
773
  {
758
774
  doc: "docs/platform-layer.md",
759
775
  heading: "Unwrapped by necessity",
760
- text: "Two families of widget are exported **raw** instead of wrapped, because a\nwrapper box around them would be invalid GTK rather than a convenience:\n\n- **toplevels** — `GtkWindow` and everything that derives it: every\n `Gtk*Dialog`, `GtkApplicationWindow`, `GtkAssistant`, `GtkShortcutsWindow`,\n and their Adwaita counterparts (`AdwWindow`, `AdwApplicationWindow`,\n `AdwAboutWindow`, `AdwMessageDialog`, `AdwPreferencesWindow`). A wrapper box\n around a window is not a layout, it is two windows.\n- **child-only widgets** — valid solely as the direct child of one specific\n parent. `GtkListBoxRow` and `GtkFlowBoxChild` (plus everything that derives\n them — every Adwaita preferences row, `AdwActionRow` included) are caught\n mechanically, by real inheritance. `AdwNavigationPage` and\n `AdwPreferencesPage` derive `Gtk.Widget` directly with no shared base to\n catch them mechanically, so they are a two-entry, doc-verified denylist\n instead — see `scripts/widget-surface/classify.ts` for the exact reasoning\n behind each.\n\n`GtkGestureClick` is a third, simpler case: an event controller, not a\nwidget at all, so it was never a candidate for wrapping in the first place.\n\nNothing here is unreachable — every raw export above is still exported,\nby name, from `react-native-gtkx/gtk` or `/adw`, exactly as gtkx binds it.",
776
+ text: "Two families of widget are exported **raw** instead of wrapped, because a\nwrapper box around them would be invalid GTK rather than a convenience:\n\n- **toplevels** — everything that implements `GtkRoot`: `GtkWindow` and\n everything that derives it (every `Gtk*Dialog`, `GtkApplicationWindow`,\n `GtkAssistant`, `GtkShortcutsWindow`, and their Adwaita counterparts\n `AdwWindow`, `AdwApplicationWindow`, `AdwAboutWindow`, `AdwMessageDialog`,\n `AdwPreferencesWindow`) — plus `GtkDragIcon`, which derives `Gtk.Widget`\n directly and is a toplevel all the same. A wrapper box around a window is\n not a layout, it is two windows; and a drag icon inside one is a widget\n GTK cannot present at all. The rule is written against `GtkRoot` (the\n capability: owns its own surface, is presented, never parented) rather\n than against `Gtk.Window` (one familiar instance of it) precisely because\n `GtkDragIcon` is the case a class-shaped rule misses. `GtkPopover` sits on\n the other side of the line — a `GtkNative` but not a `GtkRoot`, and gtkx\n parents it with `gtk_popover_set_parent`, so it stays wrapped. Build a\n drag icon the way GTK does, from the drag itself\n (`Gtk.DragIcon.getForDrag(drag).setChild(…)`), not by mounting one.\n- **child-only widgets** — valid solely as the direct child of one specific\n parent. `GtkListBoxRow` and `GtkFlowBoxChild` (plus everything that derives\n them — every Adwaita preferences row, `AdwActionRow` included) are caught\n mechanically, by real inheritance. `AdwNavigationPage` and\n `AdwPreferencesPage` derive `Gtk.Widget` directly with no shared base to\n catch them mechanically, so they are a two-entry, doc-verified denylist\n instead — see `scripts/widget-surface/classify.ts` for the exact reasoning\n behind each.\n\n`GtkGestureClick` is a third, simpler case: an event controller, not a\nwidget at all, so it was never a candidate for wrapping in the first place.\n\nNothing here is unreachable — every raw export above is still exported,\nby name, from `react-native-gtkx/gtk` or `/adw`, exactly as gtkx binds it.",
761
777
  },
762
778
  {
763
779
  doc: "docs/platform-layer.md",
@@ -767,7 +783,12 @@ export const DOC_CHUNKS = [
767
783
  {
768
784
  doc: "docs/platform-layer.md",
769
785
  heading: "The window and application AppRegistry built",
770
- text: '`useParentWindow` (the `Gtk.Window` ancestor), `useApplication` (the\n`Adw.Application` — `.sendNotification(id, notification)` is the common\nreason to reach it) and `quit` (the same function `AppRegistry` wires to a\nwindow\'s own close button) are re-exported from `react-native-gtkx/gtk`.\nNone of these give you the window or application object ITSELF to build —\n`AppRegistry.runApplication` already did that — they let already-mounted\ncode reach back into it, the same way `useBindSetting` needs a `Gtk.Window`\nto bind a `defaultWidth` property on:\n\n```tsx\nconst window = useParentWindow()\nuseBindSetting({\n schema,\n key: "window-width",\n object: window,\n property: "defaultWidth",\n})\n```',
786
+ text: '`useParentWindow` (the `Gtk.Window` ancestor), `useApplication` (the\n`Adw.Application` — `.sendNotification(id, notification)` is the common\nreason to reach it) and `quit` (the same function `AppRegistry` wires to a\nwindow\'s own close button) are re-exported from `react-native-gtkx/gtk`.\nNone of these give you the window or application object ITSELF to build —\n`AppRegistry.runApplication` already did that — they let already-mounted\ncode reach back into it, the same way `useBindSetting` needs a `Gtk.Window`\nto bind a `defaultWidth` property on:\n\n```tsx\nconst window = useParentWindow()\nuseBindSetting({\n schema,\n key: "window-width",\n object: window,\n property: "defaultWidth",\n})\n```\n\nFor the other half of reaching back into that window — putting things ON it\nrather than reading it — see the next section.',
787
+ },
788
+ {
789
+ doc: "docs/platform-layer.md",
790
+ heading: "Actions and shortcuts declared in the app tree",
791
+ text: '`WindowActions`, `ApplicationActions` and `WindowControllers` register their\nchildren on the window (or the application) **from wherever they are written\nin the app tree**. They render nothing where they sit — they are portals in\nReact\'s own sense: the children stay part of the tree at that position, with\nthe context, state and effects they would have there, while the registration\nlands on the window.\n\n```tsx\nimport { GSimpleAction, WindowActions } from "react-native-gtkx/gtk"\n\nconst NewTaskAction = () => {\n const { addTask } = useStore() // an ordinary React context store\n return (\n <WindowActions>\n <GSimpleAction\n name="new"\n onActivate={() => addTask()}\n />\n </WindowActions>\n )\n}\n```\n\nThat is `win.new` — what a HeaderBar button\'s `actionName`, a `GMenu` item\nand an `actionAccels` entry all target. `ApplicationActions` is the same\ncomponent against the application\'s action map (`app.*`), and the two are\nNOT interchangeable: a `Gio.Notification`\'s action button can only ever\nactivate an application action, and an application action outlives any one\nwindow. Which prefix you get is decided by which component you write.\n`WindowControllers` takes `Gtk.EventController` children — a\n`GtkShortcutController` with `scope={Gtk.ShortcutScope.GLOBAL}` is the whole\nreason it exists.\n\n**Reach for these, not for `runApplication`\'s `applicationActions` /\n`windowActions` / `windowControllers` options.** Those options build their\nchildren as props of the window `AppRegistry` creates, which makes them\nSIBLINGS of the app tree: no provider inside the app is above them, so an\naction declared there cannot read a React context — `examples/tasks-nav` had\nto rewrite its store as a module-level external store before Ctrl+N could\nsee any app state at all. The options still work and are not going away\nwithout notice, but they are deprecated, and everything they can express the\ncomponents can express better:\n\n- **context works**, because the declaration is a descendant of its provider;\n- **registration is dynamic** — the action is added when the declaring\n component mounts and removed when it unmounts, so one screen can own its\n own actions instead of the process owning all of them for its lifetime;\n- **it composes** — two unrelated subtrees each declare their own without\n meeting in a single options object.\n\n`actionAccels` is NOT deprecated and stays a `runApplication` option: it is a\nflat name→keys table with no children and nothing to read from context, and\nit is deliberately process-wide (an accelerator naming an action that is not\nregistered right now simply does nothing). A shortcut that should come and\ngo with a screen is a `GtkShortcutController` inside `<WindowControllers>`.\n\n**Two components, not one, on purpose.** Actions land on the window as a\n`Gio.ActionMap` (`addAction`/`removeAction`, keyed by NAME); controllers land\non it as a `Gtk.Widget` (`addController`/`removeController`, keyed by the\ncontroller object). Different children, different GObject interfaces, and —\nsee below — different duplicate semantics. One component sorting its children\nby type would fail silently on a wrong child; two fail at the type level.\n\n**A duplicated action name goes to the FIRST declaration**, and a second one\nis ignored with a development warning naming it. This is not a coin toss\nbetween first and last. `Gio.ActionMap` is name-keyed at both ends:\n`addAction` silently replaces a same-named action, and `removeAction` takes a\nname, not the action object. Under "last wins", the first of the two\ndeclarations to unmount would remove whatever currently answers to that\nname — leaving the other one mounted but dead. First-wins is the only order\nin which release always precedes acquire: the loser never registers, and when\nthe winner unmounts (removing its own action, correctly) the claim passes to\nthe next declaration still mounted, which registers in a later commit. If you\nwant a screen to override a shortcut, give it its own name, or move the\ndeclaration somewhere both screens can reach.\n\n**Inside a `Modal`, the enclosing window is the modal\'s own window**, so\nactions and controllers declared there belong to it and go away with it —\nusually what a dialog wants, and worth knowing when it is not. Under\n`chrome: "content"` and inside the navigators nothing changes: the window is\nstill the one `AppRegistry` built, the navigators own widgets inside it and\nnot its action map, and a `HeaderBar` button in a page resolves `win.*`\nthrough the widget hierarchy up to that same window. One consequence worth\nknowing: react-navigation keeps a popped screen mounted until its exit\ntransition ends, so a screen\'s actions outlive the pop by the length of the\nanimation.',
771
792
  },
772
793
  {
773
794
  doc: "docs/platform-layer.md",
@@ -787,7 +808,7 @@ export const DOC_CHUNKS = [
787
808
  {
788
809
  doc: "docs/platform-layer.md",
789
810
  heading: "Navigation without a router",
790
- text: 'The stack is an array of tags. Change the array, the widget animates.\n\n```tsx\nimport { useState } from "react"\nimport { Pressable, Text, View } from "react-native"\nimport {\n AdwHeaderBar,\n AdwToolbarView,\n NavigationStack,\n NavigationStackPage,\n SlotContent,\n} from "react-native-gtkx/gtk` and `react-native-gtkx/adw"\n\nconst App = () => {\n const [stack, setStack] = useState(["home"])\n\n return (\n <NavigationStack\n stack={stack}\n // The Adwaita back button, Escape, the back gesture and the\n // back-history menu all arrive here. Follow them in your own state.\n onPopped={(tag) => setStack((s) => s.filter((entry) => entry !== tag))}\n >\n <NavigationStackPage\n tag="home"\n title="Home"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <Pressable onPress={() => setStack((s) => [...s, "detail"])}>\n <Text>Open detail</Text>\n </Pressable>\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n\n <NavigationStackPage\n tag="detail"\n title="Detail"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <View />\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n </NavigationStack>\n )\n}\n```\n\nA runnable version is `examples/adwaita-primitives` — three levels deep, with\nReact Native content in the header bar and a raw `GtkButton` beside it.',
811
+ text: 'The stack is an array of tags. Change the array, the widget animates.\n\n```tsx\nimport { useState } from "react"\nimport { Pressable, Text, View } from "react-native"\nimport { AdwHeaderBar, AdwToolbarView } from "react-native-gtkx/adw"\nimport {\n NavigationStack,\n NavigationStackPage,\n SlotContent,\n} from "react-native-gtkx/common"\n\nconst App = () => {\n const [stack, setStack] = useState(["home"])\n\n return (\n <NavigationStack\n stack={stack}\n // The Adwaita back button, Escape, the back gesture and the\n // back-history menu all arrive here. Follow them in your own state.\n onPopped={(tag) => setStack((s) => s.filter((entry) => entry !== tag))}\n >\n <NavigationStackPage\n tag="home"\n title="Home"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <Pressable onPress={() => setStack((s) => [...s, "detail"])}>\n <Text>Open detail</Text>\n </Pressable>\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n\n <NavigationStackPage\n tag="detail"\n title="Detail"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <View />\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n </NavigationStack>\n )\n}\n```\n\nA runnable version is `examples/adwaita-primitives` — three levels deep, with\nReact Native content in the header bar and a raw `GtkButton` beside it.',
791
812
  },
792
813
  {
793
814
  doc: "docs/platform-layer.md",
@@ -812,7 +833,7 @@ export const DOC_CHUNKS = [
812
833
  {
813
834
  doc: "docs/platform-layer.md",
814
835
  heading: "Wrapping a widget we do not export",
815
- text: 'The generated surface above covers every current `Gtk.Widget`/`Adw.Widget`\nsubclass gtkx binds, but "current" is doing work in that sentence: a gtkx\nrelease can add a widget before this package\'s generator has been re-run for\nit, and non-widget GI classes (an event controller, a filter, an adjustment)\nwere never candidates for the widget surface in the first place even though\na handful of them are occasionally worth putting inside RN layout too.\n`wrapReactNative` is how you reach either without waiting on us — it is\ngeneric, so the widget\'s own props keep their types:\n\n```tsx\nimport { GtkPopover } from "@gtkx/jsx/gtk"\nimport { wrapReactNative } from "react-native-gtkx/gtk` and `react-native-gtkx/adw"\n\nconst Popover = wrapReactNative(GtkPopover)\n// <Popover style={{ width: 240 }} autohide … /> — `autohide` still typed\n```\n\n(`GtkPopover` here is already part of the generated surface — this is the\nsame mechanism `src/gtk/widgets.generated.ts` uses under the hood, just\napplied by hand. It stays useful the day gtkx binds something this package\nhas not regenerated for yet.)\n\nTwo lower-level forms exist for cases the wrapper does not fit:\n\n- `<Widget style={…}>` — wrap an element you already have in hand;\n- `useWidgetLayout(ref, { style })` — attach layout to a widget whose ref you\n own, with no wrapper component at all. Returns the GTK CSS class from the\n style\'s visual half, for you to pass to `cssClasses`.',
836
+ text: 'The generated surface above covers every current `Gtk.Widget`/`Adw.Widget`\nsubclass gtkx binds, but "current" is doing work in that sentence: a gtkx\nrelease can add a widget before this package\'s generator has been re-run for\nit, and non-widget GI classes (an event controller, a filter, an adjustment)\nwere never candidates for the widget surface in the first place even though\na handful of them are occasionally worth putting inside RN layout too.\n`wrapReactNative` is how you reach either without waiting on us — it is\ngeneric, so the widget\'s own props keep their types:\n\n```tsx\nimport { GtkPopover } from "@gtkx/jsx/gtk"\nimport { wrapReactNative } from "react-native-gtkx/common"\n\nconst Popover = wrapReactNative(GtkPopover)\n// <Popover style={{ width: 240 }} autohide … /> — `autohide` still typed\n```\n\n(`GtkPopover` here is already part of the generated surface — this is the\nsame mechanism `src/gtk/widgets.generated.ts` uses under the hood, just\napplied by hand. It stays useful the day gtkx binds something this package\nhas not regenerated for yet.)\n\nTwo lower-level forms exist for cases the wrapper does not fit:\n\n- `<Widget style={…}>` — wrap an element you already have in hand;\n- `useWidgetLayout(ref, { style })` — attach layout to a widget whose ref you\n own, with no wrapper component at all. Returns the GTK CSS class from the\n style\'s visual half, for you to pass to `cssClasses`.',
816
837
  },
817
838
  {
818
839
  doc: "docs/platform-layer.md",
@@ -862,7 +883,7 @@ export const DOC_CHUNKS = [
862
883
  {
863
884
  doc: "docs/getting-started.md",
864
885
  heading: "Shipping an app",
865
- text: "The two paths get you from source to something installable differently,\nbecause they take different positions on what stays out of the bundle.\n\n**vite path** (`gtkx build`): everything except the native GTK addon is\ninlined into one file. `dist/bundle.js` + `dist/gtkx.node` (plus\n`dist/gschemas.compiled` if the app declares a GSettings schema — the\nbundle's own banner points `GSETTINGS_SCHEMA_DIR` at its own directory) is\nthe whole runtime: copy those anywhere with Node ≥24, GTK4 ≥4.20 and\nlibadwaita ≥1.8, and `node bundle.js` runs it. No `node_modules` involved.\n\n**Metro path** (`react-native build-linux`): the release counterpart to\n`run-linux` that iOS, Android and react-native-windows already have and\nthis platform did not until now — it bundles with Metro and stops, instead\nof bundling and immediately running like `run-linux` does:\n\n```bash\nnpx react-native build-linux # writes dist/main.jsbundle\n```\n\nThis is **not** self-contained, unlike the vite path. Metro deliberately\nkeeps `@gtkx/*`, `react` and `yoga-layout` out of the bundle — they have to\nbe the exact instances the Node+GTK host loads, not a second copy Metro\ninlines (see `packages/react-native-gtkx/src/metro/index.ts`,\n`HOST_MODULE_EXTERNALS`, for why). So running `dist/main.jsbundle` needs,\non top of Node ≥24/GTK4/libadwaita, a real `node_modules` with\n`react-native-gtkx` installed and the app's `gtkx.config.ts` present at the\nworking directory:\n\n```bash\nnode node_modules/react-native-gtkx/dist/runner/host.js dist/main.jsbundle\n```\n\n(run from the app root — the config loader reads `gtkx.config.ts` from the\ncurrent directory, exactly like `run-linux` itself). Any ordinary\n`npm install` of the app already has that `node_modules`; the difference\nfrom the vite path only matters when packaging for a machine that never\nran one see `scripts/build-deb.ts`'s Metro branch, which builds that\nclosure itself: a fresh, isolated install of the locally-packed\n`react-native-gtkx` plus `gtkx codegen`, never a copy of a monorepo's own\nhoisted `node_modules` (which would prove nothing about what a real install\nneeds).\n\nThat is the **default** artifact, and it is the only one that carries the\n`node_modules` caveat. `--standalone` below removes it entirely: the same\nMetro build, emitted as one self-contained file that runs on a system Node\nwith nothing installed beside it — the vite path's shape, on the Metro\npath.",
886
+ text: "The two paths get you from source to something installable differently,\nbecause they take different positions on what stays out of the bundle.\n\n**vite path** (`gtkx build`): everything except the native GTK addon is\ninlined into one file. `dist/bundle.js` + `dist/gtkx.node` (plus\n`dist/gschemas.compiled` if the app declares a GSettings schema — the\nbundle's own banner points `GSETTINGS_SCHEMA_DIR` at its own directory) is\nthe whole runtime: copy those anywhere with Node ≥24, GTK4 ≥4.20 and\nlibadwaita ≥1.8, and `node bundle.js` runs it. No `node_modules` involved.\n\n**Metro path** (`react-native build-linux`): the release counterpart to\n`run-linux` that iOS, Android and react-native-windows already have and\nthis platform did not until now — it bundles with Metro and stops, instead\nof bundling and immediately running like `run-linux` does:\n\n```bash\nnpx react-native build-linux # writes dist/main.jsbundle\n```\n\nThis is **not** self-contained, unlike the vite path. Metro deliberately\nkeeps `@gtkx/*`, `react` and `yoga-layout` out of the bundle — they have to\nbe the exact instances the Node+GTK host loads, not a second copy Metro\ninlines (see `packages/react-native-gtkx/src/metro/index.ts`,\n`HOST_MODULE_EXTERNALS`, for why). So running `dist/main.jsbundle` needs,\non top of Node ≥24/GTK4/libadwaita, a real `node_modules` with\n`react-native-gtkx` installed and the app's `gtkx.config.ts` present at the\nworking directory:\n\n```bash\nnode node_modules/react-native-gtkx/dist/runner/host.js dist/main.jsbundle\n```\n\n(run from the app root — the config loader reads `gtkx.config.ts` from the\ncurrent directory, exactly like `run-linux` itself). Any ordinary\n`npm install` of the app already has that `node_modules`, so this is a fine\nway to run a release bundle from a checkout. It is a bad thing to _ship_:\npackaging it means shipping the closure too, and that closure is not the\nhandful of runtime modules it sounds like. Measured on the release that did\nexactly this (v0.2.0-alpha.1's `hn-app` `.deb`): **10,515 files, 206 MiB\ninstalled** to run a 369 KB bundle, because `react-native-gtkx`'s install\ndrags its build toolchain along — `typescript`, `@swc`, `rolldown`,\n`@babel`, `lightningcss`, all of it landing under `/opt` on a user's\nmachine. `scripts/build-deb.ts` no longer packages this shape at all.\n\nThat is the **default** artifact, and it is the only one that carries the\n`node_modules` caveat. `--standalone` below removes it entirely: the same\nMetro build, emitted as one self-contained file that runs on a system Node\nwith nothing installed beside it — the vite path's shape, on the Metro\npath. It is what the release `.deb` ships.",
866
887
  },
867
888
  {
868
889
  doc: "docs/getting-started.md",
@@ -877,7 +898,7 @@ export const DOC_CHUNKS = [
877
898
  {
878
899
  doc: "docs/getting-started.md",
879
900
  heading: "Tests",
880
- text: 'Unit logic is plain vitest — no special setup, runs anywhere. Component\ntests render real GTK widgets under a headless Wayland compositor, and\nreact-native-gtkx ships the whole recipe as two subpaths so a consumer app\ndoes not have to rediscover it:\n\n- `react-native-gtkx/vitest` — `reactNativeGtkxTest()`, a ready Vitest\n project config: the headless-compositor plugin, the `react-native` alias\n and Metro-style platform extensions, an inline-deps default for RN\n libraries that import `react-native` themselves (`@react-navigation`),\n and the React act-environment setup;\n- `react-native-gtkx/testing` — re-exports `@gtkx/testing`\'s\n render/screen/userEvent/fireEvent surface (already RN-shaped: `getByText`\n finds a `Text`, `userEvent.click` walks up to a `Pressable`\'s gesture\n controller — no wrapper needed) plus `renderHookWithWindow`, for hooks\n that read the active window (`useWindowDimensions` and similar) —\n `renderHook` alone mounts into a windowless container.\n\nMinimal `vitest.config.ts`:\n\n```ts\nimport { reactNativeGtkxTest } from "react-native-gtkx/vitest"\nimport { defineConfig } from "vitest/config"\n\nexport default defineConfig(reactNativeGtkxTest())\n```\n\nThe default test glob is `**/*.gtk.test.{ts,tsx}`; override `include` (and\n`name`, `headless`, `platform`, `inlineDeps`, `setupFiles`,\n`fileParallelism`) through `reactNativeGtkxTest`\'s options. For a project\nthat also has portable unit tests, use the result as one entry of\n`test.projects` instead of the whole config — `vitest.config.ts` at this\nrepo\'s root is the reference (`process.platform === "linux"` guards the\ngtk project so `npm test` still works on a non-Linux dev machine, running\nonly the unit project there).\n\n```tsx\nimport { Root } from "react-native"\nimport { render, screen } from "react-native-gtkx/testing"\nimport { expect, it } from "vitest"\nimport { App } from "../src/App"\n\nit("renders the greeting", async () => {\n // react-native-gtkx components need a layout root — AppRegistry.runApplication()\n // in the real app, <Root> in a test.\n await render(\n <Root\n width={800}\n height={600}\n >\n <App />\n </Root>,\n )\n expect(screen.getByText("Hello, GNOME!")).toBeTruthy()\n})\n```\n\nRequirements: a headless Wayland compositor and D-Bus on PATH — the same\nsystem packages CI installs, `sway xwayland dbus` (Ubuntu:\n`apt install sway xwayland dbus`). A missing compositor fails a test run\nwith a readable error (`Cannot find the "sway" executable on PATH`) rather\nthan hanging. `gtkx codegen` must already have generated the project\'s\n`@gtkx/gi` bindings before the first test run — a bare `vitest run` does\nnot trigger codegen itself, unlike `gtkx dev`/`gtkx build`; the template\'s\nown `package.json` wires this as a `pretest` script.\n\n`packages/react-native-gtkx/tests/gtk/` is this repo\'s own suite, built on\nthe same `@gtkx/testing` surface directly (it tests source, not the\npublished package) — a good place to see more query and `userEvent`\npatterns in context. Query roles with `Gtk.AccessibleRole` enums (see\ndocs/gtkx-rc2-notes.md for the live workarounds still baked into that\nrecipe).',
901
+ text: 'Unit logic is plain vitest — no special setup, runs anywhere. Component\ntests render real GTK widgets under a headless Wayland compositor, and\nreact-native-gtkx ships the whole recipe as two subpaths so a consumer app\ndoes not have to rediscover it:\n\n- `react-native-gtkx/vitest` — `reactNativeGtkxTest()`, a ready Vitest\n project config: the headless-compositor plugin, the `react-native` alias\n and Metro-style platform extensions, an inline-deps default for RN\n libraries that import `react-native` themselves (`@react-navigation`),\n and the React act-environment setup;\n- `react-native-gtkx/testing` — re-exports `@gtkx/testing`\'s\n render/screen/userEvent/fireEvent surface (already RN-shaped: `getByText`\n finds a `Text`, `userEvent.click` walks up to a `Pressable`\'s gesture\n controller — no wrapper needed) plus `renderHookWithWindow`, for hooks\n that read the active window (`useWindowDimensions` and similar) —\n `renderHook` alone mounts into a windowless container.\n\nMinimal `vitest.config.ts`:\n\n```ts\nimport { reactNativeGtkxTest } from "react-native-gtkx/vitest"\nimport { defineConfig } from "vitest/config"\n\nexport default defineConfig(reactNativeGtkxTest())\n```\n\nThe default test glob is `**/*.gtk.test.{ts,tsx}`; override `include` (and\n`name`, `headless`, `platform`, `inlineDeps`, `setupFiles`,\n`fileParallelism`) through `reactNativeGtkxTest`\'s options. For a project\nthat also has portable unit tests, use the result as one entry of\n`test.projects` instead of the whole config — `vitest.config.ts` at this\nrepo\'s root is the reference (`process.platform === "linux"` guards the\ngtk project so `npm test` still works on a non-Linux dev machine, running\nonly the unit project there).\n\n```tsx\nimport { Root } from "react-native"\nimport { render, screen } from "react-native-gtkx/testing"\nimport { expect, it } from "vitest"\nimport { App } from "../src/App"\n\nit("renders the greeting", async () => {\n // react-native-gtkx components need a layout root — AppRegistry.runApplication()\n // in the real app, <Root> in a test.\n await render(\n <Root\n width={800}\n height={600}\n >\n <App />\n </Root>,\n )\n expect(screen.getByText("Hello, GNOME!")).toBeTruthy()\n})\n```\n\nRequirements: a headless Wayland compositor and D-Bus on PATH — the same\nsystem packages CI installs, `sway xwayland dbus` (Ubuntu:\n`apt install sway xwayland dbus`). A missing compositor fails a test run\nwith a readable error (`Cannot find the "sway" executable on PATH`) rather\nthan hanging. `gtkx codegen` must already have generated the project\'s\n`@gtkx/gi` bindings before the first test run — a bare `vitest run` does\nnot trigger codegen itself, unlike `gtkx dev`/`gtkx build`; the template\'s\nown `package.json` wires this as a `pretest` script.\n\n`packages/react-native-gtkx/tests/gtk/` is this repo\'s own suite, built on\nthe same `@gtkx/testing` surface directly (it tests source, not the\npublished package) — a good place to see more query and `userEvent`\npatterns in context. Query roles with `Gtk.AccessibleRole` enums (see\ndocs/gtkx-rc3-notes.md for the live workarounds still baked into that\nrecipe).',
881
902
  },
882
903
  {
883
904
  doc: "docs/getting-started.md",
@@ -887,32 +908,42 @@ export const DOC_CHUNKS = [
887
908
  {
888
909
  doc: "docs/getting-started.md",
889
910
  heading: "Next steps",
890
- text: "- [docs/api.md](api.md) — the entire v1 surface and differences from RN;\n- [CONTRIBUTING.md](../CONTRIBUTING.md) — developing the library itself (from macOS — via the UTM VM);\n- [docs/gtkx-rc2-notes.md](gtkx-rc2-notes.md) — the gtkx rc.2 baseline: workarounds, what it fixed, quirks that stay.",
911
+ text: "- [docs/api.md](api.md) — the entire v1 surface and differences from RN;\n- [CONTRIBUTING.md](../CONTRIBUTING.md) — developing the library itself (from macOS — via the UTM VM);\n- [docs/gtkx-rc3-notes.md](gtkx-rc3-notes.md) — the gtkx rc.3 baseline: workarounds, what it fixed, quirks that stay.",
891
912
  },
892
913
  {
893
- doc: "docs/gtkx-rc2-notes.md",
914
+ doc: "docs/gtkx-rc3-notes.md",
894
915
  heading: "Live workarounds",
895
- text: '| Name | What rc.2 does | Our workaround | Removal condition |\n| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |\n| `use-signal-stale-handler` | `useSignal` routes the handler through React\'s `useEffectEvent`; `react-reconciler@0.33.0` only refreshes it in `commitBeforeMutationEffects` for `case 0` (FunctionComponent) — `case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fall through unrefreshed, so any `useEffectEvent` in a `memo`/`forwardRef` component is pinned to its mount closure forever (our `ScrollView` is a `forwardRef` with the `useSignal` calls inside it — confirmed upstream, gtkx-org/gtkx#467) — a fetch-fed FlatList empties itself on the first scroll | `gtkx/bridge/use-signal.ts` re-pins the latest handler (insertion effect) and hands gtkx a stable wrapper; the bridge exports that hook, not gtkx\'s | A stable React 19.3 (React fixed the refresh on the 19.3 line; no stable gtkx 0.34.x yet) |\n| `runtime-dedupe` | Two bundled copies of the gtkx runtime still double-init GLib and abort (`g_log_set_writer_func` called twice); nothing guards against it | `src/vite/index.ts` puts `resolve.dedupe` over `@gtkx/*` + `react` (+ `@react-navigation/*` for its context) into the preset every app inherits | Idempotent runtime init upstream, or an error that names the duplicate |\n| `renderhook-no-window` | `renderHook` still mounts into a bare `Gtk.Box`, so window-dependent APIs have no toplevel to read | Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) | `renderHook` mounts into the same harness window `render` uses |\n| `graphene-rect-nested-boxed-props` | `new Graphene.Rect({ origin: new Graphene.Point(...), size: new Graphene.Size(...) })` hits the same native "Expected an Object for Boxed field write type, got Object" as the `gsk-colorstop-boxed-write` row below a boxed struct\'s constructor writing another boxed value into one of its own fields | `gtkx/bridge/svg-node.ts` builds the clip rect through `Graphene.Rect.alloc().init(x, y, w, h)` instead a working escape hatch `Gsk.ColorStop` does not have | Upstream fixes boxed-struct fields that are themselves another boxed type |\n| `gsk-colorstop-boxed-write` | Constructing a `Gsk.ColorStop` (an inline `{ float offset; GdkRGBA color; }` boxed struct) crashes in the native addon writing the `color` field "Expected an Object for Boxed field write type, got Object". Verified through three independent paths (constructor props, the property setter, and skipping `ColorStop` for a plain `{offset, color}` object, which fails differently with "No native handle associated with Object" the array marshaling genuinely needs a native-backed instance per element) | `gtkx/bridge/svg-node.ts`\'s `makeColorStop` catches the throw and returns `null`; a gradient with zero constructible stops paints nothing for that fill/stroke instead of crashing (the same path as an unresolved `url(#missing)` reference) — SVG `<LinearGradient>`/`<RadialGradient>` ship with this degradation, not cut from the API | Upstream fixes boxed-struct fields that are themselves another boxed type (nested embed, not a pointer) |',
916
+ text: "| Name | What rc.3 does | Our workaround | Removal condition |\n| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| `use-signal-stale-handler` | `useSignal` routes the handler through React's `useEffectEvent`; `react-reconciler@0.33.0` only refreshes it in `commitBeforeMutationEffects` for `case 0` (FunctionComponent) — `case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fall through unrefreshed, so any `useEffectEvent` in a `memo`/`forwardRef` component is pinned to its mount closure forever (our `ScrollView` is a `forwardRef` with the `useSignal` calls inside it) — a fetch-fed FlatList empties itself on the first scroll. **Unchanged in rc.3, deliberately** — see below | `gtkx/bridge/use-signal.ts` re-pins the latest handler (insertion effect) and hands gtkx a stable wrapper; the bridge exports that hook, not gtkx's | A stable React 19.3 (React fixed the refresh on the 19.3 line) |\n| `runtime-dedupe` | Two bundled copies of the gtkx runtime still double-init GLib and abort (`g_log_set_writer_func` called twice); nothing guards against it | `src/vite/index.ts` puts `resolve.dedupe` over `@gtkx/*` + `react` (+ `@react-navigation/*` for its context) into the preset every app inherits | Idempotent runtime init upstream, or an error that names the duplicate |\n| `prop-portal` | `createPortal(children, container)` can only target a container's DEFAULT slot (\"children\"). Every other slot an object exposes declaratively a window's `Gio.ActionMap` (`actions`), a widget's `controllers`, an `AdwApplicationWindow`'s `breakpoints` is reached only by passing an element-valued PROP, which the reconciler routes through an internal `\"gtkx:prop\"` element. That element is exported from neither `@gtkx/react` nor its `/internal` subpath, so there is no supported way to portal into a named slot | `gtkx/bridge/slot-portal.ts` restates the `\"gtkx:prop\"` element name and wraps it in `createSlotPortal(children, target, slot)` — the one line that would move if gtkx renames it. `WindowActions`/`ApplicationActions`/`WindowControllers` are built on it | gtkx exports a slot-aware portal (or the prop element) from a public entry point |\n| `renderhook-no-window` | `renderHook` still mounts into a bare `Gtk.Box` rc.3's `render-hook.js` is byte-identical to rc.2's so window-dependent APIs have no toplevel to read | Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) | `renderHook` mounts into the same harness window `render` uses |",
917
+ },
918
+ {
919
+ doc: "docs/gtkx-rc3-notes.md",
920
+ heading: "`use-signal-stale-handler` is a decision upstream made, not an oversight",
921
+ text: "We shipped the fix as a PR (gtkx-org/gtkx#469) and it was **closed unmerged\non purpose**. @eugeniodepalo: _\"closing this in favour of waiting for\nupstream… Since React fixes this properly on 19.3 for all fiber tags, I'd\nrather take the version bump than carry a workaround I'd revert.\"_ So this\nrow does not move on any gtkx release — only a stable React 19.3 retires it,\nand the hazard is wider than `useSignal`: any hook built on `useEffectEvent`\ninherits it until then.",
922
+ },
923
+ {
924
+ doc: "docs/gtkx-rc3-notes.md",
925
+ heading: "Fixed in rc.3",
926
+ text: "- **`gsk-colorstop-boxed-write`** — constructing a `Gsk.ColorStop` threw\n `Expected an Object for Boxed field write type, got Object` in the native\n addon, so SVG `<LinearGradient>`/`<RadialGradient>` had zero constructible\n stops and painted nothing. **We fixed this upstream ourselves**\n (gtkx-org/gtkx#473, closing gtkx-org/gtkx#472): a record field write\n converts through `toNative` now, the counterpart of the `fromNative` its\n getter already used. `makeColorStop`'s try/catch and the null-filtering it\n forced through `collectStops`/`appendGradient` are gone; gradients paint for\n real, asserted on the stop colors that actually reach Gsk in\n `tests/gtk/components/svg.gtk.test.tsx`.\n- **`graphene-rect-nested-boxed-props`** — the same native bug reached through\n `new Graphene.Rect({ origin, size })`; we had been building the SVG clip\n rect through `Graphene.Rect.alloc().init(x, y, w, h)`, the escape hatch\n `Gsk.ColorStop` did not have. Same upstream fix, so `svg-node.ts` uses the\n plain constructor again.\n- **The codegen freshness lie** — `npm install` prunes `node_modules/.gtkx`,\n and rc.2's `@gtkx/cli` could report \"bindings up to date\" over a store that\n was not there. Fixed upstream in gtkx-org/gtkx#470 (also ours): both stores'\n manifests and self-links are checked now. Separately, `src/runner` calls the\n programmatic `@gtkx/codegen` API rather than the CLI, so a library\n generating bindings on a consumer's behalf has no cwd or stamp to misread.",
896
927
  },
897
928
  {
898
- doc: "docs/gtkx-rc2-notes.md",
899
- heading: "Fixed in rc.2 (rc.1 history, one line each)",
900
- text: "- **`vitest-compositor`** — rc.1 defaulted the headless display to weston and\n took sway through an option; rc.2's default IS sway, so `vitest.config.ts`\n calls the plugin with no arguments.\n- **`no-virtual-seat`** — rc.1 had no input seat under sway, so windows never\n activated and `userEvent` was impossible; rc.2 starts a virtual seat for sway\n (`needsVirtualSeat: true`), a rendered toplevel now reports `is-active: true`,\n and coordinate-level input is on the table.\n- **`fixed-layout-child`** — rc.1's declarative `<GtkFixedLayoutChild>` created\n a detached object (Gtk-CRITICAL, positions never applied); moot for us since\n containers moved to our own `RnGtkxLayout` manager and GtkFixed left the\n codebase entirely.\n- **`controllers-as-children`** — rc.1 silently ignored controllers passed as\n JSX; rc.2 has a `controllers` slot on `GtkWidget`. Pressable and TextInput\n still attach theirs imperatively on purpose (wired once per widget, handlers\n read from a ref) — a choice now, not a workaround.",
929
+ doc: "docs/gtkx-rc3-notes.md",
930
+ heading: "Fixed in rc.2 (history, one line each)",
931
+ text: "- **`vitest-compositor`** — rc.1 defaulted the headless display to weston;\n rc.2's default IS sway, so `vitest.config.ts` calls the plugin with no\n arguments.\n- **`no-virtual-seat`** — rc.1 had no input seat under sway, so windows never\n activated and `userEvent` was impossible; rc.2 starts a virtual seat.\n- **`fixed-layout-child`** — rc.1's declarative `<GtkFixedLayoutChild>`\n created a detached object; moot since containers moved to our own\n `RnGtkxLayout` manager and GtkFixed left the codebase.\n- **`controllers-as-children`** — rc.1 silently ignored controllers passed as\n JSX; rc.2 has a `controllers` slot on `GtkWidget`. Pressable and TextInput\n still attach theirs imperatively on purpose — a choice now, not a\n workaround.",
901
932
  },
902
933
  {
903
- doc: "docs/gtkx-rc2-notes.md",
904
- heading: "New in the rc.2 era",
905
- text: "Two regressions/gaps first seen on rc.2, both with reproductions and both\nwritten up for upstream in [docs/upstream-gtkx.md](upstream-gtkx.md):\n\n- **The `useSignal` freeze.** Not a `useSignal` bug and not about tree depth:\n `react-reconciler@0.33.0` refreshes `useEffectEvent` in\n `commitBeforeMutationEffects` only for `case 0` (FunctionComponent) —\n `case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fall through\n unrefreshed, so any `useEffectEvent` inside a `memo`/`forwardRef` component\n is pinned to its mount closure permanently (confirmed upstream,\n gtkx-org/gtkx#467). It reproduced for us because our `ScrollView` is a\n `forwardRef` with the `useSignal` calls inside it; simple, shallow\n components refresh correctly, which is why it survives casual testing. The\n visible symptom was a virtualized list that blanked on the first scroll.\n Repro: `tests/gtk/components/list-late-data.gtk.test.tsx`, plus the contract\n test in `tests/gtk/bridge.smoke.test.tsx`.\n- **The codegen freshness lieresolved.** `npm install` prunes\n `node_modules/.gtkx` (npm sees `@gtkx/gi`/`@gtkx/jsx` as extraneous), and on\n rc.2 `@gtkx/cli`'s codegen could report \"bindings up to date\" over a store\n that was not there; fixed upstream in gtkx-org/gtkx#470 (the freshness check\n now verifies both stores' manifests and self-links, not just one). Separately,\n we were never supposed to be exposed to this: `@gtkx/cli` is meant for apps,\n not libraries generating bindings on a consumer's behalf, so `src/runner`\n now calls the programmatic `@gtkx/codegen` API directly (see\n `docs/upstream-gtkx.md` bug 2) no CLI subprocess, no cwd, no stamp to\n misread. `rm -rf node_modules/.gtkx` before `npm run codegen` at the repo\n root is still the right sequence for our own monorepo tooling, which still\n runs the CLI.",
934
+ doc: "docs/gtkx-rc3-notes.md",
935
+ heading: "Behaviour rc.3 changed under us",
936
+ text: "None of these needed a code change, but they change what the stack does\nbeneath us and are worth knowing before debugging something odd:\n\n- **Blockable signals are no longer suppressed for a whole React commit.**\n rc.2 suppressed `onChanged`/`onToggled`/`onNotify*`/`onSelectionChanged`\n from `prepareForCommit` until after `resetAfterCommit`; rc.3 wraps each\n framework write individually, so an emission the framework did not cause —\n one raised from a `useLayoutEffect`, or aimed at another `createRoot` tree —\n now reaches its handler. Our navigators lean on exactly this class of\n signal; the full suite is unchanged (825 passed + 1 expected fail, same as\n on rc.2), so nothing of ours was relying on the over-broad suppression.\n- **`render`'s harness window is undecorated.** rc.2 gave it a `Gtk.HeaderBar`\n whose window handle, center box and title label sat in the accessibility\n tree competing with the widgets under test. Role queries now see only what\n the test rendered.\n- **A widget with `accessibleLabelledBy` reports the relation as its\n accessible name**, ahead of its own textthe precedence ARIA defines.\n Changes what `getByRole(role, { name })` matches.\n- **`toHaveTextContent` no longer falls back to the accessible name** (we have\n no uses of it); **`toHaveDisplayValue` throws** on a widget without one\n instead of comparing to `null`; **checked state is tri-state**.\n- **Records are constructible only when their bytes can be copied**, a new\n restriction that could have taken `Gsk.ColorStop` away with one hand while\n the fix above gave it with the other. Verified on the VM that neither\n `Gsk.ColorStop` nor `Graphene.Rect` is caught by it both construct from\n props which is what makes the two deletions above real rather than paper.\n- **Single-child widgets lost their `content`/`child` props** (pass the widget\n as a child instead). No JSX in this repo used either, and typecheck against\n the regenerated bindings is clean, so this cost us nothing — but an app\n built on the raw gtkx surface will feel it.",
906
937
  },
907
938
  {
908
- doc: "docs/gtkx-rc2-notes.md",
939
+ doc: "docs/gtkx-rc3-notes.md",
909
940
  heading: "Non-workarounds (quirks that stay)",
910
- text: '- 64-bit FFI values arrive as BigInt → `toNumber()` at the boundary\n (`gtkx/bridge/measure.ts`);\n- signal names are kebab-case ("value-changed"); signals do not pass the\n emitter (get the widget from a ref);\n- role queries in tests use the `Gtk.AccessibleRole` enum, not strings;\n- `npm install` prunes the codegen store (`node_modules/.gtkx` is not in the\n lockfile) → run `npm run codegen` after installing — npm behavior, not gtkx;\n- measuring unmapped widgets yields 0 (offscreen Label probes are the\n exception) → re-measure on the `map` signal + re-commit measured leaves on\n every flush (`layout/node.ts`);\n- mixed-session setups only: running an app on a bare compositor (headless\n sway) while `XDG_RUNTIME_DIR` points at a full GNOME session can segfault in\n a GTK signal handler when the GNOME settings portal pushes updates into the\n app (`g_cclosure_marshal_VOID__OBJECTv` via the FFI emit path); cutting\n `DBUS_SESSION_BUS_ADDRESS` avoids it, which is why the headless scripts do.\n Normal desktop and container runs are unaffected. Retested on rc.2 (gallery\n under headless sway with the real session bus attached): the app ran clean\n and SIGTERM teardown exited 143, so the exit-time segfault we saw on rc.1 no\n longer reproduces; the portal-push crash needs a live settings change to\n trigger and stays on the list unconfirmed.',
941
+ text: '- 64-bit FFI values arrive as BigInt → `toNumber()` at the boundary\n (`gtkx/bridge/measure.ts`);\n- signal names are kebab-case ("value-changed"); signals do not pass the\n emitter (get the widget from a ref);\n- role queries in tests use the `Gtk.AccessibleRole` enum, not strings;\n- `npm install` prunes the codegen store (`node_modules/.gtkx` is not in the\n lockfile) → run `npm run codegen` after installing — npm behavior, not gtkx;\n- measuring unmapped widgets yields 0 (offscreen Label probes are the\n exception) → re-measure on the `map` signal + re-commit measured leaves on\n every flush (`layout/node.ts`);\n- mixed-session setups only: running an app on a bare compositor (headless\n sway) while `XDG_RUNTIME_DIR` points at a full GNOME session can segfault in\n a GTK signal handler when the GNOME settings portal pushes updates into the\n app (`g_cclosure_marshal_VOID__OBJECTv` via the FFI emit path); cutting\n `DBUS_SESSION_BUS_ADDRESS` avoids it, which is why the headless scripts do.\n Normal desktop and container runs are unaffected. The exit-time segfault\n seen on rc.1 no longer reproduces; the portal-push crash needs a live\n settings change to trigger and stays on the list unconfirmed.',
911
942
  },
912
943
  {
913
- doc: "docs/gtkx-rc2-notes.md",
944
+ doc: "docs/gtkx-rc3-notes.md",
914
945
  heading: "Procedure when the next release ships",
915
- text: "1. Update the `@gtkx/*` pins (root, spike, examples, template), then\n `npm install && rm -rf node_modules/.gtkx && npm run codegen`;\n2. Run everything on Linux: `npm run typecheck && npm test`, `build:dist`,\n `check:package`, plus the headless example proofs;\n3. Walk the live-workaround table: for each row check the removal condition,\n delete the tag and the row together when it is met, and move the entry into\n the history section above;\n4. Re-tag whatever survives (`RC2-WORKAROUND` → the new release) and update\n `docs/upstream-gtkx.md` if an ask was answered.",
946
+ text: "1. Update the `@gtkx/*` pins (root, spike, examples, template), then\n `npm install && rm -rf node_modules/.gtkx && npm run codegen`;\n2. Run everything on Linux: `npm run typecheck && npm test`, `build:dist`,\n `check:package`, plus the headless example proofs;\n3. Walk the live-workaround table: for each row check the removal condition,\n delete the tag and the row together when it is met, and move the entry into\n the history section above — **with a probe that proves the fix on the real\n runtime**, not just the release notes claiming it;\n4. Re-tag whatever survives (`RC3-WORKAROUND` → the new release), rename this\n file to match the new pin, and update `docs/upstream-gtkx.md` if an ask was\n answered.",
916
947
  },
917
948
  {
918
949
  doc: "docs/research/navigation-extensibility.md",
@@ -940,34 +971,29 @@ export const DOC_CHUNKS = [
940
971
  text: "No other desktop React Native platform has native navigation integration:\n\n- **react-native-screens** lists Windows support, but it is a thin\n old-architecture module; native-stack on modern react-native-windows\n fails, because screens has no new-architecture Windows implementation and\n RNW 0.82 removed the old one. Microsoft's own react-native-gallery falls\n back to the JS drawer.\n- **react-native-macos** is not supported by react-native-screens at all —\n AppKit has no navigation-stack primitive to bind to.\n\nOur path — a real `Adw.NavigationView` driven from a custom navigator, with\nreact-navigation state as the source of truth and native pops reported back\ninto it — is structurally the iOS native-stack / `UINavigationController`\nmodel, which neither desktop platform reached. GTK's advantage is that the\nprimitive exists at all: back button, Escape, back gesture, history menu and\ntransitions ship with the widget.\n\nThe trade: a JS stack can render anything into its fake header, while our\nchrome is real and had to be opened up deliberately — which is what the\nintrinsic-size root does.\n\nSources: react-native-screens README and discussions #1575 / #2541, RNW\ndiscussions #14273 / issue #4152 / new-architecture docs / 0.82 release\npost, microsoft/react-native-gallery, reactnavigation.org native-stack docs.",
941
972
  },
942
973
  {
943
- doc: "docs/gtkx-rc2-notes.md",
944
- heading: "RC2-WORKAROUND(use-signal-stale-handler)",
945
- text: "`useSignal` routes the handler through React's `useEffectEvent`; `react-reconciler@0.33.0` only refreshes it in `commitBeforeMutationEffects` for `case 0` (FunctionComponent) — `case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fall through unrefreshed, so any `useEffectEvent` in a `memo`/`forwardRef` component is pinned to its mount closure forever (our `ScrollView` is a `forwardRef` with the `useSignal` calls inside it — confirmed upstream, gtkx-org/gtkx#467) — a fetch-fed FlatList empties itself on the first scroll — our workaround: `gtkx/bridge/use-signal.ts` re-pins the latest handler (insertion effect) and hands gtkx a stable wrapper; the bridge exports that hook, not gtkx's — removed when: A stable React 19.3 (React fixed the refresh on the 19.3 line; no stable gtkx 0.34.x yet)",
974
+ doc: "docs/gtkx-rc3-notes.md",
975
+ heading: "RC3-WORKAROUND(use-signal-stale-handler)",
976
+ text: "`useSignal` routes the handler through React's `useEffectEvent`; `react-reconciler@0.33.0` only refreshes it in `commitBeforeMutationEffects` for `case 0` (FunctionComponent) — `case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fall through unrefreshed, so any `useEffectEvent` in a `memo`/`forwardRef` component is pinned to its mount closure forever (our `ScrollView` is a `forwardRef` with the `useSignal` calls inside it) — a fetch-fed FlatList empties itself on the first scroll. **Unchanged in rc.3, deliberately** see below — our workaround: `gtkx/bridge/use-signal.ts` re-pins the latest handler (insertion effect) and hands gtkx a stable wrapper; the bridge exports that hook, not gtkx's — removed when: A stable React 19.3 (React fixed the refresh on the 19.3 line)",
946
977
  },
947
978
  {
948
- doc: "docs/gtkx-rc2-notes.md",
949
- heading: "RC2-WORKAROUND(runtime-dedupe)",
979
+ doc: "docs/gtkx-rc3-notes.md",
980
+ heading: "RC3-WORKAROUND(runtime-dedupe)",
950
981
  text: "Two bundled copies of the gtkx runtime still double-init GLib and abort (`g_log_set_writer_func` called twice); nothing guards against it — our workaround: `src/vite/index.ts` puts `resolve.dedupe` over `@gtkx/*` + `react` (+ `@react-navigation/*` for its context) into the preset every app inherits — removed when: Idempotent runtime init upstream, or an error that names the duplicate",
951
982
  },
952
983
  {
953
- doc: "docs/gtkx-rc2-notes.md",
954
- heading: "RC2-WORKAROUND(renderhook-no-window)",
955
- text: "`renderHook` still mounts into a bare `Gtk.Box`, so window-dependent APIs have no toplevel to read our workaround: Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) — removed when: `renderHook` mounts into the same harness window `render` uses",
984
+ doc: "docs/gtkx-rc3-notes.md",
985
+ heading: "RC3-WORKAROUND(prop-portal)",
986
+ text: '`createPortal(children, container)` can only target a container\'s DEFAULT slot ("children"). Every other slot an object exposes declaratively — a window\'s `Gio.ActionMap` (`actions`), a widget\'s `controllers`, an `AdwApplicationWindow`\'s `breakpoints` — is reached only by passing an element-valued PROP, which the reconciler routes through an internal `"gtkx:prop"` element. That element is exported from neither `@gtkx/react` nor its `/internal` subpath, so there is no supported way to portal into a named slot — our workaround: `gtkx/bridge/slot-portal.ts` restates the `"gtkx:prop"` element name and wraps it in `createSlotPortal(children, target, slot)` — the one line that would move if gtkx renames it. `WindowActions`/`ApplicationActions`/`WindowControllers` are built on it — removed when: gtkx exports a slot-aware portal (or the prop element) from a public entry point',
956
987
  },
957
988
  {
958
- doc: "docs/gtkx-rc2-notes.md",
959
- heading: "RC2-WORKAROUND(graphene-rect-nested-boxed-props)",
960
- text: '`new Graphene.Rect({ origin: new Graphene.Point(...), size: new Graphene.Size(...) })` hits the same native "Expected an Object for Boxed field write type, got Object" as the `gsk-colorstop-boxed-write` row below a boxed struct\'s constructor writing another boxed value into one of its own fields — our workaround: `gtkx/bridge/svg-node.ts` builds the clip rect through `Graphene.Rect.alloc().init(x, y, w, h)` instead a working escape hatch `Gsk.ColorStop` does not have — removed when: Upstream fixes boxed-struct fields that are themselves another boxed type',
961
- },
962
- {
963
- doc: "docs/gtkx-rc2-notes.md",
964
- heading: "RC2-WORKAROUND(gsk-colorstop-boxed-write)",
965
- text: 'Constructing a `Gsk.ColorStop` (an inline `{ float offset; GdkRGBA color; }` boxed struct) crashes in the native addon writing the `color` field — "Expected an Object for Boxed field write type, got Object". Verified through three independent paths (constructor props, the property setter, and skipping `ColorStop` for a plain `{offset, color}` object, which fails differently with "No native handle associated with Object" — the array marshaling genuinely needs a native-backed instance per element) — our workaround: `gtkx/bridge/svg-node.ts`\'s `makeColorStop` catches the throw and returns `null`; a gradient with zero constructible stops paints nothing for that fill/stroke instead of crashing (the same path as an unresolved `url(#missing)` reference) — SVG `<LinearGradient>`/`<RadialGradient>` ship with this degradation, not cut from the API — removed when: Upstream fixes boxed-struct fields that are themselves another boxed type (nested embed, not a pointer)',
989
+ doc: "docs/gtkx-rc3-notes.md",
990
+ heading: "RC3-WORKAROUND(renderhook-no-window)",
991
+ text: "`renderHook` still mounts into a bare `Gtk.Box` rc.3's `render-hook.js` is byte-identical to rc.2's so window-dependent APIs have no toplevel to read — our workaround: Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) — removed when: `renderHook` mounts into the same harness window `render` uses",
966
992
  },
967
993
  {
968
994
  doc: "docs/api.md",
969
995
  heading: "View",
970
- text: "View — GTK implementation: GtkBox (RnGtkxViewBox). Supported: `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins). Differences from RN: nesting another pointerEvents inside a box-only view is not supported",
996
+ text: "View — GTK implementation: GtkBox (RnGtkxViewBox). Supported: `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here. Differences from RN: nesting another pointerEvents inside a box-only view is not supported; responder negotiation is capture-then-bubble as in RN, but there is **no transfer yet**: once a view is granted, `onResponderTerminationRequest`/`onResponderReject` are never called and a second view cannot steal (GTK's `CLAIMED` is irrevocable, so a transfer could not be enforced against native widgets — see docs/research/gestures.md). The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one",
971
997
  },
972
998
  {
973
999
  doc: "docs/api.md",
@@ -987,7 +1013,7 @@ export const DOC_CHUNKS = [
987
1013
  {
988
1014
  doc: "docs/api.md",
989
1015
  heading: "Pressable",
990
- text: "Pressable — GTK implementation: GtkFixed + GestureClick/Motion. Supported: `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered}`. Differences from RN: —",
1016
+ text: "Pressable — GTK implementation: GtkFixed + GestureClick/Motion. Supported: `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered}`; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` — a desktop pointer is one fabricated touch). Differences from RN: —",
991
1017
  },
992
1018
  {
993
1019
  doc: "docs/api.md",
@@ -997,7 +1023,7 @@ export const DOC_CHUNKS = [
997
1023
  {
998
1024
  doc: "docs/api.md",
999
1025
  heading: "ScrollView",
1000
- text: "ScrollView — GTK implementation: GtkScrolledWindow. Supported: vertical/`horizontal`, `contentContainerStyle`, `onScroll`, `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` (`ScrollViewHandle`). Differences from RN: `animated` in scrollTo is ignored",
1026
+ text: "ScrollView — GTK implementation: GtkScrolledWindow. Supported: vertical/`horizontal`, `contentContainerStyle`, `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` (`ScrollViewHandle`). Differences from RN: `animated` in scrollTo is ignored",
1001
1027
  },
1002
1028
  {
1003
1029
  doc: "docs/api.md",
@@ -1027,7 +1053,7 @@ export const DOC_CHUNKS = [
1027
1053
  {
1028
1054
  doc: "docs/api.md",
1029
1055
  heading: "Animated.View",
1030
- text: "Animated.View — GTK implementation: direct widget calls. Supported: `opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange). Differences from RN: `rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`)",
1056
+ text: "Animated.View — GTK implementation: direct widget calls. Supported: `opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange), plus the same responder and touch props `View` takes — this is where an idiomatic `PanResponder` drag lands. Differences from RN: `rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`)",
1031
1057
  },
1032
1058
  {
1033
1059
  doc: "docs/api.md",
@@ -1067,7 +1093,7 @@ export const DOC_CHUNKS = [
1067
1093
  {
1068
1094
  doc: "docs/api.md",
1069
1095
  heading: "AppRegistry",
1070
- text: 'AppRegistry — Supported: `registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,applicationActions,actionAccels,windowActions,windowControllers,breakpoints})`, `getAppKeys`. Differences: desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar — the app\'s HeaderBars (navigation) become the chrome. `applicationActions`/`actionAccels` reach the underlying `GtkApplication` (`app.*` actions — what a `Gio.Notification` action button targets); `windowActions`/`windowControllers` reach the window (`win.*` actions, a window-scoped `GtkShortcutController`); `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` (a dev warning fires otherwise)',
1096
+ text: 'AppRegistry — Supported: `registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,actionAccels,breakpoints,applicationActions,windowActions,windowControllers})`, `getAppKeys`. Differences: desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar — the app\'s HeaderBars (navigation) become the chrome. `actionAccels` binds accelerators to action names on the `GtkApplication`; `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` (a dev warning fires otherwise). **`applicationActions`/`windowActions`/`windowControllers` are deprecated** — reach for [`<ApplicationActions>`/`<WindowActions>`/`<WindowControllers>`](platform-layer.md#actions-and-shortcuts-declared-in-the-app-tree) instead; they still work unchanged',
1071
1097
  },
1072
1098
  {
1073
1099
  doc: "docs/api.md",
@@ -1129,10 +1155,15 @@ export const DOC_CHUNKS = [
1129
1155
  heading: "BackHandler",
1130
1156
  text: 'BackHandler — Supported: `addEventListener("hardwareBackPress")`, `exitApp`. Differences: no hardware back key on desktop — subscriptions are honored but nothing fires them yet',
1131
1157
  },
1158
+ {
1159
+ doc: "docs/api.md",
1160
+ heading: "PanResponder",
1161
+ text: "PanResponder — Supported: `create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native's own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN's `touchHistory` store. Differences: multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`'s return value is not consumed yet",
1162
+ },
1132
1163
  {
1133
1164
  doc: "docs/api.md",
1134
1165
  heading: "Animated",
1135
- text: "Animated — Supported: `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity). Differences: `useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway",
1166
+ text: "Animated — Supported: `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity), `ValueXY` (`setValue`/`setOffset`/`flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) — the value a `PanResponder` drag writes to. Differences: `useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway; `Animated.event` is not implemented — write the value directly (`pan.setValue({x: g.dx, y: g.dy})`), which is what it would do",
1136
1167
  },
1137
1168
  {
1138
1169
  doc: "docs/api.md",