@gtkx/cli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,151 +1,148 @@
1
1
  # GTKX Code Examples
2
2
 
3
- ## Application Structure
3
+ ## App Structure
4
4
 
5
- ### Basic App with State
5
+ ### Minimal App
6
6
 
7
7
  ```tsx
8
- import * as Gtk from "@gtkx/ffi/gtk";
9
- import { GtkApplicationWindow, GtkBox, GtkLabel, quit } from "@gtkx/react";
10
- import { useCallback, useState } from "react";
8
+ import { GtkApplicationWindow, GtkLabel, render, quit } from "@gtkx/react";
11
9
 
12
- interface Todo {
13
- id: number;
14
- text: string;
15
- completed: boolean;
16
- }
17
-
18
- let nextId = 1;
10
+ const App = () => (
11
+ <GtkApplicationWindow title="Hello" defaultWidth={400} defaultHeight={300} onCloseRequest={quit}>
12
+ <GtkLabel label="Hello, World!" />
13
+ </GtkApplicationWindow>
14
+ );
19
15
 
20
- export const App = () => {
21
- const [todos, setTodos] = useState<Todo[]>([]);
16
+ render(<App />, "com.example.hello");
17
+ ```
22
18
 
23
- const addTodo = useCallback((text: string) => {
24
- setTodos((prev) => [...prev, { id: nextId++, text, completed: false }]);
25
- }, []);
19
+ ### Modern Adwaita App
26
20
 
27
- const toggleTodo = useCallback((id: number) => {
28
- setTodos((prev) =>
29
- prev.map((todo) =>
30
- todo.id === id ? { ...todo, completed: !todo.completed } : todo
31
- )
32
- );
33
- }, []);
21
+ ```tsx
22
+ import * as Gtk from "@gtkx/ffi/gtk";
23
+ import {
24
+ AdwApplicationWindow,
25
+ AdwHeaderBar,
26
+ AdwToolbarView,
27
+ AdwWindowTitle,
28
+ AdwStatusPage,
29
+ GtkButton,
30
+ Slot,
31
+ Toolbar,
32
+ quit,
33
+ } from "@gtkx/react";
34
34
 
35
- return (
36
- <GtkApplicationWindow title="Todo App" defaultWidth={400} defaultHeight={500} onCloseRequest={quit}>
37
- <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={16} marginTop={16} marginStart={16} marginEnd={16}>
38
- Todo App
39
- </GtkBox>
40
- </GtkApplicationWindow>
41
- );
42
- };
35
+ export const App = () => (
36
+ <AdwApplicationWindow title="My App" defaultWidth={800} defaultHeight={600} onCloseRequest={quit}>
37
+ <AdwToolbarView>
38
+ <Toolbar.Top>
39
+ <AdwHeaderBar>
40
+ <Slot for={AdwHeaderBar} id="titleWidget">
41
+ <AdwWindowTitle title="My App" subtitle="Welcome" />
42
+ </Slot>
43
+ </AdwHeaderBar>
44
+ </Toolbar.Top>
45
+ <AdwStatusPage
46
+ iconName="applications-system-symbolic"
47
+ title="Welcome"
48
+ description="Get started with your GTKX app"
49
+ vexpand
50
+ >
51
+ <GtkButton label="Get Started" cssClasses={["suggested-action", "pill"]} halign={Gtk.Align.CENTER} />
52
+ </AdwStatusPage>
53
+ </AdwToolbarView>
54
+ </AdwApplicationWindow>
55
+ );
43
56
 
44
- export const appId = "com.gtkx.todo";
57
+ export const appId = "com.example.myapp";
45
58
  ```
46
59
 
47
- ## Layout Patterns
60
+ ---
48
61
 
49
- ### Grid for Forms
62
+ ## State Management
63
+
64
+ ### Controlled Form
50
65
 
51
66
  ```tsx
52
67
  import * as Gtk from "@gtkx/ffi/gtk";
53
- import { GtkButton, GtkEntry, GtkGrid, GtkLabel, GridChild } from "@gtkx/react";
68
+ import { GtkBox, GtkButton, GtkEntry, GtkGrid, GtkLabel, GridChild } from "@gtkx/react";
54
69
  import { useState } from "react";
55
70
 
56
- const FormLayout = () => {
57
- const [name, setName] = useState("");
71
+ const LoginForm = () => {
58
72
  const [email, setEmail] = useState("");
73
+ const [password, setPassword] = useState("");
74
+
75
+ const handleSubmit = () => {
76
+ console.log("Login:", { email, password });
77
+ };
59
78
 
60
79
  return (
61
- <GtkGrid rowSpacing={8} columnSpacing={12}>
80
+ <GtkGrid rowSpacing={12} columnSpacing={12}>
62
81
  <GridChild column={0} row={0}>
63
- <GtkLabel label="Name:" halign={Gtk.Align.END} />
82
+ <GtkLabel label="Email:" halign={Gtk.Align.END} />
64
83
  </GridChild>
65
84
  <GridChild column={1} row={0}>
66
- <GtkEntry text={name} onChanged={(e) => setName(e.getText())} hexpand />
85
+ <GtkEntry text={email} onChanged={(e) => setEmail(e.getText())} hexpand />
67
86
  </GridChild>
68
87
  <GridChild column={0} row={1}>
69
- <GtkLabel label="Email:" halign={Gtk.Align.END} />
88
+ <GtkLabel label="Password:" halign={Gtk.Align.END} />
70
89
  </GridChild>
71
90
  <GridChild column={1} row={1}>
72
- <GtkEntry text={email} onChanged={(e) => setEmail(e.getText())} hexpand />
91
+ <GtkEntry text={password} onChanged={(e) => setPassword(e.getText())} visibility={false} hexpand />
73
92
  </GridChild>
74
93
  <GridChild column={0} row={2} columnSpan={2}>
75
- <GtkButton label="Submit" halign={Gtk.Align.END} marginTop={8} />
94
+ <GtkButton label="Login" onClicked={handleSubmit} cssClasses={["suggested-action"]} halign={Gtk.Align.END} />
76
95
  </GridChild>
77
96
  </GtkGrid>
78
97
  );
79
98
  };
80
99
  ```
81
100
 
82
- ### Stack with StackSwitcher
101
+ ### List with CRUD Operations
83
102
 
84
103
  ```tsx
85
104
  import * as Gtk from "@gtkx/ffi/gtk";
86
- import { GtkBox, GtkLabel, GtkStack, GtkStackSwitcher, StackPage } from "@gtkx/react";
87
-
88
- const TabContainer = () => (
89
- <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={8}>
90
- <GtkStackSwitcher
91
- ref={(switcher: Gtk.StackSwitcher | null) => {
92
- if (switcher) {
93
- const stack = switcher.getParent()?.getLastChild() as Gtk.Stack | null;
94
- if (stack) switcher.setStack(stack);
95
- }
96
- }}
97
- />
98
- <GtkStack transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT} transitionDuration={200}>
99
- <StackPage name="page1" title="First">First Page Content</StackPage>
100
- <StackPage name="page2" title="Second">Second Page Content</StackPage>
101
- </GtkStack>
102
- </GtkBox>
103
- );
104
- ```
105
-
106
- ## Virtual Scrolling Lists
107
-
108
- ### ListView with Selection
109
-
110
- ```tsx
111
- import * as Gtk from "@gtkx/ffi/gtk";
112
- import { GtkBox, GtkLabel, GtkScrolledWindow, ListView, ListItem } from "@gtkx/react";
113
- import { useState } from "react";
105
+ import { GtkBox, GtkButton, GtkEntry, GtkLabel, GtkScrolledWindow, ListView, ListItem } from "@gtkx/react";
106
+ import { useCallback, useState } from "react";
114
107
 
115
- interface Task {
108
+ interface Todo {
116
109
  id: string;
117
- title: string;
118
- completed: boolean;
110
+ text: string;
119
111
  }
120
112
 
121
- const tasks: Task[] = [
122
- { id: "1", title: "Learn GTK4", completed: true },
123
- { id: "2", title: "Build React app", completed: false },
124
- ];
113
+ let nextId = 1;
114
+
115
+ const TodoList = () => {
116
+ const [todos, setTodos] = useState<Todo[]>([]);
117
+ const [input, setInput] = useState("");
125
118
 
126
- const TaskList = () => {
127
- const [selectedId, setSelectedId] = useState<string | undefined>();
119
+ const addTodo = useCallback(() => {
120
+ if (!input.trim()) return;
121
+ setTodos((prev) => [...prev, { id: String(nextId++), text: input }]);
122
+ setInput("");
123
+ }, [input]);
124
+
125
+ const deleteTodo = useCallback((id: string) => {
126
+ setTodos((prev) => prev.filter((t) => t.id !== id));
127
+ }, []);
128
128
 
129
129
  return (
130
- <GtkBox cssClasses={["card"]} heightRequest={250}>
131
- <GtkScrolledWindow vexpand>
132
- <ListView<Task>
133
- vexpand
134
- selected={selectedId ? [selectedId] : []}
135
- onSelectionChanged={(ids) => setSelectedId(ids[0])}
136
- renderItem={(task) => (
137
- <GtkLabel
138
- label={task?.title ?? ""}
139
- cssClasses={task?.completed ? ["dim-label"] : []}
140
- halign={Gtk.Align.START}
141
- marginStart={12}
142
- marginTop={8}
143
- marginBottom={8}
144
- />
130
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} marginStart={16} marginEnd={16} marginTop={16} marginBottom={16}>
131
+ <GtkBox orientation={Gtk.Orientation.HORIZONTAL} spacing={8}>
132
+ <GtkEntry text={input} onChanged={(e) => setInput(e.getText())} hexpand placeholderText="New todo..." />
133
+ <GtkButton label="Add" onClicked={addTodo} cssClasses={["suggested-action"]} />
134
+ </GtkBox>
135
+ <GtkScrolledWindow vexpand cssClasses={["card"]}>
136
+ <ListView<Todo>
137
+ renderItem={(todo) => (
138
+ <GtkBox orientation={Gtk.Orientation.HORIZONTAL} spacing={8} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
139
+ <GtkLabel label={todo?.text ?? ""} hexpand halign={Gtk.Align.START} />
140
+ <GtkButton iconName="edit-delete-symbolic" cssClasses={["flat"]} onClicked={() => todo && deleteTodo(todo.id)} />
141
+ </GtkBox>
145
142
  )}
146
143
  >
147
- {tasks.map((task) => (
148
- <ListItem key={task.id} id={task.id} value={task} />
144
+ {todos.map((todo) => (
145
+ <ListItem key={todo.id} id={todo.id} value={todo} />
149
146
  ))}
150
147
  </ListView>
151
148
  </GtkScrolledWindow>
@@ -154,154 +151,102 @@ const TaskList = () => {
154
151
  };
155
152
  ```
156
153
 
157
- ### HeaderBar with Navigation
154
+ ---
155
+
156
+ ## Navigation Patterns
157
+
158
+ ### Stack with Sidebar Navigation
158
159
 
159
160
  ```tsx
160
161
  import * as Gtk from "@gtkx/ffi/gtk";
161
- import { GtkApplicationWindow, GtkBox, GtkButton, GtkHeaderBar, GtkLabel, GtkWindow, Pack, Slot, quit } from "@gtkx/react";
162
+ import { GtkBox, GtkLabel, GtkPaned, GtkScrolledWindow, GtkStack, ListView, ListItem, Slot, StackPage } from "@gtkx/react";
162
163
  import { useState } from "react";
163
164
 
164
- const AppWithHeaderBar = () => {
165
- const [page, setPage] = useState("home");
165
+ interface Page {
166
+ id: string;
167
+ name: string;
168
+ }
169
+
170
+ const pages: Page[] = [
171
+ { id: "home", name: "Home" },
172
+ { id: "settings", name: "Settings" },
173
+ { id: "about", name: "About" },
174
+ ];
175
+
176
+ const SidebarNav = () => {
177
+ const [currentPage, setCurrentPage] = useState("home");
166
178
 
167
179
  return (
168
- <GtkApplicationWindow title="My App" defaultWidth={600} defaultHeight={400} onCloseRequest={quit}>
169
- <Slot for={GtkWindow} id="titlebar">
170
- <GtkHeaderBar>
171
- <Pack.Start>
172
- {page !== "home" && (
173
- <GtkButton iconName="go-previous-symbolic" onClicked={() => setPage("home")} />
180
+ <GtkPaned orientation={Gtk.Orientation.HORIZONTAL} position={200}>
181
+ <Slot for={GtkPaned} id="startChild">
182
+ <GtkScrolledWindow cssClasses={["sidebar"]}>
183
+ <ListView<Page>
184
+ selected={[currentPage]}
185
+ selectionMode={Gtk.SelectionMode.SINGLE}
186
+ onSelectionChanged={(ids) => setCurrentPage(ids[0])}
187
+ renderItem={(page) => (
188
+ <GtkLabel label={page?.name ?? ""} halign={Gtk.Align.START} marginStart={12} marginTop={8} marginBottom={8} />
174
189
  )}
175
- </Pack.Start>
176
- <Pack.End>
177
- <GtkButton iconName="emblem-system-symbolic" onClicked={() => setPage("settings")} />
178
- </Pack.End>
179
- </GtkHeaderBar>
190
+ >
191
+ {pages.map((page) => (
192
+ <ListItem key={page.id} id={page.id} value={page} />
193
+ ))}
194
+ </ListView>
195
+ </GtkScrolledWindow>
180
196
  </Slot>
181
- <GtkLabel label={page === "home" ? "Home Page" : "Settings Page"} vexpand />
182
- </GtkApplicationWindow>
197
+ <Slot for={GtkPaned} id="endChild">
198
+ <GtkStack visibleChildName={currentPage}>
199
+ <StackPage name="home"><GtkLabel label="Home Content" vexpand /></StackPage>
200
+ <StackPage name="settings"><GtkLabel label="Settings Content" vexpand /></StackPage>
201
+ <StackPage name="about"><GtkLabel label="About Content" vexpand /></StackPage>
202
+ </GtkStack>
203
+ </Slot>
204
+ </GtkPaned>
183
205
  );
184
206
  };
185
207
  ```
186
208
 
187
- ## Menus
188
-
189
- ### MenuButton with PopoverMenu
209
+ ### Header Bar with Back Navigation
190
210
 
191
211
  ```tsx
192
212
  import * as Gtk from "@gtkx/ffi/gtk";
193
- import { GtkBox, GtkLabel, GtkMenuButton, GtkPopoverMenu, Menu, Slot } from "@gtkx/react";
213
+ import { GtkApplicationWindow, GtkBox, GtkButton, GtkHeaderBar, GtkLabel, GtkStack, GtkWindow, Pack, Slot, StackPage, quit } from "@gtkx/react";
194
214
  import { useState } from "react";
195
215
 
196
- const MenuDemo = () => {
197
- const [lastAction, setLastAction] = useState<string | null>(null);
216
+ const AppWithNavigation = () => {
217
+ const [page, setPage] = useState("home");
198
218
 
199
219
  return (
200
- <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
201
- <GtkLabel label={`Last action: ${lastAction ?? "(none)"}`} />
202
- <GtkMenuButton label="Actions">
203
- <Slot for={GtkMenuButton} id="popover">
204
- <GtkPopoverMenu>
205
- <Menu.Item id="new" label="New" onActivate={() => setLastAction("New")} accels="<Control>n" />
206
- <Menu.Item id="open" label="Open" onActivate={() => setLastAction("Open")} accels="<Control>o" />
207
- <Menu.Item id="save" label="Save" onActivate={() => setLastAction("Save")} accels="<Control>s" />
208
- </GtkPopoverMenu>
209
- </Slot>
210
- </GtkMenuButton>
211
- </GtkBox>
220
+ <GtkApplicationWindow title="App" defaultWidth={600} defaultHeight={400} onCloseRequest={quit}>
221
+ <Slot for={GtkWindow} id="titlebar">
222
+ <GtkHeaderBar>
223
+ <Pack.Start>
224
+ {page !== "home" && <GtkButton iconName="go-previous-symbolic" onClicked={() => setPage("home")} />}
225
+ </Pack.Start>
226
+ <Slot for={GtkHeaderBar} id="titleWidget">
227
+ <GtkLabel label={page === "home" ? "Home" : "Details"} cssClasses={["title"]} />
228
+ </Slot>
229
+ </GtkHeaderBar>
230
+ </Slot>
231
+ <GtkStack visibleChildName={page}>
232
+ <StackPage name="home">
233
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} vexpand halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
234
+ <GtkLabel label="Welcome" />
235
+ <GtkButton label="Go to Details" onClicked={() => setPage("details")} />
236
+ </GtkBox>
237
+ </StackPage>
238
+ <StackPage name="details">
239
+ <GtkLabel label="Details Page Content" vexpand />
240
+ </StackPage>
241
+ </GtkStack>
242
+ </GtkApplicationWindow>
212
243
  );
213
244
  };
214
245
  ```
215
246
 
216
- ## Component Props Pattern
217
-
218
- ### List Item Component
219
-
220
- ```tsx
221
- import * as Gtk from "@gtkx/ffi/gtk";
222
- import { GtkBox, GtkButton, GtkCheckButton, GtkLabel } from "@gtkx/react";
223
-
224
- interface Todo {
225
- id: number;
226
- text: string;
227
- completed: boolean;
228
- }
229
-
230
- interface TodoItemProps {
231
- todo: Todo;
232
- onToggle: (id: number) => void;
233
- onDelete: (id: number) => void;
234
- }
235
-
236
- export const TodoItem = ({ todo, onToggle, onDelete }: TodoItemProps) => (
237
- <GtkBox orientation={Gtk.Orientation.HORIZONTAL} spacing={8}>
238
- <GtkCheckButton active={todo.completed} onToggled={() => onToggle(todo.id)} />
239
- <GtkLabel label={todo.text} hexpand cssClasses={todo.completed ? ["dim-label"] : []} />
240
- <GtkButton iconName="edit-delete-symbolic" onClicked={() => onDelete(todo.id)} cssClasses={["flat"]} />
241
- </GtkBox>
242
- );
243
- ```
244
-
245
- ## Adwaita App Structure
247
+ ---
246
248
 
247
- ### Modern Adwaita App
248
-
249
- ```tsx
250
- import * as Gtk from "@gtkx/ffi/gtk";
251
- import {
252
- AdwApplicationWindow,
253
- AdwHeaderBar,
254
- AdwToolbarView,
255
- AdwWindowTitle,
256
- AdwStatusPage,
257
- AdwBanner,
258
- GtkButton,
259
- Slot,
260
- Toolbar,
261
- quit,
262
- } from "@gtkx/react";
263
- import { useState } from "react";
264
-
265
- export const App = () => {
266
- const [showBanner, setShowBanner] = useState(true);
267
-
268
- return (
269
- <AdwApplicationWindow title="My App" defaultWidth={800} defaultHeight={600} onCloseRequest={quit}>
270
- <AdwToolbarView>
271
- <Toolbar.Top>
272
- <AdwHeaderBar>
273
- <Slot for={AdwHeaderBar} id="titleWidget">
274
- <AdwWindowTitle title="My App" subtitle="Welcome" />
275
- </Slot>
276
- </AdwHeaderBar>
277
- </Toolbar.Top>
278
- <AdwBanner
279
- title="Welcome to the app!"
280
- buttonLabel="Dismiss"
281
- revealed={showBanner}
282
- onButtonClicked={() => setShowBanner(false)}
283
- />
284
- <AdwStatusPage
285
- iconName="applications-system-symbolic"
286
- title="Welcome"
287
- description="Get started with your new GTKX app"
288
- vexpand
289
- >
290
- <GtkButton
291
- label="Get Started"
292
- cssClasses={["suggested-action", "pill"]}
293
- halign={Gtk.Align.CENTER}
294
- />
295
- </AdwStatusPage>
296
- </AdwToolbarView>
297
- </AdwApplicationWindow>
298
- );
299
- };
300
-
301
- export const appId = "com.example.myapp";
302
- ```
303
-
304
- ### Settings Page with Preferences
249
+ ## Settings Page
305
250
 
306
251
  ```tsx
307
252
  import * as Gtk from "@gtkx/ffi/gtk";
@@ -326,21 +271,12 @@ const SettingsPage = () => {
326
271
  return (
327
272
  <GtkScrolledWindow vexpand>
328
273
  <AdwPreferencesPage title="Settings">
329
- <AdwPreferencesGroup title="Appearance" description="Customize the look and feel">
330
- <AdwSwitchRow
331
- title="Dark Mode"
332
- subtitle="Use dark color scheme"
333
- active={darkMode}
334
- onActivate={() => setDarkMode(!darkMode)}
335
- />
274
+ <AdwPreferencesGroup title="Appearance">
275
+ <AdwSwitchRow title="Dark Mode" subtitle="Use dark color scheme" active={darkMode} onActivated={() => setDarkMode(!darkMode)} />
336
276
  </AdwPreferencesGroup>
337
277
 
338
278
  <AdwPreferencesGroup title="Account">
339
- <AdwEntryRow
340
- title="Username"
341
- text={username}
342
- onChanged={(e) => setUsername(e.getText())}
343
- />
279
+ <AdwEntryRow title="Username" text={username} onChanged={(e) => setUsername(e.getText())} />
344
280
  <AdwActionRow title="Profile" subtitle="Manage your profile">
345
281
  <Slot for={AdwActionRow} id="activatableWidget">
346
282
  <GtkImage iconName="go-next-symbolic" valign={Gtk.Align.CENTER} />
@@ -360,3 +296,205 @@ const SettingsPage = () => {
360
296
  );
361
297
  };
362
298
  ```
299
+
300
+ ---
301
+
302
+ ## Data Table with Sorting
303
+
304
+ ```tsx
305
+ import * as Gtk from "@gtkx/ffi/gtk";
306
+ import { GtkBox, GtkColumnView, GtkLabel, GtkScrolledWindow, ColumnViewColumn, ListItem } from "@gtkx/react";
307
+ import { useMemo, useState } from "react";
308
+
309
+ interface FileItem {
310
+ id: string;
311
+ name: string;
312
+ size: number;
313
+ modified: string;
314
+ }
315
+
316
+ const files: FileItem[] = [
317
+ { id: "1", name: "document.pdf", size: 1024, modified: "2024-01-15" },
318
+ { id: "2", name: "image.png", size: 2048, modified: "2024-01-14" },
319
+ { id: "3", name: "notes.txt", size: 512, modified: "2024-01-13" },
320
+ ];
321
+
322
+ const FileTable = () => {
323
+ const [sortColumn, setSortColumn] = useState("name");
324
+ const [sortOrder, setSortOrder] = useState(Gtk.SortType.ASCENDING);
325
+
326
+ const sortedFiles = useMemo(() => {
327
+ const sorted = [...files].sort((a, b) => {
328
+ const aVal = a[sortColumn as keyof FileItem];
329
+ const bVal = b[sortColumn as keyof FileItem];
330
+ const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
331
+ return sortOrder === Gtk.SortType.ASCENDING ? cmp : -cmp;
332
+ });
333
+ return sorted;
334
+ }, [sortColumn, sortOrder]);
335
+
336
+ const handleSort = (column: string, order: Gtk.SortType) => {
337
+ setSortColumn(column);
338
+ setSortOrder(order);
339
+ };
340
+
341
+ return (
342
+ <GtkScrolledWindow vexpand cssClasses={["card"]}>
343
+ <GtkColumnView sortColumn={sortColumn} sortOrder={sortOrder} onSortChange={handleSort}>
344
+ <ColumnViewColumn<FileItem> title="Name" id="name" expand sortable renderCell={(f) => <GtkLabel label={f?.name ?? ""} />} />
345
+ <ColumnViewColumn<FileItem> title="Size" id="size" fixedWidth={100} sortable renderCell={(f) => <GtkLabel label={`${f?.size ?? 0} KB`} />} />
346
+ <ColumnViewColumn<FileItem> title="Modified" id="modified" fixedWidth={120} sortable renderCell={(f) => <GtkLabel label={f?.modified ?? ""} />} />
347
+ {sortedFiles.map((file) => (
348
+ <ListItem key={file.id} id={file.id} value={file} />
349
+ ))}
350
+ </GtkColumnView>
351
+ </GtkScrolledWindow>
352
+ );
353
+ };
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Menu with Keyboard Shortcuts
359
+
360
+ ```tsx
361
+ import * as Gtk from "@gtkx/ffi/gtk";
362
+ import { GtkBox, GtkLabel, GtkMenuButton, GtkPopoverMenu, Menu, Slot, quit } from "@gtkx/react";
363
+ import { useState } from "react";
364
+
365
+ const MenuDemo = () => {
366
+ const [lastAction, setLastAction] = useState<string | null>(null);
367
+
368
+ return (
369
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} marginStart={16} marginEnd={16} marginTop={16} marginBottom={16}>
370
+ <GtkMenuButton label="File" halign={Gtk.Align.START}>
371
+ <Slot for={GtkMenuButton} id="popover">
372
+ <GtkPopoverMenu>
373
+ <Menu.Section>
374
+ <Menu.Item id="new" label="New" onActivate={() => setLastAction("New")} accels="<Control>n" />
375
+ <Menu.Item id="open" label="Open" onActivate={() => setLastAction("Open")} accels="<Control>o" />
376
+ <Menu.Item id="save" label="Save" onActivate={() => setLastAction("Save")} accels="<Control>s" />
377
+ </Menu.Section>
378
+ <Menu.Section>
379
+ <Menu.Submenu label="Export">
380
+ <Menu.Item id="pdf" label="As PDF" onActivate={() => setLastAction("Export PDF")} />
381
+ <Menu.Item id="csv" label="As CSV" onActivate={() => setLastAction("Export CSV")} />
382
+ </Menu.Submenu>
383
+ </Menu.Section>
384
+ <Menu.Section>
385
+ <Menu.Item id="quit" label="Quit" onActivate={quit} accels="<Control>q" />
386
+ </Menu.Section>
387
+ </GtkPopoverMenu>
388
+ </Slot>
389
+ </GtkMenuButton>
390
+ <GtkLabel label={`Last action: ${lastAction ?? "(none)"}`} />
391
+ </GtkBox>
392
+ );
393
+ };
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Async Data Loading
399
+
400
+ ```tsx
401
+ import * as Gtk from "@gtkx/ffi/gtk";
402
+ import { AdwSpinner, GtkBox, GtkLabel, GtkScrolledWindow, ListView, ListItem } from "@gtkx/react";
403
+ import { useEffect, useState } from "react";
404
+
405
+ interface User {
406
+ id: string;
407
+ name: string;
408
+ email: string;
409
+ }
410
+
411
+ const AsyncList = () => {
412
+ const [users, setUsers] = useState<User[]>([]);
413
+ const [loading, setLoading] = useState(true);
414
+ const [error, setError] = useState<string | null>(null);
415
+
416
+ useEffect(() => {
417
+ const fetchUsers = async () => {
418
+ try {
419
+ const response = await fetch("https://api.example.com/users");
420
+ const data = await response.json();
421
+ setUsers(data);
422
+ } catch (err) {
423
+ setError(err instanceof Error ? err.message : "Failed to load");
424
+ } finally {
425
+ setLoading(false);
426
+ }
427
+ };
428
+ fetchUsers();
429
+ }, []);
430
+
431
+ if (loading) {
432
+ return (
433
+ <GtkBox vexpand halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
434
+ <AdwSpinner widthRequest={32} heightRequest={32} />
435
+ </GtkBox>
436
+ );
437
+ }
438
+
439
+ if (error) {
440
+ return <GtkLabel label={`Error: ${error}`} cssClasses={["error"]} vexpand halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} />;
441
+ }
442
+
443
+ return (
444
+ <GtkScrolledWindow vexpand>
445
+ <ListView<User>
446
+ renderItem={(user) => (
447
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} marginStart={12} marginTop={8} marginBottom={8}>
448
+ <GtkLabel label={user?.name ?? ""} halign={Gtk.Align.START} cssClasses={["heading"]} />
449
+ <GtkLabel label={user?.email ?? ""} halign={Gtk.Align.START} cssClasses={["dim-label"]} />
450
+ </GtkBox>
451
+ )}
452
+ >
453
+ {users.map((user) => (
454
+ <ListItem key={user.id} id={user.id} value={user} />
455
+ ))}
456
+ </ListView>
457
+ </GtkScrolledWindow>
458
+ );
459
+ };
460
+ ```
461
+
462
+ ---
463
+
464
+ ## Reusable Component Pattern
465
+
466
+ ```tsx
467
+ import * as Gtk from "@gtkx/ffi/gtk";
468
+ import { GtkBox, GtkButton, GtkLabel } from "@gtkx/react";
469
+ import type { ReactNode } from "react";
470
+
471
+ interface CardProps {
472
+ title: string;
473
+ children: ReactNode;
474
+ onAction?: () => void;
475
+ actionLabel?: string;
476
+ }
477
+
478
+ const Card = ({ title, children, onAction, actionLabel }: CardProps) => (
479
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={8} cssClasses={["card"]} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
480
+ <GtkLabel label={title} cssClasses={["title-4"]} halign={Gtk.Align.START} />
481
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={4}>
482
+ {children}
483
+ </GtkBox>
484
+ {onAction && actionLabel && (
485
+ <GtkButton label={actionLabel} onClicked={onAction} halign={Gtk.Align.END} cssClasses={["flat"]} />
486
+ )}
487
+ </GtkBox>
488
+ );
489
+
490
+ const CardDemo = () => (
491
+ <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
492
+ <Card title="Welcome" actionLabel="Learn More" onAction={() => console.log("clicked")}>
493
+ <GtkLabel label="This is a reusable card component." wrap />
494
+ </Card>
495
+ <Card title="Features">
496
+ <GtkLabel label="Build native GTK apps with React." wrap />
497
+ </Card>
498
+ </GtkBox>
499
+ );
500
+ ```