@gtkx/react 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,242 +5,138 @@
5
5
  <h1 align="center">GTKX</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Build native GTK4 desktop applications with React and TypeScript</strong>
8
+ <strong>Build native GTK4 desktop apps with React</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
12
  <a href="https://eugeniodepalo.github.io/gtkx">Documentation</a> ·
13
13
  <a href="#quick-start">Quick Start</a> ·
14
- <a href="#examples">Examples</a>
14
+ <a href="#examples">Examples</a> ·
15
+ <a href="#contributing">Contributing</a>
15
16
  </p>
16
17
 
17
18
  ---
18
19
 
19
- GTKX bridges React's component model with GTK4's native widget system. Write familiar React code and render it as native Linux desktop applications with full access to GTK4 widgets, signals, and styling.
20
+ GTKX lets you build native Linux desktop applications using React and TypeScript. Write familiar React code that renders as native GTK4 widgets—no Electron, no web views.
20
21
 
21
22
  ## Features
22
23
 
23
- - **React Components** — Use React hooks, state, and component patterns you already know
24
- - **Hot Module Replacement** — Edit your code and see changes instantly, powered by Vite
25
- - **Native Performance** — Direct FFI bindings to GTK4 via Rust and libffi
26
- - **CLI & Scaffolding** — Get started in seconds with `npx @gtkx/cli@latest create`
27
- - **CSS-in-JS Styling** — Emotion-style `css` template literals for GTK widgets
28
- - **Testing Library** — Familiar `screen`, `userEvent`, and query APIs for testing components
24
+ - **React** — Hooks, state, props, and components you already know
25
+ - **Hot Reload** — Edit code and see changes instantly via Vite
26
+ - **Native** — Direct FFI bindings to GTK4 via Rust and libffi
27
+ - **CLI** — `npx @gtkx/cli@latest create` scaffolds a ready-to-go project
28
+ - **CSS-in-JS** — Emotion-style `css` template literals for GTK styling
29
+ - **Testing** — Testing Library-style `screen`, `userEvent`, and queries
29
30
 
30
31
  ## Quick Start
31
32
 
32
- Create a new GTKX app with a single command:
33
-
34
- ```bash
35
- npx @gtkx/cli@latest create
36
- ```
37
-
38
- This launches an interactive wizard that sets up your project with TypeScript, your preferred package manager, and optional testing support.
39
-
40
- You can also pass options directly:
41
-
42
- ```bash
43
- npx @gtkx/cli@latest create my-app --app-id com.example.myapp --pm pnpm --testing vitest
44
- ```
45
-
46
- Then start developing with HMR:
47
-
48
33
  ```bash
34
+ npx @gtkx/cli@latest create my-app
49
35
  cd my-app
50
36
  npm run dev
51
37
  ```
52
38
 
53
- Edit your code and see changes instantly without restarting the app!
54
-
55
- ### Manual Setup
39
+ Edit your code and see changes instantly—no restart needed.
56
40
 
57
- Alternatively, install packages directly:
58
-
59
- ```bash
60
- npm install @gtkx/cli @gtkx/react @gtkx/ffi react
61
- npm install -D @types/react typescript
62
- ```
63
-
64
- Create your first app:
41
+ ### Example
65
42
 
66
43
  ```tsx
67
- // src/app.tsx
44
+ import { render, ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
45
+ import { Orientation } from "@gtkx/ffi/gtk";
68
46
  import { useState } from "react";
69
- import * as Gtk from "@gtkx/ffi/gtk";
70
- import { ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
71
47
 
72
- export default function App() {
48
+ const App = () => {
73
49
  const [count, setCount] = useState(0);
74
50
 
75
51
  return (
76
- <ApplicationWindow
77
- title="My App"
78
- defaultWidth={400}
79
- defaultHeight={300}
80
- onCloseRequest={quit}
81
- >
82
- <Box
83
- orientation={Gtk.Orientation.VERTICAL}
84
- spacing={12}
85
- marginStart={20}
86
- marginEnd={20}
87
- marginTop={20}
88
- marginBottom={20}
89
- >
52
+ <ApplicationWindow title="Counter" onCloseRequest={quit}>
53
+ <Box orientation={Orientation.VERTICAL} spacing={12} margin={20}>
90
54
  <Label.Root label={`Count: ${count}`} />
91
55
  <Button label="Increment" onClicked={() => setCount((c) => c + 1)} />
92
56
  </Box>
93
57
  </ApplicationWindow>
94
58
  );
95
- }
96
-
97
- export const appId = "org.example.MyApp";
98
- ```
99
-
100
- ```tsx
101
- // src/index.tsx
102
- import { render } from "@gtkx/react";
103
- import App, { appId } from "./app.js";
104
-
105
- render(<App />, appId);
106
- ```
59
+ };
107
60
 
108
- Run with HMR:
109
-
110
- ```bash
111
- npx gtkx dev src/app.tsx
112
- ```
113
-
114
- Or without HMR (production):
115
-
116
- ```bash
117
- npx tsc -b && node dist/index.js
61
+ render(<App />, "org.example.Counter");
118
62
  ```
119
63
 
120
64
  ## Styling
121
65
 
122
- Use `@gtkx/css` for CSS-in-JS styling:
123
-
124
66
  ```tsx
125
67
  import { css } from "@gtkx/css";
126
68
  import { Button } from "@gtkx/react";
127
69
 
128
- const primaryButton = css`
70
+ const primary = css`
129
71
  padding: 16px 32px;
130
72
  border-radius: 24px;
131
73
  background: linear-gradient(135deg, #3584e4, #9141ac);
132
74
  color: white;
133
- font-weight: bold;
134
75
  `;
135
76
 
136
- const MyButton = () => <Button label="Click me" cssClasses={[primaryButton]} />;
77
+ <Button label="Click me" cssClasses={[primary]} />
137
78
  ```
138
79
 
139
- GTK also provides built-in CSS classes like `suggested-action`, `destructive-action`, `card`, and `heading`.
80
+ GTK also provides built-in classes like `suggested-action`, `destructive-action`, `card`, and `heading`.
140
81
 
141
82
  ## Testing
142
83
 
143
- Use `@gtkx/testing` for Testing Library-style component tests:
144
-
145
84
  ```tsx
146
- import { cleanup, render, screen, userEvent, fireEvent } from "@gtkx/testing";
85
+ import { cleanup, render, screen, userEvent } from "@gtkx/testing";
147
86
  import { AccessibleRole } from "@gtkx/ffi/gtk";
148
- import { App } from "./app.js";
149
87
 
150
- // Clean up after each test
151
- afterEach(async () => {
152
- await cleanup();
153
- });
88
+ afterEach(() => cleanup());
154
89
 
155
- test("increments count when clicking button", async () => {
90
+ test("increments count", async () => {
156
91
  await render(<App />);
157
92
 
158
- const button = await screen.findByRole(AccessibleRole.BUTTON, {
159
- name: "Increment",
160
- });
93
+ const button = await screen.findByRole(AccessibleRole.BUTTON, { name: "Increment" });
161
94
  await userEvent.click(button);
162
95
 
163
96
  await screen.findByText("Count: 1");
164
97
  });
165
-
166
- test("can also use fireEvent for low-level signals", async () => {
167
- await render(<App />);
168
-
169
- const button = await screen.findByRole(AccessibleRole.BUTTON, {
170
- name: "Increment",
171
- });
172
- fireEvent(button, "clicked");
173
-
174
- await screen.findByText("Count: 1");
175
- });
176
98
  ```
177
99
 
178
- ### Available APIs
179
-
180
- **Queries** - Find elements in the rendered tree (all async):
181
-
182
- - `findBy*` / `findAllBy*` - Waits for element to appear
183
-
184
- Query types: `ByRole`, `ByText`, `ByLabelText`, `ByTestId`
185
-
186
- **User Interactions**:
100
+ Queries: `findByRole`, `findByText`, `findByLabelText`, `findByTestId`
187
101
 
188
- - `userEvent.click(element)` - Simulate click
189
- - `userEvent.dblClick(element)` - Simulate double click
190
- - `userEvent.activate(element)` - Activate element (e.g., press Enter in input)
191
- - `userEvent.type(element, text)` - Type text into input
192
- - `userEvent.clear(element)` - Clear input text
193
- - `userEvent.tab(element, options?)` - Simulate Tab navigation
194
- - `userEvent.selectOptions(element, values)` - Select options in ComboBox/ListBox
195
- - `userEvent.deselectOptions(element, values)` - Deselect options in ListBox
196
-
197
- **Low-level Events**:
198
-
199
- - `fireEvent(element, signalName, ...args)` - Emit any GTK signal with optional arguments
200
-
201
- **Utilities**:
202
-
203
- - `waitFor(callback)` - Wait for condition
204
- - `waitForElementToBeRemoved(element)` - Wait for element removal
102
+ User events: `click`, `dblClick`, `type`, `clear`, `tab`, `selectOptions`
205
103
 
206
104
  ## Examples
207
105
 
208
- ### GTK4 Demo
209
-
210
- A comprehensive showcase of GTK4 widgets and features:
106
+ | Example | Description |
107
+ | ------- | ----------- |
108
+ | [gtk4-demo](examples/gtk4-demo) | Widget showcase |
109
+ | [todo](examples/todo) | Todo app with tests |
211
110
 
212
111
  ```bash
213
- cd examples/gtk4-demo
214
- pnpm dev
215
- ```
216
-
217
- ### Todo App
218
-
219
- A todo app demonstrating `@gtkx/testing` with realistic component tests:
220
-
221
- ```bash
222
- cd examples/todo
223
- pnpm dev
224
- pnpm test
112
+ cd examples/gtk4-demo && pnpm dev
225
113
  ```
226
114
 
227
115
  ## Packages
228
116
 
229
- | Package | Description |
230
- | --------------------------------- | -------------------------------------------------- |
231
- | [@gtkx/cli](packages/cli) | CLI for creating and developing GTKX apps with HMR |
232
- | [@gtkx/react](packages/react) | React reconciler and JSX components |
233
- | [@gtkx/ffi](packages/ffi) | TypeScript FFI bindings for GTK4 |
234
- | [@gtkx/native](packages/native) | Rust native module for FFI bridge |
235
- | [@gtkx/css](packages/css) | CSS-in-JS styling for GTK widgets |
236
- | [@gtkx/testing](packages/testing) | Testing utilities for GTKX components |
237
- | [@gtkx/gir](packages/gir) | GObject Introspection parser for codegen |
117
+ | Package | Description |
118
+ | ------- | ----------- |
119
+ | [@gtkx/cli](packages/cli) | CLI with HMR dev server |
120
+ | [@gtkx/react](packages/react) | React reconciler and JSX components |
121
+ | [@gtkx/ffi](packages/ffi) | TypeScript bindings for GTK4/GLib/GIO |
122
+ | [@gtkx/native](packages/native) | Rust native module (libffi bridge) |
123
+ | [@gtkx/css](packages/css) | CSS-in-JS styling |
124
+ | [@gtkx/testing](packages/testing) | Testing utilities |
125
+ | [@gtkx/gir](packages/gir) | GObject Introspection parser |
238
126
 
239
127
  ## Requirements
240
128
 
241
129
  - Node.js 20+
242
- - GTK4
243
- - Linux (GTK4 is Linux-native)
130
+ - GTK4 (`gtk4-devel` on Fedora, `libgtk-4-dev` on Ubuntu)
131
+ - Linux
132
+
133
+ ## Contributing
134
+
135
+ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
136
+
137
+ - [Report a bug](https://github.com/eugeniodepalo/gtkx/issues/new?template=bug_report.md)
138
+ - [Request a feature](https://github.com/eugeniodepalo/gtkx/issues/new?template=feature_request.md)
139
+ - [Read the Code of Conduct](CODE_OF_CONDUCT.md)
244
140
 
245
141
  ## License
246
142
 
package/dist/batch.d.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  type FlushCallback = () => void;
2
+ /**
3
+ * Marks the beginning of a React reconciler commit phase.
4
+ * While in commit, flush callbacks are deferred until endCommit is called.
5
+ */
2
6
  export declare const beginCommit: () => void;
7
+ /**
8
+ * Marks the end of a React reconciler commit phase.
9
+ * Executes all pending flush callbacks that were deferred during the commit.
10
+ */
3
11
  export declare const endCommit: () => void;
12
+ /**
13
+ * Schedules a callback to be executed, deferring it if currently in a commit phase.
14
+ * This ensures GTK state updates happen after React has finished its batch of changes.
15
+ */
4
16
  export declare const scheduleFlush: (callback: FlushCallback) => void;
5
17
  export {};
package/dist/batch.js CHANGED
@@ -1,8 +1,16 @@
1
1
  const pendingFlushes = new Set();
2
2
  let inCommit = false;
3
+ /**
4
+ * Marks the beginning of a React reconciler commit phase.
5
+ * While in commit, flush callbacks are deferred until endCommit is called.
6
+ */
3
7
  export const beginCommit = () => {
4
8
  inCommit = true;
5
9
  };
10
+ /**
11
+ * Marks the end of a React reconciler commit phase.
12
+ * Executes all pending flush callbacks that were deferred during the commit.
13
+ */
6
14
  export const endCommit = () => {
7
15
  inCommit = false;
8
16
  if (pendingFlushes.size > 0) {
@@ -15,6 +23,10 @@ export const endCommit = () => {
15
23
  });
16
24
  }
17
25
  };
26
+ /**
27
+ * Schedules a callback to be executed, deferring it if currently in a commit phase.
28
+ * This ensures GTK state updates happen after React has finished its batch of changes.
29
+ */
18
30
  export const scheduleFlush = (callback) => {
19
31
  if (inCommit) {
20
32
  pendingFlushes.add(callback);
package/dist/factory.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import type * as Gtk from "@gtkx/ffi/gtk";
2
2
  import type { Node } from "./node.js";
3
3
  import { type ROOT_NODE_CONTAINER } from "./nodes/root.js";
4
+ /**
5
+ * Generic props type for React component properties passed to GTK nodes.
6
+ */
4
7
  export type Props = Record<string, unknown>;
5
8
  export { ROOT_NODE_CONTAINER } from "./nodes/root.js";
9
+ /**
10
+ * Creates a Node instance for the given JSX element type.
11
+ * Matches the type against registered node classes and initializes with props.
12
+ */
6
13
  export declare const createNode: (type: string, props: Props, existingWidget?: Gtk.Widget | typeof ROOT_NODE_CONTAINER) => Node;
package/dist/factory.js CHANGED
@@ -16,9 +16,7 @@ import { TextViewNode } from "./nodes/text-view.js";
16
16
  import { WidgetNode } from "./nodes/widget.js";
17
17
  import { WindowNode } from "./nodes/window.js";
18
18
  export { ROOT_NODE_CONTAINER } from "./nodes/root.js";
19
- const NODE_CLASSES = [
20
- RootNode,
21
- // Virtual nodes (no widget)
19
+ const VIRTUAL_NODES = [
22
20
  ColumnViewColumnNode,
23
21
  ColumnViewItemNode,
24
22
  ListItemNode,
@@ -30,14 +28,16 @@ const NODE_CLASSES = [
30
28
  MenuSectionNode,
31
29
  MenuSubmenuNode,
32
30
  SlotNode,
33
- // Specialized widget nodes
31
+ ];
32
+ const SPECIALIZED_NODES = [
34
33
  WindowNode,
35
34
  AboutDialogNode,
36
35
  TextViewNode,
37
36
  ApplicationMenuNode,
38
37
  PopoverMenuRootNode,
39
38
  PopoverMenuBarNode,
40
- // Container nodes
39
+ ];
40
+ const CONTAINER_NODES = [
41
41
  ActionBarNode,
42
42
  FlowBoxNode,
43
43
  ListBoxNode,
@@ -48,9 +48,12 @@ const NODE_CLASSES = [
48
48
  ListViewNode,
49
49
  NotebookNode,
50
50
  StackNode,
51
- // Catch-all (must be last)
52
- WidgetNode,
53
51
  ];
52
+ const NODE_CLASSES = [RootNode, ...VIRTUAL_NODES, ...SPECIALIZED_NODES, ...CONTAINER_NODES, WidgetNode];
53
+ /**
54
+ * Creates a Node instance for the given JSX element type.
55
+ * Matches the type against registered node classes and initializes with props.
56
+ */
54
57
  export const createNode = (type, props, existingWidget) => {
55
58
  for (const NodeClass of NODE_CLASSES) {
56
59
  if (NodeClass.matches(type, existingWidget)) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  export { createRef } from "@gtkx/ffi";
2
+ /**
3
+ * @private Internal symbol used to identify the root container node.
4
+ * This is an internal API used only by @gtkx/testing. Do not use directly.
5
+ */
6
+ export { ROOT_NODE_CONTAINER } from "./factory.js";
2
7
  export * from "./generated/jsx.js";
3
8
  export { createPortal } from "./portal.js";
4
9
  export { reconciler } from "./reconciler.js";
package/dist/index.js CHANGED
@@ -1,4 +1,9 @@
1
1
  export { createRef } from "@gtkx/ffi";
2
+ /**
3
+ * @private Internal symbol used to identify the root container node.
4
+ * This is an internal API used only by @gtkx/testing. Do not use directly.
5
+ */
6
+ export { ROOT_NODE_CONTAINER } from "./factory.js";
2
7
  export * from "./generated/jsx.js";
3
8
  export { createPortal } from "./portal.js";
4
9
  export { reconciler } from "./reconciler.js";
@@ -1,4 +1,5 @@
1
- import { getObject, getObjectId } from "@gtkx/ffi";
1
+ import { getInterface, getObject, getObjectId } from "@gtkx/ffi";
2
+ import * as Gio from "@gtkx/ffi/gio";
2
3
  import * as GObject from "@gtkx/ffi/gobject";
3
4
  import * as Gtk from "@gtkx/ffi/gtk";
4
5
  import { scheduleFlush } from "../batch.js";
@@ -11,7 +12,9 @@ export class ColumnViewNode extends Node {
11
12
  return type === "ColumnView.Root";
12
13
  }
13
14
  initialize(props) {
14
- // Initialize state before super.initialize() since updateProps accesses this.state
15
+ // State must be initialized before super.initialize() since updateProps accesses this.state.
16
+ // GTK objects can't be created yet because this.widget doesn't exist until super.initialize().
17
+ // The null placeholders are immediately replaced after super.initialize() completes.
15
18
  this.state = {
16
19
  stringList: null,
17
20
  selectionModel: null,
@@ -30,9 +33,9 @@ export class ColumnViewNode extends Node {
30
33
  };
31
34
  super.initialize(props);
32
35
  const stringList = new Gtk.StringList([]);
33
- const sortListModel = new Gtk.SortListModel(stringList, this.widget.getSorter());
36
+ const sortListModel = new Gtk.SortListModel(getInterface(stringList, Gio.ListModel), this.widget.getSorter());
34
37
  sortListModel.setIncremental(true);
35
- const selectionModel = new Gtk.SingleSelection(sortListModel);
38
+ const selectionModel = new Gtk.SingleSelection(getInterface(sortListModel, Gio.ListModel));
36
39
  this.widget.setModel(selectionModel);
37
40
  this.state.stringList = stringList;
38
41
  this.state.sortListModel = sortListModel;
@@ -218,7 +221,8 @@ export class ColumnViewColumnNode extends Node {
218
221
  }
219
222
  columnView = null;
220
223
  initialize(props) {
221
- // Initialize state before super.initialize() since updateProps accesses this.state
224
+ // Unlike ColumnViewNode, we can create GTK objects before super.initialize() here
225
+ // since this is a virtual node (no widget created by parent class).
222
226
  const factory = new Gtk.SignalListItemFactory();
223
227
  const column = new Gtk.ColumnViewColumn(props.title, factory);
224
228
  const columnId = props.id ?? null;
@@ -2,10 +2,24 @@ import * as Gtk from "@gtkx/ffi/gtk";
2
2
  import { type ItemContainer } from "../container-interfaces.js";
3
3
  import type { Props } from "../factory.js";
4
4
  import { Node } from "../node.js";
5
- export declare class DropDownNode extends Node<Gtk.DropDown> implements ItemContainer<unknown> {
5
+ type ItemLabelFn = (item: unknown) => string;
6
+ interface DropDownState {
7
+ store: DropDownStore;
8
+ onSelectionChanged?: (item: unknown, index: number) => void;
9
+ }
10
+ declare class DropDownStore {
11
+ private stringList;
12
+ private items;
13
+ private labelFn;
14
+ constructor(labelFn: ItemLabelFn);
15
+ getModel(): Gtk.StringList;
16
+ append(item: unknown): void;
17
+ remove(item: unknown): void;
18
+ getItem(index: number): unknown;
19
+ get length(): number;
20
+ }
21
+ export declare class DropDownNode extends Node<Gtk.DropDown, DropDownState> implements ItemContainer<unknown> {
6
22
  static matches(type: string): boolean;
7
- private store;
8
- private onSelectionChanged?;
9
23
  initialize(props: Props): void;
10
24
  addItem(item: unknown): void;
11
25
  insertItemBefore(item: unknown, _beforeItem: unknown): void;
@@ -22,3 +36,4 @@ export declare class DropDownItemNode extends Node<never> {
22
36
  attachToParent(parent: Node): void;
23
37
  detachFromParent(parent: Node): void;
24
38
  }
39
+ export {};
@@ -1,3 +1,5 @@
1
+ import { getInterface } from "@gtkx/ffi";
2
+ import * as Gio from "@gtkx/ffi/gio";
1
3
  import * as Gtk from "@gtkx/ffi/gtk";
2
4
  import { isItemContainer } from "../container-interfaces.js";
3
5
  import { Node } from "../node.js";
@@ -35,31 +37,30 @@ export class DropDownNode extends Node {
35
37
  static matches(type) {
36
38
  return type === "DropDown.Root";
37
39
  }
38
- store;
39
- onSelectionChanged;
40
40
  initialize(props) {
41
- super.initialize(props);
42
41
  const labelFn = props.itemLabel ?? ((item) => String(item));
43
- this.onSelectionChanged = props.onSelectionChanged;
44
- this.store = new DropDownStore(labelFn);
45
- this.widget.setModel(this.store.getModel());
46
- if (this.onSelectionChanged) {
42
+ const store = new DropDownStore(labelFn);
43
+ const onSelectionChanged = props.onSelectionChanged;
44
+ this.state = { store, onSelectionChanged };
45
+ super.initialize(props);
46
+ this.widget.setModel(getInterface(store.getModel(), Gio.ListModel));
47
+ if (onSelectionChanged) {
47
48
  const handler = () => {
48
49
  const index = this.widget.getSelected();
49
- const item = this.store.getItem(index);
50
- this.onSelectionChanged?.(item, index);
50
+ const item = this.state.store.getItem(index);
51
+ this.state.onSelectionChanged?.(item, index);
51
52
  };
52
53
  this.connectSignal(this.widget, "notify::selected", handler);
53
54
  }
54
55
  }
55
56
  addItem(item) {
56
- this.store.append(item);
57
+ this.state.store.append(item);
57
58
  }
58
59
  insertItemBefore(item, _beforeItem) {
59
60
  this.addItem(item);
60
61
  }
61
62
  removeItem(item) {
62
- this.store.remove(item);
63
+ this.state.store.remove(item);
63
64
  }
64
65
  consumedProps() {
65
66
  const consumed = super.consumedProps();
@@ -69,7 +70,7 @@ export class DropDownNode extends Node {
69
70
  }
70
71
  updateProps(oldProps, newProps) {
71
72
  if (oldProps.onSelectionChanged !== newProps.onSelectionChanged) {
72
- this.onSelectionChanged = newProps.onSelectionChanged;
73
+ this.state.onSelectionChanged = newProps.onSelectionChanged;
73
74
  }
74
75
  super.updateProps(oldProps, newProps);
75
76
  }
@@ -1,4 +1,5 @@
1
- import { getObject, getObjectId } from "@gtkx/ffi";
1
+ import { getInterface, getObject, getObjectId } from "@gtkx/ffi";
2
+ import * as Gio from "@gtkx/ffi/gio";
2
3
  import * as Gtk from "@gtkx/ffi/gtk";
3
4
  import { scheduleFlush } from "../batch.js";
4
5
  import { isItemContainer } from "../container-interfaces.js";
@@ -10,7 +11,9 @@ export class ListViewNode extends Node {
10
11
  return type === "ListView.Root" || type === "GridView.Root";
11
12
  }
12
13
  initialize(props) {
13
- // Initialize state before super.initialize() since updateProps accesses this.state
14
+ // State must be initialized before super.initialize() since updateProps accesses this.state.
15
+ // GTK objects can't be created yet because this.widget doesn't exist until super.initialize().
16
+ // The null placeholders are immediately replaced after super.initialize() completes.
14
17
  this.state = {
15
18
  stringList: null,
16
19
  selectionModel: null,
@@ -22,7 +25,7 @@ export class ListViewNode extends Node {
22
25
  };
23
26
  super.initialize(props);
24
27
  const stringList = new Gtk.StringList([]);
25
- const selectionModel = new Gtk.SingleSelection(stringList);
28
+ const selectionModel = new Gtk.SingleSelection(getInterface(stringList, Gio.ListModel));
26
29
  const factory = new Gtk.SignalListItemFactory();
27
30
  this.state.stringList = stringList;
28
31
  this.state.selectionModel = selectionModel;
@@ -1,4 +1,4 @@
1
- import { getCurrentApp } from "@gtkx/ffi";
1
+ import { getCurrentApp, getInterface } from "@gtkx/ffi";
2
2
  import * as Gio from "@gtkx/ffi/gio";
3
3
  import * as GObject from "@gtkx/ffi/gobject";
4
4
  import * as Gtk from "@gtkx/ffi/gtk";
@@ -177,7 +177,7 @@ export class MenuItemNode extends Node {
177
177
  this.onActivateCallback?.();
178
178
  });
179
179
  const app = getCurrentApp();
180
- app.addAction(this.action);
180
+ app.addAction(getInterface(this.action, Gio.Action));
181
181
  this.entry.action = `app.${this.actionName}`;
182
182
  if (this.currentAccels) {
183
183
  this.updateAccels(this.currentAccels);
package/dist/portal.js CHANGED
@@ -18,6 +18,8 @@ import { ROOT_NODE_CONTAINER } from "./factory.js";
18
18
  * ```
19
19
  */
20
20
  export const createPortal = (children, container, key) => {
21
+ // ReactPortal is an opaque type but we need to construct it manually for custom reconcilers.
22
+ // The shape matches React's internal portal representation.
21
23
  return {
22
24
  $$typeof: Symbol.for("react.portal"),
23
25
  key: key ?? null,
@@ -8,7 +8,16 @@ interface SingleChild extends Gtk.Widget {
8
8
  interface Removable extends Gtk.Widget {
9
9
  remove(child: unknown): void;
10
10
  }
11
+ /**
12
+ * Type guard that checks if a GTK widget supports appending children via an append method.
13
+ */
11
14
  export declare const isAppendable: (widget: Gtk.Widget) => widget is Appendable;
15
+ /**
16
+ * Type guard that checks if a GTK widget supports a single child via setChild method.
17
+ */
12
18
  export declare const isSingleChild: (widget: Gtk.Widget) => widget is SingleChild;
19
+ /**
20
+ * Type guard that checks if a GTK widget supports removing children via a remove method.
21
+ */
13
22
  export declare const isRemovable: (widget: Gtk.Widget) => widget is Removable;
14
23
  export {};
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Type guard that checks if a GTK widget supports appending children via an append method.
3
+ */
1
4
  export const isAppendable = (widget) => "append" in widget && typeof widget.append === "function";
5
+ /**
6
+ * Type guard that checks if a GTK widget supports a single child via setChild method.
7
+ */
2
8
  export const isSingleChild = (widget) => "setChild" in widget && typeof widget.setChild === "function";
9
+ /**
10
+ * Type guard that checks if a GTK widget supports removing children via a remove method.
11
+ */
3
12
  export const isRemovable = (widget) => "remove" in widget && typeof widget.remove === "function";
@@ -21,6 +21,11 @@ declare class Reconciler {
21
21
  */
22
22
  getInstance(): ReconcilerInstance;
23
23
  private createHostConfig;
24
+ /**
25
+ * Creates a React context compatible with the reconciler's internal type.
26
+ * Cast is necessary because React.Context and ReactReconciler.ReactContext
27
+ * are structurally identical at runtime but declared as separate types.
28
+ */
24
29
  private createReconcilerContext;
25
30
  private createNodeFromContainer;
26
31
  }
@@ -99,6 +99,11 @@ class Reconciler {
99
99
  waitForCommitToBeReady: () => null,
100
100
  };
101
101
  }
102
+ /**
103
+ * Creates a React context compatible with the reconciler's internal type.
104
+ * Cast is necessary because React.Context and ReactReconciler.ReactContext
105
+ * are structurally identical at runtime but declared as separate types.
106
+ */
102
107
  createReconcilerContext(value) {
103
108
  const context = React.createContext(value);
104
109
  return context;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/react",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Build GTK4 desktop applications with React and TypeScript",
5
5
  "keywords": [
6
6
  "gtk",
@@ -36,10 +36,10 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "react-reconciler": "0.33.0",
39
- "@gtkx/ffi": "0.3.1"
39
+ "@gtkx/ffi": "0.3.3"
40
40
  },
41
41
  "devDependencies": {
42
- "@gtkx/gir": "0.3.1"
42
+ "@gtkx/gir": "0.3.3"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "react": "^19"