@gtkx/cli 0.9.4 → 0.10.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/bin/gtkx.js +2 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -0
- package/dist/create.d.ts +37 -6
- package/dist/create.js +44 -996
- package/dist/dev-server.d.ts +26 -4
- package/dist/dev-server.js +64 -9
- package/dist/refresh-runtime.d.ts +9 -0
- package/dist/refresh-runtime.js +44 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +18 -0
- package/dist/vite-plugin-gtkx-refresh.d.ts +7 -0
- package/dist/vite-plugin-gtkx-refresh.js +36 -0
- package/dist/vite-plugin-swc-ssr-refresh.d.ts +7 -0
- package/dist/vite-plugin-swc-ssr-refresh.js +45 -0
- package/package.json +14 -5
- package/templates/claude/EXAMPLES.md.ejs +364 -0
- package/templates/claude/SKILL.md.ejs +372 -0
- package/templates/claude/WIDGETS.md.ejs +531 -0
- package/templates/config/jest.config.js.ejs +19 -0
- package/templates/config/vitest.config.ts.ejs +13 -0
- package/templates/gitignore.ejs +4 -0
- package/templates/package.json.ejs +15 -0
- package/templates/src/app.tsx.ejs +17 -0
- package/templates/src/dev.tsx.ejs +5 -0
- package/templates/src/index.tsx.ejs +4 -0
- package/templates/tests/app.test.tsx.ejs +27 -0
- package/templates/tsconfig.json.ejs +14 -0
package/dist/create.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
|
+
import { renderFile } from "./templates.js";
|
|
5
6
|
const DEPENDENCIES = ["@gtkx/css", "@gtkx/ffi", "@gtkx/react", "react"];
|
|
6
7
|
const DEV_DEPENDENCIES = ["@gtkx/cli", "@types/react", "typescript"];
|
|
7
8
|
const TESTING_DEV_DEPENDENCIES = {
|
|
@@ -9,988 +10,12 @@ const TESTING_DEV_DEPENDENCIES = {
|
|
|
9
10
|
jest: ["@gtkx/testing", "jest", "@types/jest", "ts-jest"],
|
|
10
11
|
node: ["@gtkx/testing", "@types/node"],
|
|
11
12
|
};
|
|
12
|
-
|
|
13
|
-
const env = "GDK_BACKEND=x11 GSK_RENDERER=cairo LIBGL_ALWAYS_SOFTWARE=1";
|
|
14
|
-
switch (testing) {
|
|
15
|
-
case "vitest":
|
|
16
|
-
return `${env} xvfb-run -a vitest`;
|
|
17
|
-
case "jest":
|
|
18
|
-
return `${env} xvfb-run -a jest`;
|
|
19
|
-
case "node":
|
|
20
|
-
return `${env} xvfb-run -a node --import tsx --test tests/**/*.test.ts`;
|
|
21
|
-
case "none":
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
export const generatePackageJson = (name, appId, testing) => {
|
|
26
|
-
const testScript = getTestScript(testing);
|
|
27
|
-
const scripts = {
|
|
28
|
-
dev: "gtkx dev src/app.tsx",
|
|
29
|
-
build: "tsc -b",
|
|
30
|
-
start: "node dist/index.js",
|
|
31
|
-
};
|
|
32
|
-
if (testScript) {
|
|
33
|
-
scripts.test = testScript;
|
|
34
|
-
}
|
|
35
|
-
return JSON.stringify({
|
|
36
|
-
name,
|
|
37
|
-
version: "0.0.1",
|
|
38
|
-
private: true,
|
|
39
|
-
type: "module",
|
|
40
|
-
scripts,
|
|
41
|
-
gtkx: {
|
|
42
|
-
appId,
|
|
43
|
-
},
|
|
44
|
-
}, null, 4);
|
|
45
|
-
};
|
|
46
|
-
export const generateTsConfig = () => {
|
|
47
|
-
return JSON.stringify({
|
|
48
|
-
compilerOptions: {
|
|
49
|
-
target: "ESNext",
|
|
50
|
-
module: "NodeNext",
|
|
51
|
-
moduleResolution: "NodeNext",
|
|
52
|
-
jsx: "react-jsx",
|
|
53
|
-
strict: true,
|
|
54
|
-
skipLibCheck: true,
|
|
55
|
-
outDir: "dist",
|
|
56
|
-
rootDir: "src",
|
|
57
|
-
},
|
|
58
|
-
include: ["src/**/*"],
|
|
59
|
-
}, null, 4);
|
|
60
|
-
};
|
|
61
|
-
const generateAppTsx = (name, appId) => {
|
|
13
|
+
const createTemplateContext = (name, appId, testing) => {
|
|
62
14
|
const title = name
|
|
63
15
|
.split("-")
|
|
64
16
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
65
17
|
.join(" ");
|
|
66
|
-
return
|
|
67
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
68
|
-
import { ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
|
|
69
|
-
|
|
70
|
-
export default function App() {
|
|
71
|
-
const [count, setCount] = useState(0);
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<ApplicationWindow title="${title}" defaultWidth={400} defaultHeight={300} onCloseRequest={quit}>
|
|
75
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={20} marginTop={40} marginStart={40} marginEnd={40}>
|
|
76
|
-
<Label label="Welcome to GTKX!" />
|
|
77
|
-
<Label label={\`Count: \${count}\`} />
|
|
78
|
-
<Button label="Increment" onClicked={() => setCount((c) => c + 1)} />
|
|
79
|
-
</Box>
|
|
80
|
-
</ApplicationWindow>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export const appId = "${appId}";
|
|
85
|
-
`;
|
|
86
|
-
};
|
|
87
|
-
const generateIndexTsx = () => {
|
|
88
|
-
return `import { render } from "@gtkx/react";
|
|
89
|
-
import App, { appId } from "./app.js";
|
|
90
|
-
|
|
91
|
-
render(<App />, appId);
|
|
92
|
-
`;
|
|
93
|
-
};
|
|
94
|
-
const generateGitignore = () => {
|
|
95
|
-
return `node_modules/
|
|
96
|
-
dist/
|
|
97
|
-
*.log
|
|
98
|
-
.DS_Store
|
|
99
|
-
`;
|
|
100
|
-
};
|
|
101
|
-
const generateSkillMd = () => {
|
|
102
|
-
return `---
|
|
103
|
-
name: developing-gtkx-apps
|
|
104
|
-
description: Build GTK4 desktop applications with GTKX React framework. Use when creating GTKX components, working with GTK widgets, handling signals, or building Linux desktop UIs with React.
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
# Developing GTKX Applications
|
|
108
|
-
|
|
109
|
-
GTKX is a React framework for building native GTK4 desktop applications on Linux. It uses a custom React reconciler to render React components as native GTK widgets.
|
|
110
|
-
|
|
111
|
-
## Quick Start
|
|
112
|
-
|
|
113
|
-
\`\`\`tsx
|
|
114
|
-
import { ApplicationWindow, render, quit } from "@gtkx/react";
|
|
115
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
116
|
-
|
|
117
|
-
const App = () => (
|
|
118
|
-
<ApplicationWindow title="My App" defaultWidth={800} defaultHeight={600}>
|
|
119
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
120
|
-
<Label label="Hello, GTKX!" />
|
|
121
|
-
<Button label="Quit" onClicked={quit} />
|
|
122
|
-
</Box>
|
|
123
|
-
</ApplicationWindow>
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
render(<App />, "com.example.myapp");
|
|
127
|
-
\`\`\`
|
|
128
|
-
|
|
129
|
-
## Widget Patterns
|
|
130
|
-
|
|
131
|
-
### Container Widgets
|
|
132
|
-
|
|
133
|
-
**Box** - Linear layout:
|
|
134
|
-
\`\`\`tsx
|
|
135
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
136
|
-
<Label label="First" />
|
|
137
|
-
<Label label="Second" />
|
|
138
|
-
</Box>
|
|
139
|
-
\`\`\`
|
|
140
|
-
|
|
141
|
-
**Grid** - 2D positioning:
|
|
142
|
-
\`\`\`tsx
|
|
143
|
-
<Grid.Root spacing={10}>
|
|
144
|
-
<Grid.Child column={0} row={0}>
|
|
145
|
-
<Label label="Top-left" />
|
|
146
|
-
</Grid.Child>
|
|
147
|
-
<Grid.Child column={1} row={0} columnSpan={2}>
|
|
148
|
-
<Label label="Spans 2 columns" />
|
|
149
|
-
</Grid.Child>
|
|
150
|
-
</Grid.Root>
|
|
151
|
-
\`\`\`
|
|
152
|
-
|
|
153
|
-
**Stack** - Page-based container:
|
|
154
|
-
\`\`\`tsx
|
|
155
|
-
<Stack.Root visibleChildName="page1">
|
|
156
|
-
<Stack.Page name="page1" title="Page 1">
|
|
157
|
-
<Label label="Content 1" />
|
|
158
|
-
</Stack.Page>
|
|
159
|
-
<Stack.Page name="page2" title="Page 2">
|
|
160
|
-
<Label label="Content 2" />
|
|
161
|
-
</Stack.Page>
|
|
162
|
-
</Stack.Root>
|
|
163
|
-
\`\`\`
|
|
164
|
-
|
|
165
|
-
**Notebook** - Tabbed container:
|
|
166
|
-
\`\`\`tsx
|
|
167
|
-
<Notebook.Root>
|
|
168
|
-
<Notebook.Page label="Tab 1">
|
|
169
|
-
<Content1 />
|
|
170
|
-
</Notebook.Page>
|
|
171
|
-
<Notebook.Page label="Tab 2">
|
|
172
|
-
<Content2 />
|
|
173
|
-
</Notebook.Page>
|
|
174
|
-
</Notebook.Root>
|
|
175
|
-
\`\`\`
|
|
176
|
-
|
|
177
|
-
**Paned** - Resizable split:
|
|
178
|
-
\`\`\`tsx
|
|
179
|
-
<Paned.Root orientation={Gtk.Orientation.HORIZONTAL} position={280}>
|
|
180
|
-
<Paned.StartChild>
|
|
181
|
-
<SideBar />
|
|
182
|
-
</Paned.StartChild>
|
|
183
|
-
<Paned.EndChild>
|
|
184
|
-
<MainContent />
|
|
185
|
-
</Paned.EndChild>
|
|
186
|
-
</Paned.Root>
|
|
187
|
-
\`\`\`
|
|
188
|
-
|
|
189
|
-
### Virtual Scrolling Lists
|
|
190
|
-
|
|
191
|
-
**ListView** - High-performance scrollable list with selection:
|
|
192
|
-
\`\`\`tsx
|
|
193
|
-
<ListView.Root
|
|
194
|
-
vexpand
|
|
195
|
-
selected={[selectedId]}
|
|
196
|
-
selectionMode={Gtk.SelectionMode.SINGLE}
|
|
197
|
-
onSelectionChanged={(ids) => setSelectedId(ids[0])}
|
|
198
|
-
renderItem={(item: Item | null) => (
|
|
199
|
-
<Label label={item?.text ?? ""} />
|
|
200
|
-
)}
|
|
201
|
-
>
|
|
202
|
-
{items.map(item => (
|
|
203
|
-
<ListView.Item key={item.id} id={item.id} item={item} />
|
|
204
|
-
))}
|
|
205
|
-
</ListView.Root>
|
|
206
|
-
\`\`\`
|
|
207
|
-
|
|
208
|
-
**GridView** - Grid-based virtual scrolling:
|
|
209
|
-
\`\`\`tsx
|
|
210
|
-
<GridView.Root
|
|
211
|
-
vexpand
|
|
212
|
-
renderItem={(item: Item | null) => (
|
|
213
|
-
<Box orientation={Gtk.Orientation.VERTICAL}>
|
|
214
|
-
<Image iconName={item?.icon ?? "image-missing"} />
|
|
215
|
-
<Label label={item?.name ?? ""} />
|
|
216
|
-
</Box>
|
|
217
|
-
)}
|
|
218
|
-
>
|
|
219
|
-
{items.map(item => (
|
|
220
|
-
<GridView.Item key={item.id} id={item.id} item={item} />
|
|
221
|
-
))}
|
|
222
|
-
</GridView.Root>
|
|
223
|
-
\`\`\`
|
|
224
|
-
|
|
225
|
-
**ColumnView** - Table with sortable columns:
|
|
226
|
-
\`\`\`tsx
|
|
227
|
-
<ColumnView.Root
|
|
228
|
-
sortColumn="name"
|
|
229
|
-
sortOrder={Gtk.SortType.ASCENDING}
|
|
230
|
-
onSortChange={handleSort}
|
|
231
|
-
>
|
|
232
|
-
<ColumnView.Column
|
|
233
|
-
title="Name"
|
|
234
|
-
id="name"
|
|
235
|
-
expand
|
|
236
|
-
sortable
|
|
237
|
-
renderCell={(item: Item | null) => (
|
|
238
|
-
<Label label={item?.name ?? ""} />
|
|
239
|
-
)}
|
|
240
|
-
/>
|
|
241
|
-
{items.map(item => (
|
|
242
|
-
<ColumnView.Item key={item.id} id={item.id} item={item} />
|
|
243
|
-
))}
|
|
244
|
-
</ColumnView.Root>
|
|
245
|
-
\`\`\`
|
|
246
|
-
|
|
247
|
-
**DropDown** - String selection widget:
|
|
248
|
-
\`\`\`tsx
|
|
249
|
-
<DropDown.Root>
|
|
250
|
-
{options.map(opt => (
|
|
251
|
-
<DropDown.Item key={opt.value} id={opt.value} label={opt.label} />
|
|
252
|
-
))}
|
|
253
|
-
</DropDown.Root>
|
|
254
|
-
\`\`\`
|
|
255
|
-
|
|
256
|
-
### HeaderBar
|
|
257
|
-
|
|
258
|
-
Pack widgets at start and end of the title bar:
|
|
259
|
-
\`\`\`tsx
|
|
260
|
-
<HeaderBar.Root>
|
|
261
|
-
<HeaderBar.Start>
|
|
262
|
-
<Button iconName="go-previous-symbolic" />
|
|
263
|
-
</HeaderBar.Start>
|
|
264
|
-
<HeaderBar.End>
|
|
265
|
-
<MenuButton.Root iconName="open-menu-symbolic" />
|
|
266
|
-
</HeaderBar.End>
|
|
267
|
-
</HeaderBar.Root>
|
|
268
|
-
\`\`\`
|
|
269
|
-
|
|
270
|
-
### ActionBar
|
|
271
|
-
|
|
272
|
-
Bottom bar with packed widgets:
|
|
273
|
-
\`\`\`tsx
|
|
274
|
-
<ActionBar.Root>
|
|
275
|
-
<ActionBar.Start>
|
|
276
|
-
<Button label="Cancel" />
|
|
277
|
-
</ActionBar.Start>
|
|
278
|
-
<ActionBar.End>
|
|
279
|
-
<Button label="Save" cssClasses={["suggested-action"]} />
|
|
280
|
-
</ActionBar.End>
|
|
281
|
-
</ActionBar.Root>
|
|
282
|
-
\`\`\`
|
|
283
|
-
|
|
284
|
-
### Controlled Input
|
|
285
|
-
|
|
286
|
-
Entry requires two-way binding:
|
|
287
|
-
\`\`\`tsx
|
|
288
|
-
const [text, setText] = useState("");
|
|
289
|
-
|
|
290
|
-
<Entry
|
|
291
|
-
text={text}
|
|
292
|
-
onChanged={(entry) => setText(entry.getText())}
|
|
293
|
-
placeholder="Type here..."
|
|
294
|
-
/>
|
|
295
|
-
\`\`\`
|
|
296
|
-
|
|
297
|
-
### Declarative Menus
|
|
298
|
-
|
|
299
|
-
\`\`\`tsx
|
|
300
|
-
<ApplicationMenu>
|
|
301
|
-
<Menu.Submenu label="File">
|
|
302
|
-
<Menu.Item
|
|
303
|
-
label="New"
|
|
304
|
-
onActivate={handleNew}
|
|
305
|
-
accels="<Control>n"
|
|
306
|
-
/>
|
|
307
|
-
<Menu.Section>
|
|
308
|
-
<Menu.Item label="Quit" onActivate={quit} accels="<Control>q" />
|
|
309
|
-
</Menu.Section>
|
|
310
|
-
</Menu.Submenu>
|
|
311
|
-
</ApplicationMenu>
|
|
312
|
-
\`\`\`
|
|
313
|
-
|
|
314
|
-
## Signal Handling
|
|
315
|
-
|
|
316
|
-
GTK signals map to \`on<SignalName>\` props:
|
|
317
|
-
- \`clicked\` → \`onClicked\`
|
|
318
|
-
- \`toggled\` → \`onToggled\`
|
|
319
|
-
- \`changed\` → \`onChanged\`
|
|
320
|
-
- \`notify::selected\` → \`onNotifySelected\`
|
|
321
|
-
|
|
322
|
-
## Widget References
|
|
323
|
-
|
|
324
|
-
\`\`\`tsx
|
|
325
|
-
import { useRef } from "react";
|
|
326
|
-
|
|
327
|
-
const entryRef = useRef<Gtk.Entry | null>(null);
|
|
328
|
-
<Entry ref={entryRef} />
|
|
329
|
-
// Later: entryRef.current?.getText()
|
|
330
|
-
\`\`\`
|
|
331
|
-
|
|
332
|
-
## Portals
|
|
333
|
-
|
|
334
|
-
\`\`\`tsx
|
|
335
|
-
import { createPortal } from "@gtkx/react";
|
|
336
|
-
|
|
337
|
-
{createPortal(<AboutDialog programName="My App" />)}
|
|
338
|
-
\`\`\`
|
|
339
|
-
|
|
340
|
-
## Constraints
|
|
341
|
-
|
|
342
|
-
- **GTK is single-threaded**: All widget operations on main thread
|
|
343
|
-
- **Virtual lists need immutable data**: Use stable object references
|
|
344
|
-
- **ToggleButton auto-prevents feedback loops**: Safe for controlled state
|
|
345
|
-
- **Entry needs two-way binding**: Use \`onChanged\` to sync state
|
|
346
|
-
|
|
347
|
-
For detailed widget reference, see [WIDGETS.md](WIDGETS.md).
|
|
348
|
-
For code examples, see [EXAMPLES.md](EXAMPLES.md).
|
|
349
|
-
`;
|
|
350
|
-
};
|
|
351
|
-
const generateWidgetsMd = () => {
|
|
352
|
-
return `# GTKX Widget Reference
|
|
353
|
-
|
|
354
|
-
## Container Widgets
|
|
355
|
-
|
|
356
|
-
### Box
|
|
357
|
-
Linear layout container.
|
|
358
|
-
|
|
359
|
-
\`\`\`tsx
|
|
360
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
361
|
-
<Label label="Child 1" />
|
|
362
|
-
<Label label="Child 2" />
|
|
363
|
-
</Box>
|
|
364
|
-
\`\`\`
|
|
365
|
-
|
|
366
|
-
Props:
|
|
367
|
-
- \`orientation\`: \`Gtk.Orientation.HORIZONTAL\` | \`Gtk.Orientation.VERTICAL\`
|
|
368
|
-
- \`spacing\`: number (pixels between children)
|
|
369
|
-
- \`homogeneous\`: boolean (equal child sizes)
|
|
370
|
-
|
|
371
|
-
### Grid
|
|
372
|
-
2D grid layout with explicit positioning.
|
|
373
|
-
|
|
374
|
-
\`\`\`tsx
|
|
375
|
-
<Grid.Root spacing={10} rowSpacing={5} columnSpacing={5}>
|
|
376
|
-
<Grid.Child column={0} row={0}>
|
|
377
|
-
<Label label="Top-left" />
|
|
378
|
-
</Grid.Child>
|
|
379
|
-
<Grid.Child column={1} row={0} columnSpan={2}>
|
|
380
|
-
<Label label="Spans 2 columns" />
|
|
381
|
-
</Grid.Child>
|
|
382
|
-
</Grid.Root>
|
|
383
|
-
\`\`\`
|
|
384
|
-
|
|
385
|
-
Grid.Child props (consumed, not passed to GTK):
|
|
386
|
-
- \`column\`: number (0-indexed)
|
|
387
|
-
- \`row\`: number (0-indexed)
|
|
388
|
-
- \`columnSpan\`: number (default 1)
|
|
389
|
-
- \`rowSpan\`: number (default 1)
|
|
390
|
-
|
|
391
|
-
### Stack
|
|
392
|
-
Shows one child at a time, switchable by name.
|
|
393
|
-
|
|
394
|
-
\`\`\`tsx
|
|
395
|
-
<Stack.Root visibleChildName="page1">
|
|
396
|
-
<Stack.Page name="page1" title="First" iconName="document-new">
|
|
397
|
-
<Content1 />
|
|
398
|
-
</Stack.Page>
|
|
399
|
-
<Stack.Page name="page2" title="Second">
|
|
400
|
-
<Content2 />
|
|
401
|
-
</Stack.Page>
|
|
402
|
-
</Stack.Root>
|
|
403
|
-
\`\`\`
|
|
404
|
-
|
|
405
|
-
Stack.Page props (consumed):
|
|
406
|
-
- \`name\`: string (required, unique identifier)
|
|
407
|
-
- \`title\`: string (display title)
|
|
408
|
-
- \`iconName\`: string (icon name)
|
|
409
|
-
|
|
410
|
-
### Notebook
|
|
411
|
-
Tabbed container with visible tabs.
|
|
412
|
-
|
|
413
|
-
\`\`\`tsx
|
|
414
|
-
<Notebook.Root>
|
|
415
|
-
<Notebook.Page label="Tab 1">
|
|
416
|
-
<Content1 />
|
|
417
|
-
</Notebook.Page>
|
|
418
|
-
<Notebook.Page label="Tab 2">
|
|
419
|
-
<Content2 />
|
|
420
|
-
</Notebook.Page>
|
|
421
|
-
</Notebook.Root>
|
|
422
|
-
\`\`\`
|
|
423
|
-
|
|
424
|
-
Notebook.Page props (consumed):
|
|
425
|
-
- \`label\`: string (tab label)
|
|
426
|
-
|
|
427
|
-
### Paned
|
|
428
|
-
Resizable split container with draggable divider.
|
|
429
|
-
|
|
430
|
-
\`\`\`tsx
|
|
431
|
-
<Paned.Root
|
|
432
|
-
orientation={Gtk.Orientation.HORIZONTAL}
|
|
433
|
-
position={280}
|
|
434
|
-
shrinkStartChild={false}
|
|
435
|
-
shrinkEndChild={false}
|
|
436
|
-
>
|
|
437
|
-
<Paned.StartChild>
|
|
438
|
-
<SidePanel />
|
|
439
|
-
</Paned.StartChild>
|
|
440
|
-
<Paned.EndChild>
|
|
441
|
-
<MainContent />
|
|
442
|
-
</Paned.EndChild>
|
|
443
|
-
</Paned.Root>
|
|
444
|
-
\`\`\`
|
|
445
|
-
|
|
446
|
-
Props:
|
|
447
|
-
- \`orientation\`: \`Gtk.Orientation.HORIZONTAL\` | \`Gtk.Orientation.VERTICAL\`
|
|
448
|
-
- \`position\`: number (divider position in pixels)
|
|
449
|
-
- \`shrinkStartChild\`: boolean
|
|
450
|
-
- \`shrinkEndChild\`: boolean
|
|
451
|
-
|
|
452
|
-
## Virtual Scrolling Widgets
|
|
453
|
-
|
|
454
|
-
### ListView
|
|
455
|
-
High-performance scrollable list with virtual rendering and selection support.
|
|
456
|
-
|
|
457
|
-
\`\`\`tsx
|
|
458
|
-
<ListView.Root
|
|
459
|
-
vexpand
|
|
460
|
-
selected={[selectedId]}
|
|
461
|
-
selectionMode={Gtk.SelectionMode.SINGLE}
|
|
462
|
-
onSelectionChanged={(ids) => setSelectedId(ids[0])}
|
|
463
|
-
renderItem={(item: Item | null) => (
|
|
464
|
-
<Label label={item?.text ?? ""} />
|
|
465
|
-
)}
|
|
466
|
-
>
|
|
467
|
-
{items.map(item => (
|
|
468
|
-
<ListView.Item key={item.id} id={item.id} item={item} />
|
|
469
|
-
))}
|
|
470
|
-
</ListView.Root>
|
|
471
|
-
\`\`\`
|
|
472
|
-
|
|
473
|
-
Props:
|
|
474
|
-
- \`renderItem\`: \`(item: T | null) => ReactElement\` (required)
|
|
475
|
-
- \`selected\`: string[] (array of selected item IDs)
|
|
476
|
-
- \`selectionMode\`: \`Gtk.SelectionMode.SINGLE\` | \`MULTIPLE\` | \`NONE\`
|
|
477
|
-
- \`onSelectionChanged\`: \`(ids: string[]) => void\`
|
|
478
|
-
|
|
479
|
-
ListView.Item props:
|
|
480
|
-
- \`id\`: string (required, unique identifier for selection)
|
|
481
|
-
- \`item\`: T (the data item)
|
|
482
|
-
|
|
483
|
-
### GridView
|
|
484
|
-
Grid-based virtual scrolling. Same API as ListView but renders items in a grid.
|
|
485
|
-
|
|
486
|
-
\`\`\`tsx
|
|
487
|
-
<GridView.Root
|
|
488
|
-
vexpand
|
|
489
|
-
renderItem={(item: Item | null) => (
|
|
490
|
-
<Box orientation={Gtk.Orientation.VERTICAL}>
|
|
491
|
-
<Image iconName={item?.icon ?? "image-missing"} />
|
|
492
|
-
<Label label={item?.name ?? ""} />
|
|
493
|
-
</Box>
|
|
494
|
-
)}
|
|
495
|
-
>
|
|
496
|
-
{items.map(item => (
|
|
497
|
-
<GridView.Item key={item.id} id={item.id} item={item} />
|
|
498
|
-
))}
|
|
499
|
-
</GridView.Root>
|
|
500
|
-
\`\`\`
|
|
501
|
-
|
|
502
|
-
### ColumnView
|
|
503
|
-
Table with sortable columns.
|
|
504
|
-
|
|
505
|
-
\`\`\`tsx
|
|
506
|
-
<ColumnView.Root
|
|
507
|
-
sortColumn="name"
|
|
508
|
-
sortOrder={Gtk.SortType.ASCENDING}
|
|
509
|
-
onSortChange={(column, order) => handleSort(column, order)}
|
|
510
|
-
>
|
|
511
|
-
<ColumnView.Column
|
|
512
|
-
title="Name"
|
|
513
|
-
id="name"
|
|
514
|
-
expand
|
|
515
|
-
resizable
|
|
516
|
-
sortable
|
|
517
|
-
renderCell={(item: Item | null) => (
|
|
518
|
-
<Label label={item?.name ?? ""} />
|
|
519
|
-
)}
|
|
520
|
-
/>
|
|
521
|
-
{items.map(item => (
|
|
522
|
-
<ColumnView.Item key={item.id} id={item.id} item={item} />
|
|
523
|
-
))}
|
|
524
|
-
</ColumnView.Root>
|
|
525
|
-
\`\`\`
|
|
526
|
-
|
|
527
|
-
ColumnView.Column props:
|
|
528
|
-
- \`title\`: string (column header)
|
|
529
|
-
- \`id\`: string (used for sorting)
|
|
530
|
-
- \`expand\`: boolean (fill available space)
|
|
531
|
-
- \`resizable\`: boolean (user can resize)
|
|
532
|
-
- \`sortable\`: boolean (clicking header triggers sort)
|
|
533
|
-
- \`fixedWidth\`: number (fixed width in pixels)
|
|
534
|
-
- \`renderCell\`: \`(item: T | null) => ReactElement\`
|
|
535
|
-
|
|
536
|
-
### DropDown
|
|
537
|
-
String selection dropdown.
|
|
538
|
-
|
|
539
|
-
\`\`\`tsx
|
|
540
|
-
<DropDown.Root>
|
|
541
|
-
{options.map(opt => (
|
|
542
|
-
<DropDown.Item key={opt.value} id={opt.value} label={opt.label} />
|
|
543
|
-
))}
|
|
544
|
-
</DropDown.Root>
|
|
545
|
-
\`\`\`
|
|
546
|
-
|
|
547
|
-
DropDown.Item props:
|
|
548
|
-
- \`id\`: string (unique identifier)
|
|
549
|
-
- \`label\`: string (display text)
|
|
550
|
-
|
|
551
|
-
## Header Widgets
|
|
552
|
-
|
|
553
|
-
### HeaderBar
|
|
554
|
-
Title bar with packed widgets at start and end.
|
|
555
|
-
|
|
556
|
-
\`\`\`tsx
|
|
557
|
-
<HeaderBar.Root>
|
|
558
|
-
<HeaderBar.Start>
|
|
559
|
-
<Button iconName="go-previous-symbolic" />
|
|
560
|
-
</HeaderBar.Start>
|
|
561
|
-
<HeaderBar.End>
|
|
562
|
-
<MenuButton.Root iconName="open-menu-symbolic" />
|
|
563
|
-
</HeaderBar.End>
|
|
564
|
-
</HeaderBar.Root>
|
|
565
|
-
\`\`\`
|
|
566
|
-
|
|
567
|
-
### ActionBar
|
|
568
|
-
Bottom action bar with start/end packing.
|
|
569
|
-
|
|
570
|
-
\`\`\`tsx
|
|
571
|
-
<ActionBar.Root>
|
|
572
|
-
<ActionBar.Start>
|
|
573
|
-
<Button label="Cancel" />
|
|
574
|
-
</ActionBar.Start>
|
|
575
|
-
<ActionBar.End>
|
|
576
|
-
<Button label="Save" cssClasses={["suggested-action"]} />
|
|
577
|
-
</ActionBar.End>
|
|
578
|
-
</ActionBar.Root>
|
|
579
|
-
\`\`\`
|
|
580
|
-
|
|
581
|
-
## Input Widgets
|
|
582
|
-
|
|
583
|
-
### Entry
|
|
584
|
-
Single-line text input. Requires two-way binding for controlled behavior.
|
|
585
|
-
|
|
586
|
-
\`\`\`tsx
|
|
587
|
-
const [text, setText] = useState("");
|
|
588
|
-
|
|
589
|
-
<Entry
|
|
590
|
-
text={text}
|
|
591
|
-
onChanged={(entry) => setText(entry.getText())}
|
|
592
|
-
placeholder="Enter text..."
|
|
593
|
-
/>
|
|
594
|
-
\`\`\`
|
|
595
|
-
|
|
596
|
-
### ToggleButton
|
|
597
|
-
Toggle button with controlled state. Auto-prevents signal feedback loops.
|
|
598
|
-
|
|
599
|
-
\`\`\`tsx
|
|
600
|
-
const [active, setActive] = useState(false);
|
|
601
|
-
|
|
602
|
-
<ToggleButton.Root
|
|
603
|
-
active={active}
|
|
604
|
-
onToggled={() => setActive(!active)}
|
|
605
|
-
label="Toggle me"
|
|
606
|
-
/>
|
|
607
|
-
\`\`\`
|
|
608
|
-
|
|
609
|
-
## Display Widgets
|
|
610
|
-
|
|
611
|
-
### Label
|
|
612
|
-
\`\`\`tsx
|
|
613
|
-
<Label label="Hello World" halign={Gtk.Align.START} wrap useMarkup />
|
|
614
|
-
\`\`\`
|
|
615
|
-
|
|
616
|
-
### Button
|
|
617
|
-
\`\`\`tsx
|
|
618
|
-
<Button label="Click me" onClicked={() => handleClick()} iconName="document-new" />
|
|
619
|
-
\`\`\`
|
|
620
|
-
|
|
621
|
-
### MenuButton
|
|
622
|
-
\`\`\`tsx
|
|
623
|
-
<MenuButton.Root label="Options" iconName="open-menu">
|
|
624
|
-
<MenuButton.Popover>
|
|
625
|
-
<PopoverMenu.Root>
|
|
626
|
-
<Menu.Item label="Action" onActivate={handle} />
|
|
627
|
-
</PopoverMenu.Root>
|
|
628
|
-
</MenuButton.Popover>
|
|
629
|
-
</MenuButton.Root>
|
|
630
|
-
\`\`\`
|
|
631
|
-
|
|
632
|
-
## Menu Widgets
|
|
633
|
-
|
|
634
|
-
### ApplicationMenu
|
|
635
|
-
\`\`\`tsx
|
|
636
|
-
<ApplicationMenu>
|
|
637
|
-
<Menu.Submenu label="File">
|
|
638
|
-
<Menu.Item label="New" onActivate={handleNew} accels="<Control>n" />
|
|
639
|
-
<Menu.Section>
|
|
640
|
-
<Menu.Item label="Quit" onActivate={quit} accels="<Control>q" />
|
|
641
|
-
</Menu.Section>
|
|
642
|
-
</Menu.Submenu>
|
|
643
|
-
</ApplicationMenu>
|
|
644
|
-
\`\`\`
|
|
645
|
-
|
|
646
|
-
### Menu.Item
|
|
647
|
-
Props:
|
|
648
|
-
- \`label\`: string
|
|
649
|
-
- \`onActivate\`: \`() => void\`
|
|
650
|
-
- \`accels\`: string | string[] (e.g., "<Control>n")
|
|
651
|
-
|
|
652
|
-
### Menu.Section
|
|
653
|
-
Groups menu items with optional label.
|
|
654
|
-
|
|
655
|
-
### Menu.Submenu
|
|
656
|
-
Nested submenu.
|
|
657
|
-
|
|
658
|
-
## Window Widgets
|
|
659
|
-
|
|
660
|
-
### ApplicationWindow
|
|
661
|
-
\`\`\`tsx
|
|
662
|
-
<ApplicationWindow
|
|
663
|
-
title="My App"
|
|
664
|
-
defaultWidth={800}
|
|
665
|
-
defaultHeight={600}
|
|
666
|
-
showMenubar
|
|
667
|
-
onCloseRequest={() => false}
|
|
668
|
-
>
|
|
669
|
-
<MainContent />
|
|
670
|
-
</ApplicationWindow>
|
|
671
|
-
\`\`\`
|
|
672
|
-
|
|
673
|
-
## Common Props
|
|
674
|
-
|
|
675
|
-
All widgets support:
|
|
676
|
-
- \`hexpand\` / \`vexpand\`: boolean (expand to fill space)
|
|
677
|
-
- \`halign\` / \`valign\`: \`Gtk.Align.START\` | \`CENTER\` | \`END\` | \`FILL\`
|
|
678
|
-
- \`marginStart\` / \`marginEnd\` / \`marginTop\` / \`marginBottom\`: number
|
|
679
|
-
- \`sensitive\`: boolean (enabled/disabled)
|
|
680
|
-
- \`visible\`: boolean
|
|
681
|
-
- \`cssClasses\`: string[]
|
|
682
|
-
`;
|
|
683
|
-
};
|
|
684
|
-
const generateExamplesMd = () => {
|
|
685
|
-
return `# GTKX Code Examples
|
|
686
|
-
|
|
687
|
-
## Application Structure
|
|
688
|
-
|
|
689
|
-
### Basic App with State
|
|
690
|
-
|
|
691
|
-
\`\`\`tsx
|
|
692
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
693
|
-
import { ApplicationWindow, Box, Label, quit } from "@gtkx/react";
|
|
694
|
-
import { useCallback, useState } from "react";
|
|
695
|
-
|
|
696
|
-
interface Todo {
|
|
697
|
-
id: number;
|
|
698
|
-
text: string;
|
|
699
|
-
completed: boolean;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
let nextId = 1;
|
|
703
|
-
|
|
704
|
-
export const App = () => {
|
|
705
|
-
const [todos, setTodos] = useState<Todo[]>([]);
|
|
706
|
-
|
|
707
|
-
const addTodo = useCallback((text: string) => {
|
|
708
|
-
setTodos((prev) => [...prev, { id: nextId++, text, completed: false }]);
|
|
709
|
-
}, []);
|
|
710
|
-
|
|
711
|
-
const toggleTodo = useCallback((id: number) => {
|
|
712
|
-
setTodos((prev) =>
|
|
713
|
-
prev.map((todo) =>
|
|
714
|
-
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
|
715
|
-
)
|
|
716
|
-
);
|
|
717
|
-
}, []);
|
|
718
|
-
|
|
719
|
-
return (
|
|
720
|
-
<ApplicationWindow title="Todo App" defaultWidth={400} defaultHeight={500} onCloseRequest={quit}>
|
|
721
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={16} marginTop={16} marginStart={16} marginEnd={16}>
|
|
722
|
-
<Label label="Todo App" />
|
|
723
|
-
</Box>
|
|
724
|
-
</ApplicationWindow>
|
|
725
|
-
);
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
export const appId = "com.gtkx.todo";
|
|
729
|
-
\`\`\`
|
|
730
|
-
|
|
731
|
-
## Layout Patterns
|
|
732
|
-
|
|
733
|
-
### Grid for Forms
|
|
734
|
-
|
|
735
|
-
\`\`\`tsx
|
|
736
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
737
|
-
import { Button, Entry, Grid, Label } from "@gtkx/react";
|
|
738
|
-
import { useState } from "react";
|
|
739
|
-
|
|
740
|
-
const FormLayout = () => {
|
|
741
|
-
const [name, setName] = useState("");
|
|
742
|
-
const [email, setEmail] = useState("");
|
|
743
|
-
|
|
744
|
-
return (
|
|
745
|
-
<Grid.Root rowSpacing={8} columnSpacing={12}>
|
|
746
|
-
<Grid.Child column={0} row={0}>
|
|
747
|
-
<Label label="Name:" halign={Gtk.Align.END} />
|
|
748
|
-
</Grid.Child>
|
|
749
|
-
<Grid.Child column={1} row={0}>
|
|
750
|
-
<Entry text={name} onChanged={(e) => setName(e.getText())} hexpand />
|
|
751
|
-
</Grid.Child>
|
|
752
|
-
<Grid.Child column={0} row={1}>
|
|
753
|
-
<Label label="Email:" halign={Gtk.Align.END} />
|
|
754
|
-
</Grid.Child>
|
|
755
|
-
<Grid.Child column={1} row={1}>
|
|
756
|
-
<Entry text={email} onChanged={(e) => setEmail(e.getText())} hexpand />
|
|
757
|
-
</Grid.Child>
|
|
758
|
-
<Grid.Child column={0} row={2} columnSpan={2}>
|
|
759
|
-
<Button label="Submit" halign={Gtk.Align.END} marginTop={8} />
|
|
760
|
-
</Grid.Child>
|
|
761
|
-
</Grid.Root>
|
|
762
|
-
);
|
|
763
|
-
};
|
|
764
|
-
\`\`\`
|
|
765
|
-
|
|
766
|
-
### Stack with StackSwitcher
|
|
767
|
-
|
|
768
|
-
\`\`\`tsx
|
|
769
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
770
|
-
import { Box, Label, Stack, StackSwitcher } from "@gtkx/react";
|
|
771
|
-
|
|
772
|
-
const TabContainer = () => (
|
|
773
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={8}>
|
|
774
|
-
<StackSwitcher.Root
|
|
775
|
-
ref={(switcher: Gtk.StackSwitcher | null) => {
|
|
776
|
-
if (switcher) {
|
|
777
|
-
const stack = switcher.getParent()?.getLastChild() as Gtk.Stack | null;
|
|
778
|
-
if (stack) switcher.setStack(stack);
|
|
779
|
-
}
|
|
780
|
-
}}
|
|
781
|
-
/>
|
|
782
|
-
<Stack.Root transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT} transitionDuration={200}>
|
|
783
|
-
<Stack.Page name="page1" title="First">
|
|
784
|
-
<Label label="First Page Content" />
|
|
785
|
-
</Stack.Page>
|
|
786
|
-
<Stack.Page name="page2" title="Second">
|
|
787
|
-
<Label label="Second Page Content" />
|
|
788
|
-
</Stack.Page>
|
|
789
|
-
</Stack.Root>
|
|
790
|
-
</Box>
|
|
791
|
-
);
|
|
792
|
-
\`\`\`
|
|
793
|
-
|
|
794
|
-
## Virtual Scrolling Lists
|
|
795
|
-
|
|
796
|
-
### ListView with Selection
|
|
797
|
-
|
|
798
|
-
\`\`\`tsx
|
|
799
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
800
|
-
import { Box, Label, ListView } from "@gtkx/react";
|
|
801
|
-
import { useState } from "react";
|
|
802
|
-
|
|
803
|
-
interface Task {
|
|
804
|
-
id: string;
|
|
805
|
-
title: string;
|
|
806
|
-
completed: boolean;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const tasks: Task[] = [
|
|
810
|
-
{ id: "1", title: "Learn GTK4", completed: true },
|
|
811
|
-
{ id: "2", title: "Build React app", completed: false },
|
|
812
|
-
];
|
|
813
|
-
|
|
814
|
-
const TaskList = () => {
|
|
815
|
-
const [selectedId, setSelectedId] = useState<string | undefined>();
|
|
816
|
-
|
|
817
|
-
return (
|
|
818
|
-
<Box cssClasses={["card"]} heightRequest={250}>
|
|
819
|
-
<ListView.Root
|
|
820
|
-
vexpand
|
|
821
|
-
selected={selectedId ? [selectedId] : []}
|
|
822
|
-
onSelectionChanged={(ids) => setSelectedId(ids[0])}
|
|
823
|
-
renderItem={(task: Task | null) => (
|
|
824
|
-
<Label
|
|
825
|
-
label={task?.title ?? ""}
|
|
826
|
-
cssClasses={task?.completed ? ["dim-label"] : []}
|
|
827
|
-
halign={Gtk.Align.START}
|
|
828
|
-
marginStart={12}
|
|
829
|
-
marginTop={8}
|
|
830
|
-
marginBottom={8}
|
|
831
|
-
/>
|
|
832
|
-
)}
|
|
833
|
-
>
|
|
834
|
-
{tasks.map((task) => (
|
|
835
|
-
<ListView.Item key={task.id} id={task.id} item={task} />
|
|
836
|
-
))}
|
|
837
|
-
</ListView.Root>
|
|
838
|
-
</Box>
|
|
839
|
-
);
|
|
840
|
-
};
|
|
841
|
-
\`\`\`
|
|
842
|
-
|
|
843
|
-
### HeaderBar with Navigation
|
|
844
|
-
|
|
845
|
-
\`\`\`tsx
|
|
846
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
847
|
-
import { ApplicationWindow, Box, Button, HeaderBar, Label, quit } from "@gtkx/react";
|
|
848
|
-
import { useState } from "react";
|
|
849
|
-
|
|
850
|
-
const AppWithHeaderBar = () => {
|
|
851
|
-
const [page, setPage] = useState("home");
|
|
852
|
-
|
|
853
|
-
return (
|
|
854
|
-
<ApplicationWindow title="My App" defaultWidth={600} defaultHeight={400} onCloseRequest={quit}>
|
|
855
|
-
<Box orientation={Gtk.Orientation.VERTICAL}>
|
|
856
|
-
<HeaderBar.Root>
|
|
857
|
-
<HeaderBar.Start>
|
|
858
|
-
{page !== "home" && (
|
|
859
|
-
<Button iconName="go-previous-symbolic" onClicked={() => setPage("home")} />
|
|
860
|
-
)}
|
|
861
|
-
</HeaderBar.Start>
|
|
862
|
-
<HeaderBar.End>
|
|
863
|
-
<Button iconName="emblem-system-symbolic" onClicked={() => setPage("settings")} />
|
|
864
|
-
</HeaderBar.End>
|
|
865
|
-
</HeaderBar.Root>
|
|
866
|
-
<Label label={page === "home" ? "Home Page" : "Settings Page"} vexpand />
|
|
867
|
-
</Box>
|
|
868
|
-
</ApplicationWindow>
|
|
869
|
-
);
|
|
870
|
-
};
|
|
871
|
-
\`\`\`
|
|
872
|
-
|
|
873
|
-
## Menus
|
|
874
|
-
|
|
875
|
-
### MenuButton with PopoverMenu
|
|
876
|
-
|
|
877
|
-
\`\`\`tsx
|
|
878
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
879
|
-
import { Box, Label, Menu, MenuButton, PopoverMenu } from "@gtkx/react";
|
|
880
|
-
import { useState } from "react";
|
|
881
|
-
|
|
882
|
-
const MenuDemo = () => {
|
|
883
|
-
const [lastAction, setLastAction] = useState<string | null>(null);
|
|
884
|
-
|
|
885
|
-
return (
|
|
886
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
887
|
-
<Label label={\`Last action: \${lastAction ?? "(none)"}\`} />
|
|
888
|
-
<MenuButton.Root label="Actions">
|
|
889
|
-
<MenuButton.Popover>
|
|
890
|
-
<PopoverMenu.Root>
|
|
891
|
-
<Menu.Item label="New" onActivate={() => setLastAction("New")} accels="<Control>n" />
|
|
892
|
-
<Menu.Item label="Open" onActivate={() => setLastAction("Open")} accels="<Control>o" />
|
|
893
|
-
<Menu.Item label="Save" onActivate={() => setLastAction("Save")} accels="<Control>s" />
|
|
894
|
-
</PopoverMenu.Root>
|
|
895
|
-
</MenuButton.Popover>
|
|
896
|
-
</MenuButton.Root>
|
|
897
|
-
</Box>
|
|
898
|
-
);
|
|
899
|
-
};
|
|
900
|
-
\`\`\`
|
|
901
|
-
|
|
902
|
-
## Component Props Pattern
|
|
903
|
-
|
|
904
|
-
### List Item Component
|
|
905
|
-
|
|
906
|
-
\`\`\`tsx
|
|
907
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
908
|
-
import { Box, Button, CheckButton, Label } from "@gtkx/react";
|
|
909
|
-
|
|
910
|
-
interface Todo {
|
|
911
|
-
id: number;
|
|
912
|
-
text: string;
|
|
913
|
-
completed: boolean;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
interface TodoItemProps {
|
|
917
|
-
todo: Todo;
|
|
918
|
-
onToggle: (id: number) => void;
|
|
919
|
-
onDelete: (id: number) => void;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
export const TodoItem = ({ todo, onToggle, onDelete }: TodoItemProps) => (
|
|
923
|
-
<Box orientation={Gtk.Orientation.HORIZONTAL} spacing={8}>
|
|
924
|
-
<CheckButton active={todo.completed} onToggled={() => onToggle(todo.id)} />
|
|
925
|
-
<Label label={todo.text} hexpand cssClasses={todo.completed ? ["dim-label"] : []} />
|
|
926
|
-
<Button iconName="edit-delete-symbolic" onClicked={() => onDelete(todo.id)} cssClasses={["flat"]} />
|
|
927
|
-
</Box>
|
|
928
|
-
);
|
|
929
|
-
\`\`\`
|
|
930
|
-
`;
|
|
931
|
-
};
|
|
932
|
-
const generateExampleTest = (testing) => {
|
|
933
|
-
const imports = testing === "vitest"
|
|
934
|
-
? `import { describe, it, expect, afterEach } from "vitest";`
|
|
935
|
-
: testing === "jest"
|
|
936
|
-
? `import { describe, it, expect, afterEach } from "@jest/globals";`
|
|
937
|
-
: `import { describe, it, after } from "node:test";
|
|
938
|
-
import { strict as assert } from "node:assert";`;
|
|
939
|
-
const afterEachFn = testing === "node" ? "after" : "afterEach";
|
|
940
|
-
const assertion = testing === "node" ? `assert.ok(button, "Button should be rendered");` : `expect(button).toBeDefined();`;
|
|
941
|
-
return `${imports}
|
|
942
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
943
|
-
import { cleanup, render, screen } from "@gtkx/testing";
|
|
944
|
-
import App from "../src/app.js";
|
|
945
|
-
|
|
946
|
-
${afterEachFn}(async () => {
|
|
947
|
-
await cleanup();
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
describe("App", () => {
|
|
951
|
-
it("renders the increment button", async () => {
|
|
952
|
-
await render(<App />, { wrapper: false });
|
|
953
|
-
const button = await screen.findByRole(Gtk.AccessibleRole.BUTTON, { name: "Increment" });
|
|
954
|
-
${assertion}
|
|
955
|
-
});
|
|
956
|
-
});
|
|
957
|
-
`;
|
|
958
|
-
};
|
|
959
|
-
const generateVitestConfig = () => {
|
|
960
|
-
return `import { defineConfig } from "vitest/config";
|
|
961
|
-
|
|
962
|
-
export default defineConfig({
|
|
963
|
-
test: {
|
|
964
|
-
include: ["tests/**/*.test.{ts,tsx}"],
|
|
965
|
-
globals: false,
|
|
966
|
-
},
|
|
967
|
-
esbuild: {
|
|
968
|
-
jsx: "automatic",
|
|
969
|
-
},
|
|
970
|
-
});
|
|
971
|
-
`;
|
|
972
|
-
};
|
|
973
|
-
const generateJestConfig = () => {
|
|
974
|
-
return `/** @type {import('jest').Config} */
|
|
975
|
-
export default {
|
|
976
|
-
preset: "ts-jest/presets/default-esm",
|
|
977
|
-
testEnvironment: "node",
|
|
978
|
-
testMatch: ["**/tests/**/*.test.ts"],
|
|
979
|
-
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
|
980
|
-
moduleNameMapper: {
|
|
981
|
-
"^(\\\\.{1,2}/.*)\\\\.js$": "$1",
|
|
982
|
-
},
|
|
983
|
-
transform: {
|
|
984
|
-
"^.+\\\\.tsx?$": [
|
|
985
|
-
"ts-jest",
|
|
986
|
-
{
|
|
987
|
-
useESM: true,
|
|
988
|
-
tsconfig: "tsconfig.json",
|
|
989
|
-
},
|
|
990
|
-
],
|
|
991
|
-
},
|
|
992
|
-
};
|
|
993
|
-
`;
|
|
18
|
+
return { name, appId, title, testing };
|
|
994
19
|
};
|
|
995
20
|
export const getAddCommand = (pm, deps, dev) => {
|
|
996
21
|
const devFlag = dev ? (pm === "npm" ? "--save-dev" : "-D") : "";
|
|
@@ -1056,7 +81,6 @@ const promptForOptions = async (options) => {
|
|
|
1056
81
|
if (existsSync(resolve(process.cwd(), value))) {
|
|
1057
82
|
return `Directory "${value}" already exists`;
|
|
1058
83
|
}
|
|
1059
|
-
return undefined;
|
|
1060
84
|
},
|
|
1061
85
|
}));
|
|
1062
86
|
const defaultAppId = suggestAppId(name);
|
|
@@ -1071,7 +95,6 @@ const promptForOptions = async (options) => {
|
|
|
1071
95
|
if (!isValidAppId(value)) {
|
|
1072
96
|
return "App ID must be reverse domain notation (e.g., com.example.myapp)";
|
|
1073
97
|
}
|
|
1074
|
-
return undefined;
|
|
1075
98
|
},
|
|
1076
99
|
}));
|
|
1077
100
|
const packageManager = options.packageManager ??
|
|
@@ -1105,33 +128,35 @@ const promptForOptions = async (options) => {
|
|
|
1105
128
|
};
|
|
1106
129
|
const scaffoldProject = (projectPath, resolved) => {
|
|
1107
130
|
const { name, appId, testing, claudeSkills } = resolved;
|
|
131
|
+
const context = createTemplateContext(name, appId, testing);
|
|
1108
132
|
mkdirSync(projectPath, { recursive: true });
|
|
1109
133
|
mkdirSync(join(projectPath, "src"), { recursive: true });
|
|
1110
134
|
if (testing !== "none") {
|
|
1111
135
|
mkdirSync(join(projectPath, "tests"), { recursive: true });
|
|
1112
136
|
}
|
|
1113
|
-
writeFileSync(join(projectPath, "package.json"),
|
|
1114
|
-
writeFileSync(join(projectPath, "tsconfig.json"),
|
|
1115
|
-
writeFileSync(join(projectPath, "src", "app.tsx"),
|
|
1116
|
-
writeFileSync(join(projectPath, "src", "
|
|
1117
|
-
writeFileSync(join(projectPath, ".
|
|
137
|
+
writeFileSync(join(projectPath, "package.json"), renderFile("package.json.ejs", context));
|
|
138
|
+
writeFileSync(join(projectPath, "tsconfig.json"), renderFile("tsconfig.json.ejs", context));
|
|
139
|
+
writeFileSync(join(projectPath, "src", "app.tsx"), renderFile("src/app.tsx.ejs", context));
|
|
140
|
+
writeFileSync(join(projectPath, "src", "dev.tsx"), renderFile("src/dev.tsx.ejs", context));
|
|
141
|
+
writeFileSync(join(projectPath, "src", "index.tsx"), renderFile("src/index.tsx.ejs", context));
|
|
142
|
+
writeFileSync(join(projectPath, ".gitignore"), renderFile("gitignore.ejs", context));
|
|
1118
143
|
if (claudeSkills) {
|
|
1119
144
|
const skillsDir = join(projectPath, ".claude", "skills", "developing-gtkx-apps");
|
|
1120
145
|
mkdirSync(skillsDir, { recursive: true });
|
|
1121
|
-
writeFileSync(join(skillsDir, "SKILL.md"),
|
|
1122
|
-
writeFileSync(join(skillsDir, "WIDGETS.md"),
|
|
1123
|
-
writeFileSync(join(skillsDir, "EXAMPLES.md"),
|
|
146
|
+
writeFileSync(join(skillsDir, "SKILL.md"), renderFile("claude/SKILL.md.ejs", context));
|
|
147
|
+
writeFileSync(join(skillsDir, "WIDGETS.md"), renderFile("claude/WIDGETS.md.ejs", context));
|
|
148
|
+
writeFileSync(join(skillsDir, "EXAMPLES.md"), renderFile("claude/EXAMPLES.md.ejs", context));
|
|
1124
149
|
}
|
|
1125
150
|
if (testing === "vitest") {
|
|
1126
|
-
writeFileSync(join(projectPath, "vitest.config.ts"),
|
|
1127
|
-
writeFileSync(join(projectPath, "tests", "app.test.tsx"),
|
|
151
|
+
writeFileSync(join(projectPath, "vitest.config.ts"), renderFile("config/vitest.config.ts.ejs", context));
|
|
152
|
+
writeFileSync(join(projectPath, "tests", "app.test.tsx"), renderFile("tests/app.test.tsx.ejs", context));
|
|
1128
153
|
}
|
|
1129
154
|
else if (testing === "jest") {
|
|
1130
|
-
writeFileSync(join(projectPath, "jest.config.js"),
|
|
1131
|
-
writeFileSync(join(projectPath, "tests", "app.test.tsx"),
|
|
155
|
+
writeFileSync(join(projectPath, "jest.config.js"), renderFile("config/jest.config.js.ejs", context));
|
|
156
|
+
writeFileSync(join(projectPath, "tests", "app.test.tsx"), renderFile("tests/app.test.tsx.ejs", context));
|
|
1132
157
|
}
|
|
1133
158
|
else if (testing === "node") {
|
|
1134
|
-
writeFileSync(join(projectPath, "tests", "app.test.tsx"),
|
|
159
|
+
writeFileSync(join(projectPath, "tests", "app.test.tsx"), renderFile("tests/app.test.tsx.ejs", context));
|
|
1135
160
|
}
|
|
1136
161
|
};
|
|
1137
162
|
const getDevDependencies = (testing) => {
|
|
@@ -1176,9 +201,32 @@ To run tests, you need xvfb installed:
|
|
|
1176
201
|
p.note(`${nextSteps}${testingNote}`, "Next steps");
|
|
1177
202
|
};
|
|
1178
203
|
/**
|
|
1179
|
-
* Creates a new GTKX
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
204
|
+
* Creates a new GTKX project with interactive prompts.
|
|
205
|
+
*
|
|
206
|
+
* Scaffolds a complete project structure including:
|
|
207
|
+
* - TypeScript configuration
|
|
208
|
+
* - React component template
|
|
209
|
+
* - Development server entry point
|
|
210
|
+
* - Optional testing setup
|
|
211
|
+
* - Optional Claude Code skills
|
|
212
|
+
*
|
|
213
|
+
* @param options - Pre-filled options to skip prompts
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```tsx
|
|
217
|
+
* import { createApp } from "@gtkx/cli";
|
|
218
|
+
*
|
|
219
|
+
* // Interactive mode
|
|
220
|
+
* await createApp();
|
|
221
|
+
*
|
|
222
|
+
* // With pre-filled options
|
|
223
|
+
* await createApp({
|
|
224
|
+
* name: "my-app",
|
|
225
|
+
* appId: "com.example.myapp",
|
|
226
|
+
* packageManager: "pnpm",
|
|
227
|
+
* testing: "vitest",
|
|
228
|
+
* });
|
|
229
|
+
* ```
|
|
1182
230
|
*/
|
|
1183
231
|
export const createApp = async (options = {}) => {
|
|
1184
232
|
p.intro("Create GTKX App");
|