create-gtkx 0.21.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/LICENSE +373 -0
- package/bin/create-gtkx.js +5 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +1 -0
- package/dist/command.d.ts +65 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +69 -0
- package/dist/command.js.map +1 -0
- package/dist/create.d.ts +3 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +7 -0
- package/dist/create.js.map +1 -0
- package/dist/deps.d.ts +3 -0
- package/dist/deps.d.ts.map +1 -0
- package/dist/deps.js +51 -0
- package/dist/deps.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +25 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +13 -0
- package/dist/options.js.map +1 -0
- package/dist/scaffolder.d.ts +51 -0
- package/dist/scaffolder.d.ts.map +1 -0
- package/dist/scaffolder.js +191 -0
- package/dist/scaffolder.js.map +1 -0
- package/dist/templates.d.ts +11 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +25 -0
- package/dist/templates.js.map +1 -0
- package/package.json +65 -0
- package/src/cli.ts +21 -0
- package/src/command.ts +87 -0
- package/src/create.ts +7 -0
- package/src/deps.ts +51 -0
- package/src/index.ts +4 -0
- package/src/options.ts +24 -0
- package/src/scaffolder.ts +306 -0
- package/src/templates.ts +42 -0
- package/templates/claude/EXAMPLES.md.ejs +790 -0
- package/templates/claude/SKILL.md.ejs +126 -0
- package/templates/claude/WIDGETS.md.ejs +934 -0
- package/templates/config/vitest.config.ts.ejs +11 -0
- package/templates/gitignore.ejs +4 -0
- package/templates/gtkx.config.ts.ejs +6 -0
- package/templates/package.json.ejs +16 -0
- package/templates/src/app.tsx.ejs +48 -0
- package/templates/src/gtkx-env.d.ts.ejs +2 -0
- package/templates/src/index.tsx.ejs +4 -0
- package/templates/tests/app.test.tsx.ejs +16 -0
- package/templates/tsconfig.json.ejs +14 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
# GTKX Code Examples
|
|
2
|
+
|
|
3
|
+
## App Structure
|
|
4
|
+
|
|
5
|
+
### Minimal App
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// src/index.tsx
|
|
9
|
+
import { applicationId } from "virtual:gtkx-config";
|
|
10
|
+
import { GtkApplication, GtkApplicationWindow, GtkLabel } from "@gtkx/jsx/gtk";
|
|
11
|
+
import { createRoot, quit } from "@gtkx/react";
|
|
12
|
+
|
|
13
|
+
const App = () => (
|
|
14
|
+
<GtkApplication applicationId={applicationId}>
|
|
15
|
+
<GtkApplicationWindow
|
|
16
|
+
title="Hello"
|
|
17
|
+
defaultWidth={400}
|
|
18
|
+
defaultHeight={300}
|
|
19
|
+
onCloseRequest={() => {
|
|
20
|
+
quit();
|
|
21
|
+
return true;
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<GtkLabel label="Hello, World!" />
|
|
25
|
+
</GtkApplicationWindow>
|
|
26
|
+
</GtkApplication>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
createRoot().render(<App />);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The entry calls `createRoot().render()` directly (similar to `createRoot` in `react-dom`). The `<GtkApplication>` component constructs, registers, and activates the GTK application; pass its `applicationId` explicitly, reading the one declared in `gtkx.config.ts` from the `virtual:gtkx-config` module. `gtkx dev` restarts the Node process on entry-level changes so the top-level call only runs once per process life.
|
|
33
|
+
|
|
34
|
+
### Modern Adwaita App
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { applicationId } from "virtual:gtkx-config";
|
|
38
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
39
|
+
import { AdwApplication, AdwApplicationWindow, AdwHeaderBar, AdwToolbarView, AdwWindowTitle, AdwStatusPage } from "@gtkx/jsx/adw";
|
|
40
|
+
import { quit } from "@gtkx/react";
|
|
41
|
+
import { GtkButton } from "@gtkx/jsx/gtk";
|
|
42
|
+
|
|
43
|
+
const MainWindow = () => (
|
|
44
|
+
<AdwApplicationWindow
|
|
45
|
+
title="My App"
|
|
46
|
+
defaultWidth={800}
|
|
47
|
+
defaultHeight={600}
|
|
48
|
+
onCloseRequest={() => {
|
|
49
|
+
quit();
|
|
50
|
+
return true;
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<AdwToolbarView
|
|
54
|
+
addTopBar={
|
|
55
|
+
<AdwHeaderBar titleWidget={<AdwWindowTitle title="My App" subtitle="Welcome" />} />
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
<AdwStatusPage
|
|
59
|
+
iconName="applications-system-symbolic"
|
|
60
|
+
title="Welcome"
|
|
61
|
+
description="Get started with your GTKX app"
|
|
62
|
+
vexpand
|
|
63
|
+
>
|
|
64
|
+
<GtkButton label="Get Started" cssClasses={["suggested-action", "pill"]} halign={Gtk.Align.CENTER} />
|
|
65
|
+
</AdwStatusPage>
|
|
66
|
+
</AdwToolbarView>
|
|
67
|
+
</AdwApplicationWindow>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const App = () => (
|
|
71
|
+
<AdwApplication applicationId={applicationId}>
|
|
72
|
+
<MainWindow />
|
|
73
|
+
</AdwApplication>
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## State Management
|
|
80
|
+
|
|
81
|
+
### Controlled Form
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
85
|
+
import { GtkButton, GtkEntry, GtkGrid, GtkGridChild, GtkLabel } from "@gtkx/jsx/gtk";
|
|
86
|
+
import { useState } from "react";
|
|
87
|
+
|
|
88
|
+
const LoginForm = () => {
|
|
89
|
+
const [email, setEmail] = useState("");
|
|
90
|
+
const [password, setPassword] = useState("");
|
|
91
|
+
|
|
92
|
+
const handleSubmit = () => {
|
|
93
|
+
console.log("Login:", { email, password });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<GtkGrid rowSpacing={12} columnSpacing={12}>
|
|
98
|
+
<GtkGridChild column={0} row={0}>
|
|
99
|
+
<GtkLabel label="Email:" halign={Gtk.Align.END} />
|
|
100
|
+
</GtkGridChild>
|
|
101
|
+
<GtkGridChild column={1} row={0}>
|
|
102
|
+
<GtkEntry text={email} onChanged={(e) => setEmail(e.getText())} hexpand />
|
|
103
|
+
</GtkGridChild>
|
|
104
|
+
<GtkGridChild column={0} row={1}>
|
|
105
|
+
<GtkLabel label="Password:" halign={Gtk.Align.END} />
|
|
106
|
+
</GtkGridChild>
|
|
107
|
+
<GtkGridChild column={1} row={1}>
|
|
108
|
+
<GtkEntry text={password} onChanged={(e) => setPassword(e.getText())} visibility={false} hexpand />
|
|
109
|
+
</GtkGridChild>
|
|
110
|
+
<GtkGridChild column={0} row={2} columnSpan={2}>
|
|
111
|
+
<GtkButton label="Login" onClicked={handleSubmit} cssClasses={["suggested-action"]} halign={Gtk.Align.END} />
|
|
112
|
+
</GtkGridChild>
|
|
113
|
+
</GtkGrid>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### List with CRUD Operations
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
122
|
+
import { GtkBox, GtkButton, GtkEntry, GtkLabel, GtkListView, GtkScrolledWindow } from "@gtkx/jsx/gtk";
|
|
123
|
+
import { useCallback, useState } from "react";
|
|
124
|
+
|
|
125
|
+
interface Todo {
|
|
126
|
+
id: string;
|
|
127
|
+
text: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let nextId = 1;
|
|
131
|
+
|
|
132
|
+
const TodoList = () => {
|
|
133
|
+
const [todos, setTodos] = useState<Todo[]>([]);
|
|
134
|
+
const [input, setInput] = useState("");
|
|
135
|
+
|
|
136
|
+
const addTodo = useCallback(() => {
|
|
137
|
+
if (!input.trim()) return;
|
|
138
|
+
setTodos((prev) => [...prev, { id: String(nextId++), text: input }]);
|
|
139
|
+
setInput("");
|
|
140
|
+
}, [input]);
|
|
141
|
+
|
|
142
|
+
const deleteTodo = useCallback((id: string) => {
|
|
143
|
+
setTodos((prev) => prev.filter((t) => t.id !== id));
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} marginStart={16} marginEnd={16} marginTop={16} marginBottom={16}>
|
|
148
|
+
<GtkBox spacing={8}>
|
|
149
|
+
<GtkEntry text={input} onChanged={(e) => setInput(e.getText())} hexpand placeholderText="New todo..." />
|
|
150
|
+
<GtkButton label="Add" onClicked={addTodo} cssClasses={["suggested-action"]} />
|
|
151
|
+
</GtkBox>
|
|
152
|
+
<GtkScrolledWindow vexpand cssClasses={["card"]}>
|
|
153
|
+
<GtkListView
|
|
154
|
+
items={todos.map((todo) => ({ id: todo.id, value: todo }))}
|
|
155
|
+
renderItem={(todo: Todo) => (
|
|
156
|
+
<GtkBox spacing={8} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
|
|
157
|
+
<GtkLabel label={todo.text} hexpand halign={Gtk.Align.START} />
|
|
158
|
+
<GtkButton iconName="edit-delete-symbolic" cssClasses={["flat"]} onClicked={() => deleteTodo(todo.id)} />
|
|
159
|
+
</GtkBox>
|
|
160
|
+
)}
|
|
161
|
+
/>
|
|
162
|
+
</GtkScrolledWindow>
|
|
163
|
+
</GtkBox>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Navigation Patterns
|
|
171
|
+
|
|
172
|
+
### Stack with Sidebar Navigation
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
176
|
+
import { GtkLabel, GtkListView, GtkPaned, GtkScrolledWindow, GtkStack, GtkStackPage } from "@gtkx/jsx/gtk";
|
|
177
|
+
import { useState } from "react";
|
|
178
|
+
|
|
179
|
+
interface Page {
|
|
180
|
+
id: string;
|
|
181
|
+
name: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const pages: Page[] = [
|
|
185
|
+
{ id: "home", name: "Home" },
|
|
186
|
+
{ id: "settings", name: "Settings" },
|
|
187
|
+
{ id: "about", name: "About" },
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const SidebarNav = () => {
|
|
191
|
+
const [currentPage, setCurrentPage] = useState("home");
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<GtkPaned
|
|
195
|
+
position={200}
|
|
196
|
+
startChild={
|
|
197
|
+
<GtkScrolledWindow cssClasses={["sidebar"]}>
|
|
198
|
+
<GtkListView
|
|
199
|
+
selected={[currentPage]}
|
|
200
|
+
selectionMode={Gtk.SelectionMode.SINGLE}
|
|
201
|
+
onSelectionChanged={(ids) => setCurrentPage(ids[0])}
|
|
202
|
+
items={pages.map((page) => ({ id: page.id, value: page }))}
|
|
203
|
+
renderItem={(page: Page) => (
|
|
204
|
+
<GtkLabel label={page.name} halign={Gtk.Align.START} marginStart={12} marginTop={8} marginBottom={8} />
|
|
205
|
+
)}
|
|
206
|
+
/>
|
|
207
|
+
</GtkScrolledWindow>
|
|
208
|
+
}
|
|
209
|
+
endChild={
|
|
210
|
+
<GtkStack visibleChildName={currentPage}>
|
|
211
|
+
<GtkStackPage id="home"><GtkLabel label="Home Content" vexpand /></GtkStackPage>
|
|
212
|
+
<GtkStackPage id="settings"><GtkLabel label="Settings Content" vexpand /></GtkStackPage>
|
|
213
|
+
<GtkStackPage id="about"><GtkLabel label="About Content" vexpand /></GtkStackPage>
|
|
214
|
+
</GtkStack>
|
|
215
|
+
}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Header Bar with Back Navigation
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
225
|
+
import { quit } from "@gtkx/react";
|
|
226
|
+
import { GtkApplicationWindow, GtkBox, GtkButton, GtkHeaderBar, GtkLabel, GtkStack, GtkStackPage } from "@gtkx/jsx/gtk";
|
|
227
|
+
import { useState } from "react";
|
|
228
|
+
|
|
229
|
+
const AppWithNavigation = () => {
|
|
230
|
+
const [page, setPage] = useState("home");
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<GtkApplicationWindow
|
|
234
|
+
title="App"
|
|
235
|
+
defaultWidth={600}
|
|
236
|
+
defaultHeight={400}
|
|
237
|
+
onCloseRequest={() => {
|
|
238
|
+
quit();
|
|
239
|
+
return true;
|
|
240
|
+
}}
|
|
241
|
+
titlebar={
|
|
242
|
+
<GtkHeaderBar
|
|
243
|
+
packStart={
|
|
244
|
+
page !== "home" && <GtkButton iconName="go-previous-symbolic" onClicked={() => setPage("home")} />
|
|
245
|
+
}
|
|
246
|
+
titleWidget={<GtkLabel label={page === "home" ? "Home" : "Details"} cssClasses={["title"]} />}
|
|
247
|
+
/>
|
|
248
|
+
}
|
|
249
|
+
>
|
|
250
|
+
<GtkStack visibleChildName={page}>
|
|
251
|
+
<GtkStackPage id="home">
|
|
252
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} vexpand halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
|
|
253
|
+
<GtkLabel label="Welcome" />
|
|
254
|
+
<GtkButton label="Go to Details" onClicked={() => setPage("details")} />
|
|
255
|
+
</GtkBox>
|
|
256
|
+
</GtkStackPage>
|
|
257
|
+
<GtkStackPage id="details">
|
|
258
|
+
<GtkLabel label="Details Page Content" vexpand />
|
|
259
|
+
</GtkStackPage>
|
|
260
|
+
</GtkStack>
|
|
261
|
+
</GtkApplicationWindow>
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Settings Page
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
272
|
+
import { AdwPreferencesPage, AdwPreferencesGroup, AdwActionRow, AdwSwitchRow, AdwExpanderRow, AdwEntryRow } from "@gtkx/jsx/adw";
|
|
273
|
+
import { GtkImage, GtkScrolledWindow } from "@gtkx/jsx/gtk";
|
|
274
|
+
import { useState } from "react";
|
|
275
|
+
|
|
276
|
+
const SettingsPage = () => {
|
|
277
|
+
const [darkMode, setDarkMode] = useState(false);
|
|
278
|
+
const [notifications, setNotifications] = useState(true);
|
|
279
|
+
const [username, setUsername] = useState("");
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<GtkScrolledWindow vexpand>
|
|
283
|
+
<AdwPreferencesPage title="Settings">
|
|
284
|
+
<AdwPreferencesGroup title="Appearance">
|
|
285
|
+
<AdwSwitchRow title="Dark Mode" subtitle="Use dark color scheme" active={darkMode} onNotifyActive={(active) => setDarkMode(active ?? false)} />
|
|
286
|
+
</AdwPreferencesGroup>
|
|
287
|
+
|
|
288
|
+
<AdwPreferencesGroup title="Account">
|
|
289
|
+
<AdwEntryRow title="Username" text={username} onChanged={(e) => setUsername(e.getText())} />
|
|
290
|
+
<AdwActionRow
|
|
291
|
+
title="Profile"
|
|
292
|
+
subtitle="Manage your profile"
|
|
293
|
+
addSuffix={<GtkImage iconName="go-next-symbolic" valign={Gtk.Align.CENTER} />}
|
|
294
|
+
/>
|
|
295
|
+
</AdwPreferencesGroup>
|
|
296
|
+
|
|
297
|
+
<AdwPreferencesGroup title="Notifications">
|
|
298
|
+
<AdwExpanderRow
|
|
299
|
+
title="Notification Settings"
|
|
300
|
+
subtitle="Configure alerts"
|
|
301
|
+
addRow={
|
|
302
|
+
<>
|
|
303
|
+
<AdwSwitchRow title="Sound" active />
|
|
304
|
+
<AdwSwitchRow title="Badges" active />
|
|
305
|
+
<AdwSwitchRow title="Lock Screen" />
|
|
306
|
+
</>
|
|
307
|
+
}
|
|
308
|
+
/>
|
|
309
|
+
</AdwPreferencesGroup>
|
|
310
|
+
</AdwPreferencesPage>
|
|
311
|
+
</GtkScrolledWindow>
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Data Table with Sorting
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
322
|
+
import { GtkColumnView, GtkColumnViewColumn, GtkLabel, GtkScrolledWindow } from "@gtkx/jsx/gtk";
|
|
323
|
+
import { useMemo, useState } from "react";
|
|
324
|
+
|
|
325
|
+
interface FileItem {
|
|
326
|
+
id: string;
|
|
327
|
+
name: string;
|
|
328
|
+
size: number;
|
|
329
|
+
modified: string;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const files: FileItem[] = [
|
|
333
|
+
{ id: "1", name: "document.pdf", size: 1024, modified: "2024-01-15" },
|
|
334
|
+
{ id: "2", name: "image.png", size: 2048, modified: "2024-01-14" },
|
|
335
|
+
{ id: "3", name: "notes.txt", size: 512, modified: "2024-01-13" },
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const FileTable = () => {
|
|
339
|
+
const [sortColumn, setSortColumn] = useState("name");
|
|
340
|
+
const [sortOrder, setSortOrder] = useState(Gtk.SortType.ASCENDING);
|
|
341
|
+
|
|
342
|
+
const sortedFiles = useMemo(() => {
|
|
343
|
+
const sorted = [...files].sort((a, b) => {
|
|
344
|
+
const aVal = a[sortColumn as keyof FileItem];
|
|
345
|
+
const bVal = b[sortColumn as keyof FileItem];
|
|
346
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
347
|
+
return sortOrder === Gtk.SortType.ASCENDING ? cmp : -cmp;
|
|
348
|
+
});
|
|
349
|
+
return sorted;
|
|
350
|
+
}, [sortColumn, sortOrder]);
|
|
351
|
+
|
|
352
|
+
const handleSort = (column: string, order: Gtk.SortType) => {
|
|
353
|
+
setSortColumn(column);
|
|
354
|
+
setSortOrder(order);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<GtkScrolledWindow vexpand cssClasses={["card"]}>
|
|
359
|
+
<GtkColumnView
|
|
360
|
+
estimatedRowHeight={48}
|
|
361
|
+
sortColumn={sortColumn}
|
|
362
|
+
sortOrder={sortOrder}
|
|
363
|
+
onSortChanged={handleSort}
|
|
364
|
+
items={sortedFiles.map((file) => ({ id: file.id, value: file }))}
|
|
365
|
+
>
|
|
366
|
+
<GtkColumnViewColumn title="Name" id="name" expand sortable renderCell={(f: FileItem) => <GtkLabel label={f.name} />} />
|
|
367
|
+
<GtkColumnViewColumn title="Size" id="size" fixedWidth={100} sortable renderCell={(f: FileItem) => <GtkLabel label={`${f.size} KB`} />} />
|
|
368
|
+
<GtkColumnViewColumn title="Modified" id="modified" fixedWidth={120} sortable renderCell={(f: FileItem) => <GtkLabel label={f.modified} />} />
|
|
369
|
+
</GtkColumnView>
|
|
370
|
+
</GtkScrolledWindow>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Menu with Keyboard Shortcuts
|
|
378
|
+
|
|
379
|
+
Menus are data: `<GMenu>` takes an `items` array of `MenuEntry` objects whose `action` names reference `<GSimpleAction>` elements installed through the window's `addAction` prop.
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
383
|
+
import { quit } from "@gtkx/react";
|
|
384
|
+
import { GMenu, GSimpleAction } from "@gtkx/jsx/gio";
|
|
385
|
+
import { GtkApplicationWindow, GtkBox, GtkLabel, GtkMenuButton } from "@gtkx/jsx/gtk";
|
|
386
|
+
import { useState } from "react";
|
|
387
|
+
|
|
388
|
+
const MenuDemo = () => {
|
|
389
|
+
const [lastAction, setLastAction] = useState<string | null>(null);
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<GtkApplicationWindow
|
|
393
|
+
title="Menu Demo"
|
|
394
|
+
onCloseRequest={() => {
|
|
395
|
+
quit();
|
|
396
|
+
return true;
|
|
397
|
+
}}
|
|
398
|
+
addAction={
|
|
399
|
+
<>
|
|
400
|
+
<GSimpleAction name="new" onActivate={() => setLastAction("New")} accels="<Control>n" />
|
|
401
|
+
<GSimpleAction name="open" onActivate={() => setLastAction("Open")} accels="<Control>o" />
|
|
402
|
+
<GSimpleAction name="save" onActivate={() => setLastAction("Save")} accels="<Control>s" />
|
|
403
|
+
<GSimpleAction name="export-pdf" onActivate={() => setLastAction("Export PDF")} />
|
|
404
|
+
<GSimpleAction name="export-csv" onActivate={() => setLastAction("Export CSV")} />
|
|
405
|
+
<GSimpleAction name="quit" onActivate={quit} accels="<Control>q" />
|
|
406
|
+
</>
|
|
407
|
+
}
|
|
408
|
+
>
|
|
409
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} marginStart={16} marginEnd={16} marginTop={16} marginBottom={16}>
|
|
410
|
+
<GtkMenuButton
|
|
411
|
+
label="File"
|
|
412
|
+
halign={Gtk.Align.START}
|
|
413
|
+
menuModel={
|
|
414
|
+
<GMenu
|
|
415
|
+
items={[
|
|
416
|
+
{ label: "_New", action: "win.new" },
|
|
417
|
+
{ label: "_Open", action: "win.open" },
|
|
418
|
+
{ label: "_Save", action: "win.save" },
|
|
419
|
+
{
|
|
420
|
+
label: "Export",
|
|
421
|
+
submenu: [
|
|
422
|
+
{ label: "As PDF", action: "win.export-pdf" },
|
|
423
|
+
{ label: "As CSV", action: "win.export-csv" },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
{ section: [{ label: "_Quit", action: "win.quit" }] },
|
|
427
|
+
]}
|
|
428
|
+
/>
|
|
429
|
+
}
|
|
430
|
+
/>
|
|
431
|
+
<GtkLabel label={`Last action: ${lastAction ?? "(none)"}`} />
|
|
432
|
+
</GtkBox>
|
|
433
|
+
</GtkApplicationWindow>
|
|
434
|
+
);
|
|
435
|
+
};
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Async Data Loading
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
444
|
+
import { AdwSpinner } from "@gtkx/jsx/adw";
|
|
445
|
+
import { GtkBox, GtkLabel, GtkListView, GtkScrolledWindow } from "@gtkx/jsx/gtk";
|
|
446
|
+
import { useEffect, useState } from "react";
|
|
447
|
+
|
|
448
|
+
interface User {
|
|
449
|
+
id: string;
|
|
450
|
+
name: string;
|
|
451
|
+
email: string;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const AsyncList = () => {
|
|
455
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
456
|
+
const [loading, setLoading] = useState(true);
|
|
457
|
+
const [error, setError] = useState<string | null>(null);
|
|
458
|
+
|
|
459
|
+
useEffect(() => {
|
|
460
|
+
const fetchUsers = async () => {
|
|
461
|
+
try {
|
|
462
|
+
const response = await fetch("https://api.example.com/users");
|
|
463
|
+
const data = await response.json();
|
|
464
|
+
setUsers(data);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
setError(error instanceof Error ? error.message : "Failed to load");
|
|
467
|
+
} finally {
|
|
468
|
+
setLoading(false);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
fetchUsers();
|
|
472
|
+
}, []);
|
|
473
|
+
|
|
474
|
+
if (loading) {
|
|
475
|
+
return (
|
|
476
|
+
<GtkBox vexpand halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
|
|
477
|
+
<AdwSpinner widthRequest={32} heightRequest={32} />
|
|
478
|
+
</GtkBox>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (error) {
|
|
483
|
+
return <GtkLabel label={`Error: ${error}`} cssClasses={["error"]} vexpand halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} />;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<GtkScrolledWindow vexpand>
|
|
488
|
+
<GtkListView
|
|
489
|
+
items={users.map((user) => ({ id: user.id, value: user }))}
|
|
490
|
+
renderItem={(user: User) => (
|
|
491
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} marginStart={12} marginTop={8} marginBottom={8}>
|
|
492
|
+
<GtkLabel label={user.name} halign={Gtk.Align.START} cssClasses={["heading"]} />
|
|
493
|
+
<GtkLabel label={user.email} halign={Gtk.Align.START} cssClasses={["dim-label"]} />
|
|
494
|
+
</GtkBox>
|
|
495
|
+
)}
|
|
496
|
+
/>
|
|
497
|
+
</GtkScrolledWindow>
|
|
498
|
+
);
|
|
499
|
+
};
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Reusable Component Pattern
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
508
|
+
import { GtkBox, GtkButton, GtkLabel } from "@gtkx/jsx/gtk";
|
|
509
|
+
import type { ReactNode } from "react";
|
|
510
|
+
|
|
511
|
+
interface CardProps {
|
|
512
|
+
title: string;
|
|
513
|
+
children: ReactNode;
|
|
514
|
+
onAction?: () => void;
|
|
515
|
+
actionLabel?: string;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const Card = ({ title, children, onAction, actionLabel }: CardProps) => (
|
|
519
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={8} cssClasses={["card"]} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
|
|
520
|
+
<GtkLabel label={title} cssClasses={["title-4"]} halign={Gtk.Align.START} />
|
|
521
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={4}>
|
|
522
|
+
{children}
|
|
523
|
+
</GtkBox>
|
|
524
|
+
{onAction && actionLabel && (
|
|
525
|
+
<GtkButton label={actionLabel} onClicked={onAction} halign={Gtk.Align.END} cssClasses={["flat"]} />
|
|
526
|
+
)}
|
|
527
|
+
</GtkBox>
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const CardDemo = () => (
|
|
531
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
532
|
+
<Card title="Welcome" actionLabel="Learn More" onAction={() => console.log("clicked")}>
|
|
533
|
+
<GtkLabel label="This is a reusable card component." wrap />
|
|
534
|
+
</Card>
|
|
535
|
+
<Card title="Features">
|
|
536
|
+
<GtkLabel label="Build native GTK apps with React." wrap />
|
|
537
|
+
</Card>
|
|
538
|
+
</GtkBox>
|
|
539
|
+
);
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Navigation with AdwNavigationView
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
548
|
+
import { quit } from "@gtkx/react";
|
|
549
|
+
import { AdwApplicationWindow, AdwHeaderBar, AdwNavigationPage, AdwNavigationView, AdwToolbarView } from "@gtkx/jsx/adw";
|
|
550
|
+
import { GtkBox, GtkButton, GtkLabel } from "@gtkx/jsx/gtk";
|
|
551
|
+
import { useState } from "react";
|
|
552
|
+
|
|
553
|
+
const NavigationDemo = () => {
|
|
554
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
555
|
+
|
|
556
|
+
return (
|
|
557
|
+
<AdwApplicationWindow
|
|
558
|
+
title="Navigation Demo"
|
|
559
|
+
defaultWidth={600}
|
|
560
|
+
defaultHeight={400}
|
|
561
|
+
onCloseRequest={() => {
|
|
562
|
+
quit();
|
|
563
|
+
return true;
|
|
564
|
+
}}
|
|
565
|
+
>
|
|
566
|
+
<AdwNavigationView
|
|
567
|
+
visiblePageTag={showSettings ? "settings" : "home"}
|
|
568
|
+
onPopped={() => setShowSettings(false)}
|
|
569
|
+
>
|
|
570
|
+
<AdwNavigationPage tag="home" title="Home">
|
|
571
|
+
<AdwToolbarView addTopBar={<AdwHeaderBar />}>
|
|
572
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
|
|
573
|
+
<GtkLabel label="Welcome!" cssClasses={["title-1"]} />
|
|
574
|
+
<GtkButton label="Go to Settings" onClicked={() => setShowSettings(true)} cssClasses={["suggested-action"]} />
|
|
575
|
+
</GtkBox>
|
|
576
|
+
</AdwToolbarView>
|
|
577
|
+
</AdwNavigationPage>
|
|
578
|
+
{showSettings && (
|
|
579
|
+
<AdwNavigationPage tag="settings" title="Settings">
|
|
580
|
+
<AdwToolbarView addTopBar={<AdwHeaderBar />}>
|
|
581
|
+
<GtkLabel label="Settings page content" vexpand />
|
|
582
|
+
</AdwToolbarView>
|
|
583
|
+
</AdwNavigationPage>
|
|
584
|
+
)}
|
|
585
|
+
</AdwNavigationView>
|
|
586
|
+
</AdwApplicationWindow>
|
|
587
|
+
);
|
|
588
|
+
};
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Sidebar/Content Split with AdwNavigationSplitView
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
597
|
+
import { quit } from "@gtkx/react";
|
|
598
|
+
import { AdwActionRow, AdwApplicationWindow, AdwHeaderBar, AdwNavigationPage, AdwNavigationSplitView, AdwToolbarView } from "@gtkx/jsx/adw";
|
|
599
|
+
import { GtkBox, GtkImage, GtkLabel, GtkListBox, GtkScrolledWindow } from "@gtkx/jsx/gtk";
|
|
600
|
+
import { useState } from "react";
|
|
601
|
+
|
|
602
|
+
interface Item {
|
|
603
|
+
id: string;
|
|
604
|
+
title: string;
|
|
605
|
+
icon: string;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const items: Item[] = [
|
|
609
|
+
{ id: "inbox", title: "Inbox", icon: "mail-unread-symbolic" },
|
|
610
|
+
{ id: "starred", title: "Starred", icon: "starred-symbolic" },
|
|
611
|
+
{ id: "sent", title: "Sent", icon: "mail-send-symbolic" },
|
|
612
|
+
];
|
|
613
|
+
|
|
614
|
+
const SplitViewDemo = () => {
|
|
615
|
+
const [selected, setSelected] = useState(items[0]);
|
|
616
|
+
|
|
617
|
+
return (
|
|
618
|
+
<AdwApplicationWindow
|
|
619
|
+
title="Split View Demo"
|
|
620
|
+
defaultWidth={800}
|
|
621
|
+
defaultHeight={500}
|
|
622
|
+
onCloseRequest={() => {
|
|
623
|
+
quit();
|
|
624
|
+
return true;
|
|
625
|
+
}}
|
|
626
|
+
>
|
|
627
|
+
<AdwNavigationSplitView
|
|
628
|
+
sidebarWidthFraction={0.33}
|
|
629
|
+
minSidebarWidth={200}
|
|
630
|
+
maxSidebarWidth={300}
|
|
631
|
+
sidebar={
|
|
632
|
+
<AdwNavigationPage title="Mail">
|
|
633
|
+
<AdwToolbarView addTopBar={<AdwHeaderBar />}>
|
|
634
|
+
<GtkScrolledWindow vexpand>
|
|
635
|
+
<GtkListBox
|
|
636
|
+
cssClasses={["navigation-sidebar"]}
|
|
637
|
+
onRowSelected={(row) => {
|
|
638
|
+
if (!row) return;
|
|
639
|
+
const item = items[row.getIndex()];
|
|
640
|
+
if (item) setSelected(item);
|
|
641
|
+
}}
|
|
642
|
+
>
|
|
643
|
+
{items.map((item) => (
|
|
644
|
+
<AdwActionRow key={item.id} title={item.title} addPrefix={<GtkImage iconName={item.icon} />} />
|
|
645
|
+
))}
|
|
646
|
+
</GtkListBox>
|
|
647
|
+
</GtkScrolledWindow>
|
|
648
|
+
</AdwToolbarView>
|
|
649
|
+
</AdwNavigationPage>
|
|
650
|
+
}
|
|
651
|
+
content={
|
|
652
|
+
<AdwNavigationPage title={selected?.title ?? ""}>
|
|
653
|
+
<AdwToolbarView addTopBar={<AdwHeaderBar />}>
|
|
654
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} vexpand>
|
|
655
|
+
<GtkImage iconName={selected?.icon ?? ""} iconSize={Gtk.IconSize.LARGE} />
|
|
656
|
+
<GtkLabel label={selected?.title ?? ""} cssClasses={["title-2"]} />
|
|
657
|
+
</GtkBox>
|
|
658
|
+
</AdwToolbarView>
|
|
659
|
+
</AdwNavigationPage>
|
|
660
|
+
}
|
|
661
|
+
/>
|
|
662
|
+
</AdwApplicationWindow>
|
|
663
|
+
);
|
|
664
|
+
};
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## File Browser with GtkListView (tree)
|
|
670
|
+
|
|
671
|
+
```tsx
|
|
672
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
673
|
+
import { GtkBox, GtkImage, GtkLabel, GtkListView, GtkScrolledWindow } from "@gtkx/jsx/gtk";
|
|
674
|
+
import { useState } from "react";
|
|
675
|
+
|
|
676
|
+
interface FileNode {
|
|
677
|
+
id: string;
|
|
678
|
+
name: string;
|
|
679
|
+
isDirectory: boolean;
|
|
680
|
+
children?: FileNode[];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const files: FileNode[] = [
|
|
684
|
+
{
|
|
685
|
+
id: "src",
|
|
686
|
+
name: "src",
|
|
687
|
+
isDirectory: true,
|
|
688
|
+
children: [
|
|
689
|
+
{ id: "src/app.tsx", name: "app.tsx", isDirectory: false },
|
|
690
|
+
{ id: "src/index.tsx", name: "index.tsx", isDirectory: false },
|
|
691
|
+
],
|
|
692
|
+
},
|
|
693
|
+
{ id: "package.json", name: "package.json", isDirectory: false },
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
const FileBrowser = () => {
|
|
697
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
698
|
+
|
|
699
|
+
return (
|
|
700
|
+
<GtkScrolledWindow vexpand cssClasses={["card"]}>
|
|
701
|
+
<GtkListView
|
|
702
|
+
estimatedItemHeight={36}
|
|
703
|
+
vexpand
|
|
704
|
+
autoexpand={false}
|
|
705
|
+
selectionMode={Gtk.SelectionMode.SINGLE}
|
|
706
|
+
selected={selected ? [selected] : []}
|
|
707
|
+
onSelectionChanged={(ids) => setSelected(ids[0] ?? null)}
|
|
708
|
+
items={files.map((file) => ({
|
|
709
|
+
id: file.id,
|
|
710
|
+
value: file,
|
|
711
|
+
children: file.children?.map((child) => ({ id: child.id, value: child })),
|
|
712
|
+
}))}
|
|
713
|
+
renderItem={(item: FileNode) => (
|
|
714
|
+
<GtkBox spacing={8}>
|
|
715
|
+
<GtkImage iconName={item.isDirectory ? "folder-symbolic" : "text-x-generic-symbolic"} />
|
|
716
|
+
<GtkLabel label={item.name} halign={Gtk.Align.START} />
|
|
717
|
+
</GtkBox>
|
|
718
|
+
)}
|
|
719
|
+
/>
|
|
720
|
+
</GtkScrolledWindow>
|
|
721
|
+
);
|
|
722
|
+
};
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## Stack with Programmatic Navigation
|
|
728
|
+
|
|
729
|
+
```tsx
|
|
730
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
731
|
+
import { GtkBox, GtkButton, GtkLabel, GtkStack, GtkStackPage } from "@gtkx/jsx/gtk";
|
|
732
|
+
import { useState } from "react";
|
|
733
|
+
|
|
734
|
+
const StackNavigation = () => {
|
|
735
|
+
const [page, setPage] = useState("home");
|
|
736
|
+
|
|
737
|
+
return (
|
|
738
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
739
|
+
<GtkBox spacing={6} halign={Gtk.Align.CENTER}>
|
|
740
|
+
<GtkButton label="Home" onClicked={() => setPage("home")} />
|
|
741
|
+
<GtkButton label="Settings" onClicked={() => setPage("settings")} />
|
|
742
|
+
</GtkBox>
|
|
743
|
+
<GtkStack visibleChildName={page} onNotifyVisibleChildName={(name) => setPage(name ?? "home")} vexpand>
|
|
744
|
+
<GtkStackPage id="home">
|
|
745
|
+
<GtkLabel label="Home Content" />
|
|
746
|
+
</GtkStackPage>
|
|
747
|
+
<GtkStackPage id="settings">
|
|
748
|
+
<GtkLabel label="Settings Content" />
|
|
749
|
+
</GtkStackPage>
|
|
750
|
+
</GtkStack>
|
|
751
|
+
</GtkBox>
|
|
752
|
+
);
|
|
753
|
+
};
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
## Animated Card with Toggle
|
|
759
|
+
|
|
760
|
+
```tsx
|
|
761
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
762
|
+
import { AdwSpringAnimation } from "@gtkx/animate";
|
|
763
|
+
import { GtkBox, GtkButton, GtkLabel } from "@gtkx/jsx/gtk";
|
|
764
|
+
import { useState } from "react";
|
|
765
|
+
|
|
766
|
+
const AnimatedCard = () => {
|
|
767
|
+
const [visible, setVisible] = useState(true);
|
|
768
|
+
|
|
769
|
+
return (
|
|
770
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} marginStart={16} marginEnd={16} marginTop={16} marginBottom={16}>
|
|
771
|
+
<GtkButton label={visible ? "Hide Card" : "Show Card"} onClicked={() => setVisible(!visible)} halign={Gtk.Align.START} />
|
|
772
|
+
{visible && (
|
|
773
|
+
<AdwSpringAnimation
|
|
774
|
+
initial={{ opacity: 0, scale: 0.8, translateY: -20 }}
|
|
775
|
+
animate={{ opacity: 1, scale: 1, translateY: 0 }}
|
|
776
|
+
exit={{ opacity: 0, scale: 0.8, translateY: 20 }}
|
|
777
|
+
damping={0.7}
|
|
778
|
+
stiffness={200}
|
|
779
|
+
animateOnMount
|
|
780
|
+
>
|
|
781
|
+
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={8} cssClasses={["card"]} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
|
|
782
|
+
<GtkLabel label="Animated Card" cssClasses={["title-4"]} halign={Gtk.Align.START} />
|
|
783
|
+
<GtkLabel label="This card animates in with a spring effect and fades out when dismissed." wrap />
|
|
784
|
+
</GtkBox>
|
|
785
|
+
</AdwSpringAnimation>
|
|
786
|
+
)}
|
|
787
|
+
</GtkBox>
|
|
788
|
+
);
|
|
789
|
+
};
|
|
790
|
+
```
|