@gtkx/cli 0.9.3 → 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 -844
- 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,836 +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:
|
|
192
|
-
\`\`\`tsx
|
|
193
|
-
<ListView.Root
|
|
194
|
-
vexpand
|
|
195
|
-
renderItem={(item: Item | null) => (
|
|
196
|
-
<Label label={item?.text ?? ""} />
|
|
197
|
-
)}
|
|
198
|
-
>
|
|
199
|
-
{items.map(item => (
|
|
200
|
-
<ListView.Item key={item.id} item={item} />
|
|
201
|
-
))}
|
|
202
|
-
</ListView.Root>
|
|
203
|
-
\`\`\`
|
|
204
|
-
|
|
205
|
-
**ColumnView** - Table with columns:
|
|
206
|
-
\`\`\`tsx
|
|
207
|
-
<ColumnView.Root
|
|
208
|
-
sortColumn="name"
|
|
209
|
-
sortOrder={Gtk.SortType.ASCENDING}
|
|
210
|
-
onSortChange={handleSort}
|
|
211
|
-
>
|
|
212
|
-
<ColumnView.Column
|
|
213
|
-
title="Name"
|
|
214
|
-
id="name"
|
|
215
|
-
expand
|
|
216
|
-
renderCell={(item: Item | null) => (
|
|
217
|
-
<Label label={item?.name ?? ""} />
|
|
218
|
-
)}
|
|
219
|
-
/>
|
|
220
|
-
{items.map(item => (
|
|
221
|
-
<ColumnView.Item key={item.id} item={item} />
|
|
222
|
-
))}
|
|
223
|
-
</ColumnView.Root>
|
|
224
|
-
\`\`\`
|
|
225
|
-
|
|
226
|
-
**DropDown** - Selection widget:
|
|
227
|
-
\`\`\`tsx
|
|
228
|
-
<DropDown.Root
|
|
229
|
-
itemLabel={(opt: Option) => opt.label}
|
|
230
|
-
onSelectionChanged={(item, index) => setSelected(item.value)}
|
|
231
|
-
>
|
|
232
|
-
{options.map(opt => (
|
|
233
|
-
<DropDown.Item key={opt.value} item={opt} />
|
|
234
|
-
))}
|
|
235
|
-
</DropDown.Root>
|
|
236
|
-
\`\`\`
|
|
237
|
-
|
|
238
|
-
### Controlled Input
|
|
239
|
-
|
|
240
|
-
Entry requires two-way binding:
|
|
241
|
-
\`\`\`tsx
|
|
242
|
-
const [text, setText] = useState("");
|
|
243
|
-
|
|
244
|
-
<Entry
|
|
245
|
-
text={text}
|
|
246
|
-
onChanged={(entry) => setText(entry.getText())}
|
|
247
|
-
placeholder="Type here..."
|
|
248
|
-
/>
|
|
249
|
-
\`\`\`
|
|
250
|
-
|
|
251
|
-
### Declarative Menus
|
|
252
|
-
|
|
253
|
-
\`\`\`tsx
|
|
254
|
-
<ApplicationMenu>
|
|
255
|
-
<Menu.Submenu label="File">
|
|
256
|
-
<Menu.Item
|
|
257
|
-
label="New"
|
|
258
|
-
onActivate={handleNew}
|
|
259
|
-
accels="<Control>n"
|
|
260
|
-
/>
|
|
261
|
-
<Menu.Section>
|
|
262
|
-
<Menu.Item label="Quit" onActivate={quit} accels="<Control>q" />
|
|
263
|
-
</Menu.Section>
|
|
264
|
-
</Menu.Submenu>
|
|
265
|
-
</ApplicationMenu>
|
|
266
|
-
\`\`\`
|
|
267
|
-
|
|
268
|
-
## Signal Handling
|
|
269
|
-
|
|
270
|
-
GTK signals map to \`on<SignalName>\` props:
|
|
271
|
-
- \`clicked\` → \`onClicked\`
|
|
272
|
-
- \`toggled\` → \`onToggled\`
|
|
273
|
-
- \`changed\` → \`onChanged\`
|
|
274
|
-
- \`notify::selected\` → \`onNotifySelected\`
|
|
275
|
-
|
|
276
|
-
## Widget References
|
|
277
|
-
|
|
278
|
-
\`\`\`tsx
|
|
279
|
-
import { useRef } from "react";
|
|
280
|
-
|
|
281
|
-
const entryRef = useRef<Gtk.Entry | null>(null);
|
|
282
|
-
<Entry ref={entryRef} />
|
|
283
|
-
// Later: entryRef.current?.getText()
|
|
284
|
-
\`\`\`
|
|
285
|
-
|
|
286
|
-
## Portals
|
|
287
|
-
|
|
288
|
-
\`\`\`tsx
|
|
289
|
-
import { createPortal } from "@gtkx/react";
|
|
290
|
-
|
|
291
|
-
{createPortal(<AboutDialog programName="My App" />)}
|
|
292
|
-
\`\`\`
|
|
293
|
-
|
|
294
|
-
## Constraints
|
|
295
|
-
|
|
296
|
-
- **GTK is single-threaded**: All widget operations on main thread
|
|
297
|
-
- **Virtual lists need immutable data**: Use stable object references
|
|
298
|
-
- **ToggleButton auto-prevents feedback loops**: Safe for controlled state
|
|
299
|
-
- **Entry needs two-way binding**: Use \`onChanged\` to sync state
|
|
300
|
-
|
|
301
|
-
For detailed widget reference, see [WIDGETS.md](WIDGETS.md).
|
|
302
|
-
For code examples, see [EXAMPLES.md](EXAMPLES.md).
|
|
303
|
-
`;
|
|
304
|
-
};
|
|
305
|
-
const generateWidgetsMd = () => {
|
|
306
|
-
return `# GTKX Widget Reference
|
|
307
|
-
|
|
308
|
-
## Container Widgets
|
|
309
|
-
|
|
310
|
-
### Box
|
|
311
|
-
Linear layout container.
|
|
312
|
-
|
|
313
|
-
\`\`\`tsx
|
|
314
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
315
|
-
<Label label="Child 1" />
|
|
316
|
-
<Label label="Child 2" />
|
|
317
|
-
</Box>
|
|
318
|
-
\`\`\`
|
|
319
|
-
|
|
320
|
-
Props:
|
|
321
|
-
- \`orientation\`: \`Gtk.Orientation.HORIZONTAL\` | \`Gtk.Orientation.VERTICAL\`
|
|
322
|
-
- \`spacing\`: number (pixels between children)
|
|
323
|
-
- \`homogeneous\`: boolean (equal child sizes)
|
|
324
|
-
|
|
325
|
-
### Grid
|
|
326
|
-
2D grid layout with explicit positioning.
|
|
327
|
-
|
|
328
|
-
\`\`\`tsx
|
|
329
|
-
<Grid.Root spacing={10} rowSpacing={5} columnSpacing={5}>
|
|
330
|
-
<Grid.Child column={0} row={0}>
|
|
331
|
-
<Label label="Top-left" />
|
|
332
|
-
</Grid.Child>
|
|
333
|
-
<Grid.Child column={1} row={0} columnSpan={2}>
|
|
334
|
-
<Label label="Spans 2 columns" />
|
|
335
|
-
</Grid.Child>
|
|
336
|
-
</Grid.Root>
|
|
337
|
-
\`\`\`
|
|
338
|
-
|
|
339
|
-
Grid.Child props (consumed, not passed to GTK):
|
|
340
|
-
- \`column\`: number (0-indexed)
|
|
341
|
-
- \`row\`: number (0-indexed)
|
|
342
|
-
- \`columnSpan\`: number (default 1)
|
|
343
|
-
- \`rowSpan\`: number (default 1)
|
|
344
|
-
|
|
345
|
-
### Stack
|
|
346
|
-
Shows one child at a time, switchable by name.
|
|
347
|
-
|
|
348
|
-
\`\`\`tsx
|
|
349
|
-
<Stack.Root visibleChildName="page1">
|
|
350
|
-
<Stack.Page name="page1" title="First" iconName="document-new">
|
|
351
|
-
<Content1 />
|
|
352
|
-
</Stack.Page>
|
|
353
|
-
<Stack.Page name="page2" title="Second">
|
|
354
|
-
<Content2 />
|
|
355
|
-
</Stack.Page>
|
|
356
|
-
</Stack.Root>
|
|
357
|
-
\`\`\`
|
|
358
|
-
|
|
359
|
-
Stack.Page props (consumed):
|
|
360
|
-
- \`name\`: string (required, unique identifier)
|
|
361
|
-
- \`title\`: string (display title)
|
|
362
|
-
- \`iconName\`: string (icon name)
|
|
363
|
-
|
|
364
|
-
### Notebook
|
|
365
|
-
Tabbed container with visible tabs.
|
|
366
|
-
|
|
367
|
-
\`\`\`tsx
|
|
368
|
-
<Notebook.Root>
|
|
369
|
-
<Notebook.Page label="Tab 1">
|
|
370
|
-
<Content1 />
|
|
371
|
-
</Notebook.Page>
|
|
372
|
-
<Notebook.Page label="Tab 2">
|
|
373
|
-
<Content2 />
|
|
374
|
-
</Notebook.Page>
|
|
375
|
-
</Notebook.Root>
|
|
376
|
-
\`\`\`
|
|
377
|
-
|
|
378
|
-
Notebook.Page props (consumed):
|
|
379
|
-
- \`label\`: string (tab label)
|
|
380
|
-
|
|
381
|
-
### Paned
|
|
382
|
-
Resizable split container with draggable divider.
|
|
383
|
-
|
|
384
|
-
\`\`\`tsx
|
|
385
|
-
<Paned.Root
|
|
386
|
-
orientation={Gtk.Orientation.HORIZONTAL}
|
|
387
|
-
position={280}
|
|
388
|
-
shrinkStartChild={false}
|
|
389
|
-
shrinkEndChild={false}
|
|
390
|
-
>
|
|
391
|
-
<Paned.StartChild>
|
|
392
|
-
<SidePanel />
|
|
393
|
-
</Paned.StartChild>
|
|
394
|
-
<Paned.EndChild>
|
|
395
|
-
<MainContent />
|
|
396
|
-
</Paned.EndChild>
|
|
397
|
-
</Paned.Root>
|
|
398
|
-
\`\`\`
|
|
399
|
-
|
|
400
|
-
Props:
|
|
401
|
-
- \`orientation\`: \`Gtk.Orientation.HORIZONTAL\` | \`Gtk.Orientation.VERTICAL\`
|
|
402
|
-
- \`position\`: number (divider position in pixels)
|
|
403
|
-
- \`shrinkStartChild\`: boolean
|
|
404
|
-
- \`shrinkEndChild\`: boolean
|
|
405
|
-
|
|
406
|
-
## Virtual Scrolling Widgets
|
|
407
|
-
|
|
408
|
-
### ListView
|
|
409
|
-
High-performance scrollable list with virtual rendering.
|
|
410
|
-
|
|
411
|
-
\`\`\`tsx
|
|
412
|
-
<ListView.Root
|
|
413
|
-
vexpand
|
|
414
|
-
renderItem={(item: Item | null) => (
|
|
415
|
-
<Label label={item?.text ?? ""} />
|
|
416
|
-
)}
|
|
417
|
-
>
|
|
418
|
-
{items.map(item => (
|
|
419
|
-
<ListView.Item key={item.id} item={item} />
|
|
420
|
-
))}
|
|
421
|
-
</ListView.Root>
|
|
422
|
-
\`\`\`
|
|
423
|
-
|
|
424
|
-
Props:
|
|
425
|
-
- \`renderItem\`: \`(item: T | null) => ReactElement\` (required)
|
|
426
|
-
- Standard scrollable props
|
|
427
|
-
|
|
428
|
-
### ColumnView
|
|
429
|
-
Table with sortable columns.
|
|
430
|
-
|
|
431
|
-
\`\`\`tsx
|
|
432
|
-
<ColumnView.Root
|
|
433
|
-
sortColumn="name"
|
|
434
|
-
sortOrder={Gtk.SortType.ASCENDING}
|
|
435
|
-
onSortChange={(column, order) => handleSort(column, order)}
|
|
436
|
-
>
|
|
437
|
-
<ColumnView.Column
|
|
438
|
-
title="Name"
|
|
439
|
-
id="name"
|
|
440
|
-
expand
|
|
441
|
-
resizable
|
|
442
|
-
renderCell={(item: Item | null) => (
|
|
443
|
-
<Label label={item?.name ?? ""} />
|
|
444
|
-
)}
|
|
445
|
-
/>
|
|
446
|
-
{items.map(item => (
|
|
447
|
-
<ColumnView.Item key={item.id} item={item} />
|
|
448
|
-
))}
|
|
449
|
-
</ColumnView.Root>
|
|
450
|
-
\`\`\`
|
|
451
|
-
|
|
452
|
-
### DropDown
|
|
453
|
-
Selection dropdown with custom rendering.
|
|
454
|
-
|
|
455
|
-
\`\`\`tsx
|
|
456
|
-
<DropDown.Root
|
|
457
|
-
itemLabel={(opt: Option) => opt.label}
|
|
458
|
-
onSelectionChanged={(item, index) => setSelected(item.value)}
|
|
459
|
-
>
|
|
460
|
-
{options.map(opt => (
|
|
461
|
-
<DropDown.Item key={opt.value} item={opt} />
|
|
462
|
-
))}
|
|
463
|
-
</DropDown.Root>
|
|
464
|
-
\`\`\`
|
|
465
|
-
|
|
466
|
-
## Input Widgets
|
|
467
|
-
|
|
468
|
-
### Entry
|
|
469
|
-
Single-line text input. Requires two-way binding for controlled behavior.
|
|
470
|
-
|
|
471
|
-
\`\`\`tsx
|
|
472
|
-
const [text, setText] = useState("");
|
|
473
|
-
|
|
474
|
-
<Entry
|
|
475
|
-
text={text}
|
|
476
|
-
onChanged={(entry) => setText(entry.getText())}
|
|
477
|
-
placeholder="Enter text..."
|
|
478
|
-
/>
|
|
479
|
-
\`\`\`
|
|
480
|
-
|
|
481
|
-
### ToggleButton
|
|
482
|
-
Toggle button with controlled state. Auto-prevents signal feedback loops.
|
|
483
|
-
|
|
484
|
-
\`\`\`tsx
|
|
485
|
-
const [active, setActive] = useState(false);
|
|
486
|
-
|
|
487
|
-
<ToggleButton.Root
|
|
488
|
-
active={active}
|
|
489
|
-
onToggled={() => setActive(!active)}
|
|
490
|
-
label="Toggle me"
|
|
491
|
-
/>
|
|
492
|
-
\`\`\`
|
|
493
|
-
|
|
494
|
-
## Display Widgets
|
|
495
|
-
|
|
496
|
-
### Label
|
|
497
|
-
\`\`\`tsx
|
|
498
|
-
<Label label="Hello World" halign={Gtk.Align.START} wrap useMarkup />
|
|
499
|
-
\`\`\`
|
|
500
|
-
|
|
501
|
-
### Button
|
|
502
|
-
\`\`\`tsx
|
|
503
|
-
<Button label="Click me" onClicked={() => handleClick()} iconName="document-new" />
|
|
504
|
-
\`\`\`
|
|
505
|
-
|
|
506
|
-
### MenuButton
|
|
507
|
-
\`\`\`tsx
|
|
508
|
-
<MenuButton.Root label="Options" iconName="open-menu">
|
|
509
|
-
<MenuButton.Popover>
|
|
510
|
-
<PopoverMenu.Root>
|
|
511
|
-
<Menu.Item label="Action" onActivate={handle} />
|
|
512
|
-
</PopoverMenu.Root>
|
|
513
|
-
</MenuButton.Popover>
|
|
514
|
-
</MenuButton.Root>
|
|
515
|
-
\`\`\`
|
|
516
|
-
|
|
517
|
-
## Menu Widgets
|
|
518
|
-
|
|
519
|
-
### ApplicationMenu
|
|
520
|
-
\`\`\`tsx
|
|
521
|
-
<ApplicationMenu>
|
|
522
|
-
<Menu.Submenu label="File">
|
|
523
|
-
<Menu.Item label="New" onActivate={handleNew} accels="<Control>n" />
|
|
524
|
-
<Menu.Section>
|
|
525
|
-
<Menu.Item label="Quit" onActivate={quit} accels="<Control>q" />
|
|
526
|
-
</Menu.Section>
|
|
527
|
-
</Menu.Submenu>
|
|
528
|
-
</ApplicationMenu>
|
|
529
|
-
\`\`\`
|
|
530
|
-
|
|
531
|
-
### Menu.Item
|
|
532
|
-
Props:
|
|
533
|
-
- \`label\`: string
|
|
534
|
-
- \`onActivate\`: \`() => void\`
|
|
535
|
-
- \`accels\`: string | string[] (e.g., "<Control>n")
|
|
536
|
-
|
|
537
|
-
### Menu.Section
|
|
538
|
-
Groups menu items with optional label.
|
|
539
|
-
|
|
540
|
-
### Menu.Submenu
|
|
541
|
-
Nested submenu.
|
|
542
|
-
|
|
543
|
-
## Window Widgets
|
|
544
|
-
|
|
545
|
-
### ApplicationWindow
|
|
546
|
-
\`\`\`tsx
|
|
547
|
-
<ApplicationWindow
|
|
548
|
-
title="My App"
|
|
549
|
-
defaultWidth={800}
|
|
550
|
-
defaultHeight={600}
|
|
551
|
-
showMenubar
|
|
552
|
-
onCloseRequest={() => false}
|
|
553
|
-
>
|
|
554
|
-
<MainContent />
|
|
555
|
-
</ApplicationWindow>
|
|
556
|
-
\`\`\`
|
|
557
|
-
|
|
558
|
-
## Common Props
|
|
559
|
-
|
|
560
|
-
All widgets support:
|
|
561
|
-
- \`hexpand\` / \`vexpand\`: boolean (expand to fill space)
|
|
562
|
-
- \`halign\` / \`valign\`: \`Gtk.Align.START\` | \`CENTER\` | \`END\` | \`FILL\`
|
|
563
|
-
- \`marginStart\` / \`marginEnd\` / \`marginTop\` / \`marginBottom\`: number
|
|
564
|
-
- \`sensitive\`: boolean (enabled/disabled)
|
|
565
|
-
- \`visible\`: boolean
|
|
566
|
-
- \`cssClasses\`: string[]
|
|
567
|
-
`;
|
|
568
|
-
};
|
|
569
|
-
const generateExamplesMd = () => {
|
|
570
|
-
return `# GTKX Code Examples
|
|
571
|
-
|
|
572
|
-
## Application Structure
|
|
573
|
-
|
|
574
|
-
### Basic App with State
|
|
575
|
-
|
|
576
|
-
\`\`\`tsx
|
|
577
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
578
|
-
import { ApplicationWindow, Box, Label, quit } from "@gtkx/react";
|
|
579
|
-
import { useCallback, useState } from "react";
|
|
580
|
-
|
|
581
|
-
interface Todo {
|
|
582
|
-
id: number;
|
|
583
|
-
text: string;
|
|
584
|
-
completed: boolean;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
let nextId = 1;
|
|
588
|
-
|
|
589
|
-
export const App = () => {
|
|
590
|
-
const [todos, setTodos] = useState<Todo[]>([]);
|
|
591
|
-
|
|
592
|
-
const addTodo = useCallback((text: string) => {
|
|
593
|
-
setTodos((prev) => [...prev, { id: nextId++, text, completed: false }]);
|
|
594
|
-
}, []);
|
|
595
|
-
|
|
596
|
-
const toggleTodo = useCallback((id: number) => {
|
|
597
|
-
setTodos((prev) =>
|
|
598
|
-
prev.map((todo) =>
|
|
599
|
-
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
|
600
|
-
)
|
|
601
|
-
);
|
|
602
|
-
}, []);
|
|
603
|
-
|
|
604
|
-
return (
|
|
605
|
-
<ApplicationWindow title="Todo App" defaultWidth={400} defaultHeight={500} onCloseRequest={quit}>
|
|
606
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={16} marginTop={16} marginStart={16} marginEnd={16}>
|
|
607
|
-
<Label label="Todo App" />
|
|
608
|
-
</Box>
|
|
609
|
-
</ApplicationWindow>
|
|
610
|
-
);
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
export const appId = "com.gtkx.todo";
|
|
614
|
-
\`\`\`
|
|
615
|
-
|
|
616
|
-
## Layout Patterns
|
|
617
|
-
|
|
618
|
-
### Grid for Forms
|
|
619
|
-
|
|
620
|
-
\`\`\`tsx
|
|
621
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
622
|
-
import { Button, Entry, Grid, Label } from "@gtkx/react";
|
|
623
|
-
import { useState } from "react";
|
|
624
|
-
|
|
625
|
-
const FormLayout = () => {
|
|
626
|
-
const [name, setName] = useState("");
|
|
627
|
-
const [email, setEmail] = useState("");
|
|
628
|
-
|
|
629
|
-
return (
|
|
630
|
-
<Grid.Root rowSpacing={8} columnSpacing={12}>
|
|
631
|
-
<Grid.Child column={0} row={0}>
|
|
632
|
-
<Label label="Name:" halign={Gtk.Align.END} />
|
|
633
|
-
</Grid.Child>
|
|
634
|
-
<Grid.Child column={1} row={0}>
|
|
635
|
-
<Entry text={name} onChanged={(e) => setName(e.getText())} hexpand />
|
|
636
|
-
</Grid.Child>
|
|
637
|
-
<Grid.Child column={0} row={1}>
|
|
638
|
-
<Label label="Email:" halign={Gtk.Align.END} />
|
|
639
|
-
</Grid.Child>
|
|
640
|
-
<Grid.Child column={1} row={1}>
|
|
641
|
-
<Entry text={email} onChanged={(e) => setEmail(e.getText())} hexpand />
|
|
642
|
-
</Grid.Child>
|
|
643
|
-
<Grid.Child column={0} row={2} columnSpan={2}>
|
|
644
|
-
<Button label="Submit" halign={Gtk.Align.END} marginTop={8} />
|
|
645
|
-
</Grid.Child>
|
|
646
|
-
</Grid.Root>
|
|
647
|
-
);
|
|
648
|
-
};
|
|
649
|
-
\`\`\`
|
|
650
|
-
|
|
651
|
-
### Stack with StackSwitcher
|
|
652
|
-
|
|
653
|
-
\`\`\`tsx
|
|
654
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
655
|
-
import { Box, Label, Stack, StackSwitcher } from "@gtkx/react";
|
|
656
|
-
|
|
657
|
-
const TabContainer = () => (
|
|
658
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={8}>
|
|
659
|
-
<StackSwitcher.Root
|
|
660
|
-
ref={(switcher: Gtk.StackSwitcher | null) => {
|
|
661
|
-
if (switcher) {
|
|
662
|
-
const stack = switcher.getParent()?.getLastChild() as Gtk.Stack | null;
|
|
663
|
-
if (stack) switcher.setStack(stack);
|
|
664
|
-
}
|
|
665
|
-
}}
|
|
666
|
-
/>
|
|
667
|
-
<Stack.Root transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT} transitionDuration={200}>
|
|
668
|
-
<Stack.Page name="page1" title="First">
|
|
669
|
-
<Label label="First Page Content" />
|
|
670
|
-
</Stack.Page>
|
|
671
|
-
<Stack.Page name="page2" title="Second">
|
|
672
|
-
<Label label="Second Page Content" />
|
|
673
|
-
</Stack.Page>
|
|
674
|
-
</Stack.Root>
|
|
675
|
-
</Box>
|
|
676
|
-
);
|
|
677
|
-
\`\`\`
|
|
678
|
-
|
|
679
|
-
## Virtual Scrolling Lists
|
|
680
|
-
|
|
681
|
-
### ListView with Custom Rendering
|
|
682
|
-
|
|
683
|
-
\`\`\`tsx
|
|
684
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
685
|
-
import { Box, Label, ListView } from "@gtkx/react";
|
|
686
|
-
|
|
687
|
-
interface Task {
|
|
688
|
-
id: string;
|
|
689
|
-
title: string;
|
|
690
|
-
completed: boolean;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const tasks: Task[] = [
|
|
694
|
-
{ id: "1", title: "Learn GTK4", completed: true },
|
|
695
|
-
{ id: "2", title: "Build React app", completed: false },
|
|
696
|
-
];
|
|
697
|
-
|
|
698
|
-
const TaskList = () => (
|
|
699
|
-
<Box cssClasses={["card"]} heightRequest={250}>
|
|
700
|
-
<ListView.Root
|
|
701
|
-
vexpand
|
|
702
|
-
renderItem={(task: Task | null) => (
|
|
703
|
-
<Label
|
|
704
|
-
label={task?.title ?? ""}
|
|
705
|
-
cssClasses={task?.completed ? ["dim-label"] : []}
|
|
706
|
-
halign={Gtk.Align.START}
|
|
707
|
-
marginStart={12}
|
|
708
|
-
marginTop={8}
|
|
709
|
-
marginBottom={8}
|
|
710
|
-
/>
|
|
711
|
-
)}
|
|
712
|
-
>
|
|
713
|
-
{tasks.map((task) => (
|
|
714
|
-
<ListView.Item key={task.id} item={task} />
|
|
715
|
-
))}
|
|
716
|
-
</ListView.Root>
|
|
717
|
-
</Box>
|
|
718
|
-
);
|
|
719
|
-
\`\`\`
|
|
720
|
-
|
|
721
|
-
## Menus
|
|
722
|
-
|
|
723
|
-
### MenuButton with PopoverMenu
|
|
724
|
-
|
|
725
|
-
\`\`\`tsx
|
|
726
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
727
|
-
import { Box, Label, Menu, MenuButton, PopoverMenu } from "@gtkx/react";
|
|
728
|
-
import { useState } from "react";
|
|
729
|
-
|
|
730
|
-
const MenuDemo = () => {
|
|
731
|
-
const [lastAction, setLastAction] = useState<string | null>(null);
|
|
732
|
-
|
|
733
|
-
return (
|
|
734
|
-
<Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
|
735
|
-
<Label label={\`Last action: \${lastAction ?? "(none)"}\`} />
|
|
736
|
-
<MenuButton.Root label="Actions">
|
|
737
|
-
<MenuButton.Popover>
|
|
738
|
-
<PopoverMenu.Root>
|
|
739
|
-
<Menu.Item label="New" onActivate={() => setLastAction("New")} accels="<Control>n" />
|
|
740
|
-
<Menu.Item label="Open" onActivate={() => setLastAction("Open")} accels="<Control>o" />
|
|
741
|
-
<Menu.Item label="Save" onActivate={() => setLastAction("Save")} accels="<Control>s" />
|
|
742
|
-
</PopoverMenu.Root>
|
|
743
|
-
</MenuButton.Popover>
|
|
744
|
-
</MenuButton.Root>
|
|
745
|
-
</Box>
|
|
746
|
-
);
|
|
747
|
-
};
|
|
748
|
-
\`\`\`
|
|
749
|
-
|
|
750
|
-
## Component Props Pattern
|
|
751
|
-
|
|
752
|
-
### List Item Component
|
|
753
|
-
|
|
754
|
-
\`\`\`tsx
|
|
755
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
756
|
-
import { Box, Button, CheckButton, Label } from "@gtkx/react";
|
|
757
|
-
|
|
758
|
-
interface Todo {
|
|
759
|
-
id: number;
|
|
760
|
-
text: string;
|
|
761
|
-
completed: boolean;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
interface TodoItemProps {
|
|
765
|
-
todo: Todo;
|
|
766
|
-
onToggle: (id: number) => void;
|
|
767
|
-
onDelete: (id: number) => void;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
export const TodoItem = ({ todo, onToggle, onDelete }: TodoItemProps) => (
|
|
771
|
-
<Box orientation={Gtk.Orientation.HORIZONTAL} spacing={8}>
|
|
772
|
-
<CheckButton active={todo.completed} onToggled={() => onToggle(todo.id)} />
|
|
773
|
-
<Label label={todo.text} hexpand cssClasses={todo.completed ? ["dim-label"] : []} />
|
|
774
|
-
<Button iconName="edit-delete-symbolic" onClicked={() => onDelete(todo.id)} cssClasses={["flat"]} />
|
|
775
|
-
</Box>
|
|
776
|
-
);
|
|
777
|
-
\`\`\`
|
|
778
|
-
`;
|
|
779
|
-
};
|
|
780
|
-
const generateExampleTest = (testing) => {
|
|
781
|
-
const imports = testing === "vitest"
|
|
782
|
-
? `import { describe, it, expect, afterEach } from "vitest";`
|
|
783
|
-
: testing === "jest"
|
|
784
|
-
? `import { describe, it, expect, afterEach } from "@jest/globals";`
|
|
785
|
-
: `import { describe, it, after } from "node:test";
|
|
786
|
-
import { strict as assert } from "node:assert";`;
|
|
787
|
-
const afterEachFn = testing === "node" ? "after" : "afterEach";
|
|
788
|
-
const assertion = testing === "node" ? `assert.ok(button, "Button should be rendered");` : `expect(button).toBeDefined();`;
|
|
789
|
-
return `${imports}
|
|
790
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
791
|
-
import { cleanup, render, screen } from "@gtkx/testing";
|
|
792
|
-
import App from "../src/app.js";
|
|
793
|
-
|
|
794
|
-
${afterEachFn}(async () => {
|
|
795
|
-
await cleanup();
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
describe("App", () => {
|
|
799
|
-
it("renders the increment button", async () => {
|
|
800
|
-
await render(<App />, { wrapper: false });
|
|
801
|
-
const button = await screen.findByRole(Gtk.AccessibleRole.BUTTON, { name: "Increment" });
|
|
802
|
-
${assertion}
|
|
803
|
-
});
|
|
804
|
-
});
|
|
805
|
-
`;
|
|
806
|
-
};
|
|
807
|
-
const generateVitestConfig = () => {
|
|
808
|
-
return `import { defineConfig } from "vitest/config";
|
|
809
|
-
|
|
810
|
-
export default defineConfig({
|
|
811
|
-
test: {
|
|
812
|
-
include: ["tests/**/*.test.{ts,tsx}"],
|
|
813
|
-
globals: false,
|
|
814
|
-
},
|
|
815
|
-
esbuild: {
|
|
816
|
-
jsx: "automatic",
|
|
817
|
-
},
|
|
818
|
-
});
|
|
819
|
-
`;
|
|
820
|
-
};
|
|
821
|
-
const generateJestConfig = () => {
|
|
822
|
-
return `/** @type {import('jest').Config} */
|
|
823
|
-
export default {
|
|
824
|
-
preset: "ts-jest/presets/default-esm",
|
|
825
|
-
testEnvironment: "node",
|
|
826
|
-
testMatch: ["**/tests/**/*.test.ts"],
|
|
827
|
-
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
|
828
|
-
moduleNameMapper: {
|
|
829
|
-
"^(\\\\.{1,2}/.*)\\\\.js$": "$1",
|
|
830
|
-
},
|
|
831
|
-
transform: {
|
|
832
|
-
"^.+\\\\.tsx?$": [
|
|
833
|
-
"ts-jest",
|
|
834
|
-
{
|
|
835
|
-
useESM: true,
|
|
836
|
-
tsconfig: "tsconfig.json",
|
|
837
|
-
},
|
|
838
|
-
],
|
|
839
|
-
},
|
|
840
|
-
};
|
|
841
|
-
`;
|
|
18
|
+
return { name, appId, title, testing };
|
|
842
19
|
};
|
|
843
20
|
export const getAddCommand = (pm, deps, dev) => {
|
|
844
21
|
const devFlag = dev ? (pm === "npm" ? "--save-dev" : "-D") : "";
|
|
@@ -904,7 +81,6 @@ const promptForOptions = async (options) => {
|
|
|
904
81
|
if (existsSync(resolve(process.cwd(), value))) {
|
|
905
82
|
return `Directory "${value}" already exists`;
|
|
906
83
|
}
|
|
907
|
-
return undefined;
|
|
908
84
|
},
|
|
909
85
|
}));
|
|
910
86
|
const defaultAppId = suggestAppId(name);
|
|
@@ -919,7 +95,6 @@ const promptForOptions = async (options) => {
|
|
|
919
95
|
if (!isValidAppId(value)) {
|
|
920
96
|
return "App ID must be reverse domain notation (e.g., com.example.myapp)";
|
|
921
97
|
}
|
|
922
|
-
return undefined;
|
|
923
98
|
},
|
|
924
99
|
}));
|
|
925
100
|
const packageManager = options.packageManager ??
|
|
@@ -953,33 +128,35 @@ const promptForOptions = async (options) => {
|
|
|
953
128
|
};
|
|
954
129
|
const scaffoldProject = (projectPath, resolved) => {
|
|
955
130
|
const { name, appId, testing, claudeSkills } = resolved;
|
|
131
|
+
const context = createTemplateContext(name, appId, testing);
|
|
956
132
|
mkdirSync(projectPath, { recursive: true });
|
|
957
133
|
mkdirSync(join(projectPath, "src"), { recursive: true });
|
|
958
134
|
if (testing !== "none") {
|
|
959
135
|
mkdirSync(join(projectPath, "tests"), { recursive: true });
|
|
960
136
|
}
|
|
961
|
-
writeFileSync(join(projectPath, "package.json"),
|
|
962
|
-
writeFileSync(join(projectPath, "tsconfig.json"),
|
|
963
|
-
writeFileSync(join(projectPath, "src", "app.tsx"),
|
|
964
|
-
writeFileSync(join(projectPath, "src", "
|
|
965
|
-
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));
|
|
966
143
|
if (claudeSkills) {
|
|
967
144
|
const skillsDir = join(projectPath, ".claude", "skills", "developing-gtkx-apps");
|
|
968
145
|
mkdirSync(skillsDir, { recursive: true });
|
|
969
|
-
writeFileSync(join(skillsDir, "SKILL.md"),
|
|
970
|
-
writeFileSync(join(skillsDir, "WIDGETS.md"),
|
|
971
|
-
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));
|
|
972
149
|
}
|
|
973
150
|
if (testing === "vitest") {
|
|
974
|
-
writeFileSync(join(projectPath, "vitest.config.ts"),
|
|
975
|
-
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));
|
|
976
153
|
}
|
|
977
154
|
else if (testing === "jest") {
|
|
978
|
-
writeFileSync(join(projectPath, "jest.config.js"),
|
|
979
|
-
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));
|
|
980
157
|
}
|
|
981
158
|
else if (testing === "node") {
|
|
982
|
-
writeFileSync(join(projectPath, "tests", "app.test.tsx"),
|
|
159
|
+
writeFileSync(join(projectPath, "tests", "app.test.tsx"), renderFile("tests/app.test.tsx.ejs", context));
|
|
983
160
|
}
|
|
984
161
|
};
|
|
985
162
|
const getDevDependencies = (testing) => {
|
|
@@ -1024,9 +201,32 @@ To run tests, you need xvfb installed:
|
|
|
1024
201
|
p.note(`${nextSteps}${testingNote}`, "Next steps");
|
|
1025
202
|
};
|
|
1026
203
|
/**
|
|
1027
|
-
* Creates a new GTKX
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
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
|
+
* ```
|
|
1030
230
|
*/
|
|
1031
231
|
export const createApp = async (options = {}) => {
|
|
1032
232
|
p.intro("Create GTKX App");
|