@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.
- package/dist/cli.js +6 -0
- package/dist/mcp-client.d.ts +30 -0
- package/dist/mcp-client.js +401 -0
- package/dist/refresh-runtime.d.ts +27 -0
- package/dist/refresh-runtime.js +27 -0
- package/package.json +17 -5
- package/templates/claude/EXAMPLES.md.ejs +372 -234
- package/templates/claude/SKILL.md.ejs +29 -319
- package/templates/claude/WIDGETS.md.ejs +213 -332
|
@@ -1,151 +1,148 @@
|
|
|
1
1
|
# GTKX Code Examples
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## App Structure
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Minimal App
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
|
-
import
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
16
|
+
render(<App />, "com.example.hello");
|
|
17
|
+
```
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
setTodos((prev) => [...prev, { id: nextId++, text, completed: false }]);
|
|
25
|
-
}, []);
|
|
19
|
+
### Modern Adwaita App
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
57
|
+
export const appId = "com.example.myapp";
|
|
45
58
|
```
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
---
|
|
48
61
|
|
|
49
|
-
|
|
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
|
|
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={
|
|
80
|
+
<GtkGrid rowSpacing={12} columnSpacing={12}>
|
|
62
81
|
<GridChild column={0} row={0}>
|
|
63
|
-
<GtkLabel label="
|
|
82
|
+
<GtkLabel label="Email:" halign={Gtk.Align.END} />
|
|
64
83
|
</GridChild>
|
|
65
84
|
<GridChild column={1} row={0}>
|
|
66
|
-
<GtkEntry text={
|
|
85
|
+
<GtkEntry text={email} onChanged={(e) => setEmail(e.getText())} hexpand />
|
|
67
86
|
</GridChild>
|
|
68
87
|
<GridChild column={0} row={1}>
|
|
69
|
-
<GtkLabel label="
|
|
88
|
+
<GtkLabel label="Password:" halign={Gtk.Align.END} />
|
|
70
89
|
</GridChild>
|
|
71
90
|
<GridChild column={1} row={1}>
|
|
72
|
-
<GtkEntry text={
|
|
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="
|
|
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
|
-
###
|
|
101
|
+
### List with CRUD Operations
|
|
83
102
|
|
|
84
103
|
```tsx
|
|
85
104
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
86
|
-
import { GtkBox, GtkLabel,
|
|
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
|
|
108
|
+
interface Todo {
|
|
116
109
|
id: string;
|
|
117
|
-
|
|
118
|
-
completed: boolean;
|
|
110
|
+
text: string;
|
|
119
111
|
}
|
|
120
112
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
];
|
|
113
|
+
let nextId = 1;
|
|
114
|
+
|
|
115
|
+
const TodoList = () => {
|
|
116
|
+
const [todos, setTodos] = useState<Todo[]>([]);
|
|
117
|
+
const [input, setInput] = useState("");
|
|
125
118
|
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
|
131
|
-
<
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
{
|
|
148
|
-
<ListItem key={
|
|
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
|
-
|
|
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 {
|
|
162
|
+
import { GtkBox, GtkLabel, GtkPaned, GtkScrolledWindow, GtkStack, ListView, ListItem, Slot, StackPage } from "@gtkx/react";
|
|
162
163
|
import { useState } from "react";
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
<
|
|
169
|
-
<Slot for={
|
|
170
|
-
<
|
|
171
|
-
<
|
|
172
|
-
{
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
>
|
|
191
|
+
{pages.map((page) => (
|
|
192
|
+
<ListItem key={page.id} id={page.id} value={page} />
|
|
193
|
+
))}
|
|
194
|
+
</ListView>
|
|
195
|
+
</GtkScrolledWindow>
|
|
180
196
|
</Slot>
|
|
181
|
-
<
|
|
182
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
197
|
-
const [
|
|
216
|
+
const AppWithNavigation = () => {
|
|
217
|
+
const [page, setPage] = useState("home");
|
|
198
218
|
|
|
199
219
|
return (
|
|
200
|
-
<
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
<
|
|
208
|
-
</
|
|
209
|
-
</
|
|
210
|
-
</
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
+
```
|