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.
Files changed (55) hide show
  1. package/LICENSE +373 -0
  2. package/bin/create-gtkx.js +5 -0
  3. package/dist/cli.d.ts +24 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +19 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/command.d.ts +65 -0
  8. package/dist/command.d.ts.map +1 -0
  9. package/dist/command.js +69 -0
  10. package/dist/command.js.map +1 -0
  11. package/dist/create.d.ts +3 -0
  12. package/dist/create.d.ts.map +1 -0
  13. package/dist/create.js +7 -0
  14. package/dist/create.js.map +1 -0
  15. package/dist/deps.d.ts +3 -0
  16. package/dist/deps.d.ts.map +1 -0
  17. package/dist/deps.js +51 -0
  18. package/dist/deps.js.map +1 -0
  19. package/dist/index.d.ts +5 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/options.d.ts +25 -0
  24. package/dist/options.d.ts.map +1 -0
  25. package/dist/options.js +13 -0
  26. package/dist/options.js.map +1 -0
  27. package/dist/scaffolder.d.ts +51 -0
  28. package/dist/scaffolder.d.ts.map +1 -0
  29. package/dist/scaffolder.js +191 -0
  30. package/dist/scaffolder.js.map +1 -0
  31. package/dist/templates.d.ts +11 -0
  32. package/dist/templates.d.ts.map +1 -0
  33. package/dist/templates.js +25 -0
  34. package/dist/templates.js.map +1 -0
  35. package/package.json +65 -0
  36. package/src/cli.ts +21 -0
  37. package/src/command.ts +87 -0
  38. package/src/create.ts +7 -0
  39. package/src/deps.ts +51 -0
  40. package/src/index.ts +4 -0
  41. package/src/options.ts +24 -0
  42. package/src/scaffolder.ts +306 -0
  43. package/src/templates.ts +42 -0
  44. package/templates/claude/EXAMPLES.md.ejs +790 -0
  45. package/templates/claude/SKILL.md.ejs +126 -0
  46. package/templates/claude/WIDGETS.md.ejs +934 -0
  47. package/templates/config/vitest.config.ts.ejs +11 -0
  48. package/templates/gitignore.ejs +4 -0
  49. package/templates/gtkx.config.ts.ejs +6 -0
  50. package/templates/package.json.ejs +16 -0
  51. package/templates/src/app.tsx.ejs +48 -0
  52. package/templates/src/gtkx-env.d.ts.ejs +2 -0
  53. package/templates/src/index.tsx.ejs +4 -0
  54. package/templates/tests/app.test.tsx.ejs +16 -0
  55. 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
+ ```