@gtkx/cli 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/create.d.ts CHANGED
@@ -1,10 +1,21 @@
1
+ /** Supported package managers for project scaffolding. */
1
2
  export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
3
+ /** Supported testing frameworks for project scaffolding. */
2
4
  export type TestingFramework = "vitest" | "jest" | "node" | "none";
5
+ /**
6
+ * Options for creating a new GTKX application.
7
+ */
3
8
  export interface CreateOptions {
9
+ /** Project name (lowercase letters, numbers, and hyphens only). */
4
10
  name?: string;
11
+ /** Application ID in reverse domain notation (e.g., com.example.myapp). */
5
12
  appId?: string;
13
+ /** Package manager to use for installing dependencies. */
6
14
  packageManager?: PackageManager;
15
+ /** Testing framework to set up. */
7
16
  testing?: TestingFramework;
17
+ /** Whether to include Claude Code skills for AI assistance. */
18
+ claudeSkills?: boolean;
8
19
  }
9
20
  export declare const getTestScript: (testing: TestingFramework) => string | undefined;
10
21
  export declare const generatePackageJson: (name: string, appId: string, testing: TestingFramework) => string;
@@ -13,4 +24,9 @@ export declare const getAddCommand: (pm: PackageManager, deps: string[], dev: bo
13
24
  export declare const getRunCommand: (pm: PackageManager) => string;
14
25
  export declare const isValidProjectName: (name: string) => boolean;
15
26
  export declare const isValidAppId: (appId: string) => boolean;
27
+ /**
28
+ * Creates a new GTKX application with interactive prompts.
29
+ * Scaffolds project structure, installs dependencies, and sets up configuration.
30
+ * @param options - Pre-filled options to skip interactive prompts
31
+ */
16
32
  export declare const createApp: (options?: CreateOptions) => Promise<void>;
package/dist/create.js CHANGED
@@ -97,6 +97,685 @@ dist/
97
97
  .DS_Store
98
98
  `;
99
99
  };
100
+ const generateSkillMd = () => {
101
+ return `---
102
+ name: developing-gtkx-apps
103
+ 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.
104
+ ---
105
+
106
+ # Developing GTKX Applications
107
+
108
+ 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.
109
+
110
+ ## Quick Start
111
+
112
+ \`\`\`tsx
113
+ import { ApplicationWindow, render, quit } from "@gtkx/react";
114
+ import * as Gtk from "@gtkx/ffi/gtk";
115
+
116
+ const App = () => (
117
+ <ApplicationWindow title="My App" defaultWidth={800} defaultHeight={600}>
118
+ <Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
119
+ <Label.Root label="Hello, GTKX!" />
120
+ <Button label="Quit" onClicked={quit} />
121
+ </Box>
122
+ </ApplicationWindow>
123
+ );
124
+
125
+ render(<App />, "com.example.myapp");
126
+ \`\`\`
127
+
128
+ ## Widget Patterns
129
+
130
+ ### Container Widgets
131
+
132
+ **Box** - Linear layout:
133
+ \`\`\`tsx
134
+ <Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
135
+ <Label.Root label="First" />
136
+ <Label.Root label="Second" />
137
+ </Box>
138
+ \`\`\`
139
+
140
+ **Grid** - 2D positioning:
141
+ \`\`\`tsx
142
+ <Grid.Root spacing={10}>
143
+ <Grid.Child column={0} row={0}>
144
+ <Label.Root label="Top-left" />
145
+ </Grid.Child>
146
+ <Grid.Child column={1} row={0} columnSpan={2}>
147
+ <Label.Root label="Spans 2 columns" />
148
+ </Grid.Child>
149
+ </Grid.Root>
150
+ \`\`\`
151
+
152
+ **Stack** - Page-based container:
153
+ \`\`\`tsx
154
+ <Stack.Root visibleChildName="page1">
155
+ <Stack.Page name="page1" title="Page 1">
156
+ <Label.Root label="Content 1" />
157
+ </Stack.Page>
158
+ <Stack.Page name="page2" title="Page 2">
159
+ <Label.Root label="Content 2" />
160
+ </Stack.Page>
161
+ </Stack.Root>
162
+ \`\`\`
163
+
164
+ **Notebook** - Tabbed container:
165
+ \`\`\`tsx
166
+ <Notebook.Root>
167
+ <Notebook.Page label="Tab 1">
168
+ <Content1 />
169
+ </Notebook.Page>
170
+ <Notebook.Page label="Tab 2">
171
+ <Content2 />
172
+ </Notebook.Page>
173
+ </Notebook.Root>
174
+ \`\`\`
175
+
176
+ **Paned** - Resizable split:
177
+ \`\`\`tsx
178
+ <Paned.Root orientation={Gtk.Orientation.HORIZONTAL} position={280}>
179
+ <Paned.StartChild>
180
+ <SideBar />
181
+ </Paned.StartChild>
182
+ <Paned.EndChild>
183
+ <MainContent />
184
+ </Paned.EndChild>
185
+ </Paned.Root>
186
+ \`\`\`
187
+
188
+ ### Virtual Scrolling Lists
189
+
190
+ **ListView** - High-performance scrollable list:
191
+ \`\`\`tsx
192
+ <ListView.Root
193
+ vexpand
194
+ renderItem={(item: Item | null) => (
195
+ <Label.Root label={item?.text ?? ""} />
196
+ )}
197
+ >
198
+ {items.map(item => (
199
+ <ListView.Item key={item.id} item={item} />
200
+ ))}
201
+ </ListView.Root>
202
+ \`\`\`
203
+
204
+ **ColumnView** - Table with columns:
205
+ \`\`\`tsx
206
+ <ColumnView.Root
207
+ sortColumn="name"
208
+ sortOrder={Gtk.SortType.ASCENDING}
209
+ onSortChange={handleSort}
210
+ >
211
+ <ColumnView.Column
212
+ title="Name"
213
+ id="name"
214
+ expand
215
+ renderCell={(item: Item | null) => (
216
+ <Label.Root label={item?.name ?? ""} />
217
+ )}
218
+ />
219
+ {items.map(item => (
220
+ <ColumnView.Item key={item.id} item={item} />
221
+ ))}
222
+ </ColumnView.Root>
223
+ \`\`\`
224
+
225
+ **DropDown** - Selection widget:
226
+ \`\`\`tsx
227
+ <DropDown.Root
228
+ itemLabel={(opt: Option) => opt.label}
229
+ onSelectionChanged={(item, index) => setSelected(item.value)}
230
+ >
231
+ {options.map(opt => (
232
+ <DropDown.Item key={opt.value} item={opt} />
233
+ ))}
234
+ </DropDown.Root>
235
+ \`\`\`
236
+
237
+ ### Controlled Input
238
+
239
+ Entry requires two-way binding:
240
+ \`\`\`tsx
241
+ const [text, setText] = useState("");
242
+
243
+ <Entry
244
+ text={text}
245
+ onChanged={(entry) => setText(entry.getText())}
246
+ placeholder="Type here..."
247
+ />
248
+ \`\`\`
249
+
250
+ ### Declarative Menus
251
+
252
+ \`\`\`tsx
253
+ <ApplicationMenu>
254
+ <Menu.Submenu label="File">
255
+ <Menu.Item
256
+ label="New"
257
+ onActivate={handleNew}
258
+ accels="<Control>n"
259
+ />
260
+ <Menu.Section>
261
+ <Menu.Item label="Quit" onActivate={quit} accels="<Control>q" />
262
+ </Menu.Section>
263
+ </Menu.Submenu>
264
+ </ApplicationMenu>
265
+ \`\`\`
266
+
267
+ ## Signal Handling
268
+
269
+ GTK signals map to \`on<SignalName>\` props:
270
+ - \`clicked\` → \`onClicked\`
271
+ - \`toggled\` → \`onToggled\`
272
+ - \`changed\` → \`onChanged\`
273
+ - \`notify::selected\` → \`onNotifySelected\`
274
+
275
+ ## Widget References
276
+
277
+ \`\`\`tsx
278
+ import { createRef } from "@gtkx/ffi";
279
+
280
+ const entryRef = createRef<Gtk.Entry>();
281
+ <Entry ref={entryRef} />
282
+ // Later: entryRef.current?.getText()
283
+ \`\`\`
284
+
285
+ ## Portals
286
+
287
+ \`\`\`tsx
288
+ import { createPortal } from "@gtkx/react";
289
+
290
+ {createPortal(<AboutDialog programName="My App" />)}
291
+ \`\`\`
292
+
293
+ ## Constraints
294
+
295
+ - **GTK is single-threaded**: All widget operations on main thread
296
+ - **Virtual lists need immutable data**: Use stable object references
297
+ - **ToggleButton auto-prevents feedback loops**: Safe for controlled state
298
+ - **Entry needs two-way binding**: Use \`onChanged\` to sync state
299
+
300
+ For detailed widget reference, see [WIDGETS.md](WIDGETS.md).
301
+ For code examples, see [EXAMPLES.md](EXAMPLES.md).
302
+ `;
303
+ };
304
+ const generateWidgetsMd = () => {
305
+ return `# GTKX Widget Reference
306
+
307
+ ## Container Widgets
308
+
309
+ ### Box
310
+ Linear layout container.
311
+
312
+ \`\`\`tsx
313
+ <Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
314
+ <Label.Root label="Child 1" />
315
+ <Label.Root label="Child 2" />
316
+ </Box>
317
+ \`\`\`
318
+
319
+ Props:
320
+ - \`orientation\`: \`Gtk.Orientation.HORIZONTAL\` | \`Gtk.Orientation.VERTICAL\`
321
+ - \`spacing\`: number (pixels between children)
322
+ - \`homogeneous\`: boolean (equal child sizes)
323
+
324
+ ### Grid
325
+ 2D grid layout with explicit positioning.
326
+
327
+ \`\`\`tsx
328
+ <Grid.Root spacing={10} rowSpacing={5} columnSpacing={5}>
329
+ <Grid.Child column={0} row={0}>
330
+ <Label.Root label="Top-left" />
331
+ </Grid.Child>
332
+ <Grid.Child column={1} row={0} columnSpan={2}>
333
+ <Label.Root label="Spans 2 columns" />
334
+ </Grid.Child>
335
+ </Grid.Root>
336
+ \`\`\`
337
+
338
+ Grid.Child props (consumed, not passed to GTK):
339
+ - \`column\`: number (0-indexed)
340
+ - \`row\`: number (0-indexed)
341
+ - \`columnSpan\`: number (default 1)
342
+ - \`rowSpan\`: number (default 1)
343
+
344
+ ### Stack
345
+ Shows one child at a time, switchable by name.
346
+
347
+ \`\`\`tsx
348
+ <Stack.Root visibleChildName="page1">
349
+ <Stack.Page name="page1" title="First" iconName="document-new">
350
+ <Content1 />
351
+ </Stack.Page>
352
+ <Stack.Page name="page2" title="Second">
353
+ <Content2 />
354
+ </Stack.Page>
355
+ </Stack.Root>
356
+ \`\`\`
357
+
358
+ Stack.Page props (consumed):
359
+ - \`name\`: string (required, unique identifier)
360
+ - \`title\`: string (display title)
361
+ - \`iconName\`: string (icon name)
362
+
363
+ ### Notebook
364
+ Tabbed container with visible tabs.
365
+
366
+ \`\`\`tsx
367
+ <Notebook.Root>
368
+ <Notebook.Page label="Tab 1">
369
+ <Content1 />
370
+ </Notebook.Page>
371
+ <Notebook.Page label="Tab 2">
372
+ <Content2 />
373
+ </Notebook.Page>
374
+ </Notebook.Root>
375
+ \`\`\`
376
+
377
+ Notebook.Page props (consumed):
378
+ - \`label\`: string (tab label)
379
+
380
+ ### Paned
381
+ Resizable split container with draggable divider.
382
+
383
+ \`\`\`tsx
384
+ <Paned.Root
385
+ orientation={Gtk.Orientation.HORIZONTAL}
386
+ position={280}
387
+ shrinkStartChild={false}
388
+ shrinkEndChild={false}
389
+ >
390
+ <Paned.StartChild>
391
+ <SidePanel />
392
+ </Paned.StartChild>
393
+ <Paned.EndChild>
394
+ <MainContent />
395
+ </Paned.EndChild>
396
+ </Paned.Root>
397
+ \`\`\`
398
+
399
+ Props:
400
+ - \`orientation\`: \`Gtk.Orientation.HORIZONTAL\` | \`Gtk.Orientation.VERTICAL\`
401
+ - \`position\`: number (divider position in pixels)
402
+ - \`shrinkStartChild\`: boolean
403
+ - \`shrinkEndChild\`: boolean
404
+
405
+ ## Virtual Scrolling Widgets
406
+
407
+ ### ListView
408
+ High-performance scrollable list with virtual rendering.
409
+
410
+ \`\`\`tsx
411
+ <ListView.Root
412
+ vexpand
413
+ renderItem={(item: Item | null) => (
414
+ <Label.Root label={item?.text ?? ""} />
415
+ )}
416
+ >
417
+ {items.map(item => (
418
+ <ListView.Item key={item.id} item={item} />
419
+ ))}
420
+ </ListView.Root>
421
+ \`\`\`
422
+
423
+ Props:
424
+ - \`renderItem\`: \`(item: T | null) => ReactElement\` (required)
425
+ - Standard scrollable props
426
+
427
+ ### ColumnView
428
+ Table with sortable columns.
429
+
430
+ \`\`\`tsx
431
+ <ColumnView.Root
432
+ sortColumn="name"
433
+ sortOrder={Gtk.SortType.ASCENDING}
434
+ onSortChange={(column, order) => handleSort(column, order)}
435
+ >
436
+ <ColumnView.Column
437
+ title="Name"
438
+ id="name"
439
+ expand
440
+ resizable
441
+ renderCell={(item: Item | null) => (
442
+ <Label.Root label={item?.name ?? ""} />
443
+ )}
444
+ />
445
+ {items.map(item => (
446
+ <ColumnView.Item key={item.id} item={item} />
447
+ ))}
448
+ </ColumnView.Root>
449
+ \`\`\`
450
+
451
+ ### DropDown
452
+ Selection dropdown with custom rendering.
453
+
454
+ \`\`\`tsx
455
+ <DropDown.Root
456
+ itemLabel={(opt: Option) => opt.label}
457
+ onSelectionChanged={(item, index) => setSelected(item.value)}
458
+ >
459
+ {options.map(opt => (
460
+ <DropDown.Item key={opt.value} item={opt} />
461
+ ))}
462
+ </DropDown.Root>
463
+ \`\`\`
464
+
465
+ ## Input Widgets
466
+
467
+ ### Entry
468
+ Single-line text input. Requires two-way binding for controlled behavior.
469
+
470
+ \`\`\`tsx
471
+ const [text, setText] = useState("");
472
+
473
+ <Entry
474
+ text={text}
475
+ onChanged={(entry) => setText(entry.getText())}
476
+ placeholder="Enter text..."
477
+ />
478
+ \`\`\`
479
+
480
+ ### ToggleButton
481
+ Toggle button with controlled state. Auto-prevents signal feedback loops.
482
+
483
+ \`\`\`tsx
484
+ const [active, setActive] = useState(false);
485
+
486
+ <ToggleButton.Root
487
+ active={active}
488
+ onToggled={() => setActive(!active)}
489
+ label="Toggle me"
490
+ />
491
+ \`\`\`
492
+
493
+ ## Display Widgets
494
+
495
+ ### Label.Root
496
+ \`\`\`tsx
497
+ <Label.Root label="Hello World" halign={Gtk.Align.START} wrap useMarkup />
498
+ \`\`\`
499
+
500
+ ### Button
501
+ \`\`\`tsx
502
+ <Button label="Click me" onClicked={() => handleClick()} iconName="document-new" />
503
+ \`\`\`
504
+
505
+ ### MenuButton
506
+ \`\`\`tsx
507
+ <MenuButton.Root label="Options" iconName="open-menu">
508
+ <MenuButton.Popover>
509
+ <PopoverMenu.Root>
510
+ <Menu.Item label="Action" onActivate={handle} />
511
+ </PopoverMenu.Root>
512
+ </MenuButton.Popover>
513
+ </MenuButton.Root>
514
+ \`\`\`
515
+
516
+ ## Menu Widgets
517
+
518
+ ### ApplicationMenu
519
+ \`\`\`tsx
520
+ <ApplicationMenu>
521
+ <Menu.Submenu label="File">
522
+ <Menu.Item label="New" onActivate={handleNew} accels="<Control>n" />
523
+ <Menu.Section>
524
+ <Menu.Item label="Quit" onActivate={quit} accels="<Control>q" />
525
+ </Menu.Section>
526
+ </Menu.Submenu>
527
+ </ApplicationMenu>
528
+ \`\`\`
529
+
530
+ ### Menu.Item
531
+ Props:
532
+ - \`label\`: string
533
+ - \`onActivate\`: \`() => void\`
534
+ - \`accels\`: string | string[] (e.g., "<Control>n")
535
+
536
+ ### Menu.Section
537
+ Groups menu items with optional label.
538
+
539
+ ### Menu.Submenu
540
+ Nested submenu.
541
+
542
+ ## Window Widgets
543
+
544
+ ### ApplicationWindow
545
+ \`\`\`tsx
546
+ <ApplicationWindow
547
+ title="My App"
548
+ defaultWidth={800}
549
+ defaultHeight={600}
550
+ showMenubar
551
+ onCloseRequest={() => false}
552
+ >
553
+ <MainContent />
554
+ </ApplicationWindow>
555
+ \`\`\`
556
+
557
+ ## Common Props
558
+
559
+ All widgets support:
560
+ - \`hexpand\` / \`vexpand\`: boolean (expand to fill space)
561
+ - \`halign\` / \`valign\`: \`Gtk.Align.START\` | \`CENTER\` | \`END\` | \`FILL\`
562
+ - \`marginStart\` / \`marginEnd\` / \`marginTop\` / \`marginBottom\`: number
563
+ - \`sensitive\`: boolean (enabled/disabled)
564
+ - \`visible\`: boolean
565
+ - \`cssClasses\`: string[]
566
+ `;
567
+ };
568
+ const generateExamplesMd = () => {
569
+ return `# GTKX Code Examples
570
+
571
+ ## Application Structure
572
+
573
+ ### Basic App with State
574
+
575
+ \`\`\`tsx
576
+ import { Orientation } from "@gtkx/ffi/gtk";
577
+ import { ApplicationWindow, Box, Label, quit } from "@gtkx/react";
578
+ import { useCallback, useState } from "react";
579
+
580
+ interface Todo {
581
+ id: number;
582
+ text: string;
583
+ completed: boolean;
584
+ }
585
+
586
+ let nextId = 1;
587
+
588
+ export const App = () => {
589
+ const [todos, setTodos] = useState<Todo[]>([]);
590
+
591
+ const addTodo = useCallback((text: string) => {
592
+ setTodos((prev) => [...prev, { id: nextId++, text, completed: false }]);
593
+ }, []);
594
+
595
+ const toggleTodo = useCallback((id: number) => {
596
+ setTodos((prev) =>
597
+ prev.map((todo) =>
598
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
599
+ )
600
+ );
601
+ }, []);
602
+
603
+ return (
604
+ <ApplicationWindow title="Todo App" defaultWidth={400} defaultHeight={500} onCloseRequest={quit}>
605
+ <Box orientation={Orientation.VERTICAL} spacing={16} marginTop={16} marginStart={16} marginEnd={16}>
606
+ <Label.Root label="Todo App" />
607
+ </Box>
608
+ </ApplicationWindow>
609
+ );
610
+ };
611
+
612
+ export const appId = "com.gtkx.todo";
613
+ \`\`\`
614
+
615
+ ## Layout Patterns
616
+
617
+ ### Grid for Forms
618
+
619
+ \`\`\`tsx
620
+ import * as Gtk from "@gtkx/ffi/gtk";
621
+ import { Button, Entry, Grid, Label } from "@gtkx/react";
622
+ import { useState } from "react";
623
+
624
+ const FormLayout = () => {
625
+ const [name, setName] = useState("");
626
+ const [email, setEmail] = useState("");
627
+
628
+ return (
629
+ <Grid.Root rowSpacing={8} columnSpacing={12}>
630
+ <Grid.Child column={0} row={0}>
631
+ <Label.Root label="Name:" halign={Gtk.Align.END} />
632
+ </Grid.Child>
633
+ <Grid.Child column={1} row={0}>
634
+ <Entry text={name} onChanged={(e) => setName(e.getText())} hexpand />
635
+ </Grid.Child>
636
+ <Grid.Child column={0} row={1}>
637
+ <Label.Root label="Email:" halign={Gtk.Align.END} />
638
+ </Grid.Child>
639
+ <Grid.Child column={1} row={1}>
640
+ <Entry text={email} onChanged={(e) => setEmail(e.getText())} hexpand />
641
+ </Grid.Child>
642
+ <Grid.Child column={0} row={2} columnSpan={2}>
643
+ <Button label="Submit" halign={Gtk.Align.END} marginTop={8} />
644
+ </Grid.Child>
645
+ </Grid.Root>
646
+ );
647
+ };
648
+ \`\`\`
649
+
650
+ ### Stack with StackSwitcher
651
+
652
+ \`\`\`tsx
653
+ import * as Gtk from "@gtkx/ffi/gtk";
654
+ import { Box, Label, Stack, StackSwitcher } from "@gtkx/react";
655
+
656
+ const TabContainer = () => (
657
+ <Box orientation={Gtk.Orientation.VERTICAL} spacing={8}>
658
+ <StackSwitcher.Root
659
+ ref={(switcher: Gtk.StackSwitcher | null) => {
660
+ if (switcher) {
661
+ const stack = switcher.getParent()?.getLastChild() as Gtk.Stack | null;
662
+ if (stack) switcher.setStack(stack);
663
+ }
664
+ }}
665
+ />
666
+ <Stack.Root transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT} transitionDuration={200}>
667
+ <Stack.Page name="page1" title="First">
668
+ <Label.Root label="First Page Content" />
669
+ </Stack.Page>
670
+ <Stack.Page name="page2" title="Second">
671
+ <Label.Root label="Second Page Content" />
672
+ </Stack.Page>
673
+ </Stack.Root>
674
+ </Box>
675
+ );
676
+ \`\`\`
677
+
678
+ ## Virtual Scrolling Lists
679
+
680
+ ### ListView with Custom Rendering
681
+
682
+ \`\`\`tsx
683
+ import * as Gtk from "@gtkx/ffi/gtk";
684
+ import { Box, Label, ListView } from "@gtkx/react";
685
+
686
+ interface Task {
687
+ id: string;
688
+ title: string;
689
+ completed: boolean;
690
+ }
691
+
692
+ const tasks: Task[] = [
693
+ { id: "1", title: "Learn GTK4", completed: true },
694
+ { id: "2", title: "Build React app", completed: false },
695
+ ];
696
+
697
+ const TaskList = () => (
698
+ <Box cssClasses={["card"]} heightRequest={250}>
699
+ <ListView.Root
700
+ vexpand
701
+ renderItem={(task: Task | null) => (
702
+ <Label.Root
703
+ label={task?.title ?? ""}
704
+ cssClasses={task?.completed ? ["dim-label"] : []}
705
+ halign={Gtk.Align.START}
706
+ marginStart={12}
707
+ marginTop={8}
708
+ marginBottom={8}
709
+ />
710
+ )}
711
+ >
712
+ {tasks.map((task) => (
713
+ <ListView.Item key={task.id} item={task} />
714
+ ))}
715
+ </ListView.Root>
716
+ </Box>
717
+ );
718
+ \`\`\`
719
+
720
+ ## Menus
721
+
722
+ ### MenuButton with PopoverMenu
723
+
724
+ \`\`\`tsx
725
+ import * as Gtk from "@gtkx/ffi/gtk";
726
+ import { Box, Label, Menu, MenuButton, PopoverMenu } from "@gtkx/react";
727
+ import { useState } from "react";
728
+
729
+ const MenuDemo = () => {
730
+ const [lastAction, setLastAction] = useState<string | null>(null);
731
+
732
+ return (
733
+ <Box orientation={Gtk.Orientation.VERTICAL} spacing={12}>
734
+ <Label.Root label={\`Last action: \${lastAction ?? "(none)"}\`} />
735
+ <MenuButton.Root label="Actions">
736
+ <MenuButton.Popover>
737
+ <PopoverMenu.Root>
738
+ <Menu.Item label="New" onActivate={() => setLastAction("New")} accels="<Control>n" />
739
+ <Menu.Item label="Open" onActivate={() => setLastAction("Open")} accels="<Control>o" />
740
+ <Menu.Item label="Save" onActivate={() => setLastAction("Save")} accels="<Control>s" />
741
+ </PopoverMenu.Root>
742
+ </MenuButton.Popover>
743
+ </MenuButton.Root>
744
+ </Box>
745
+ );
746
+ };
747
+ \`\`\`
748
+
749
+ ## Component Props Pattern
750
+
751
+ ### List Item Component
752
+
753
+ \`\`\`tsx
754
+ import { Box, Button, CheckButton, Label } from "@gtkx/react";
755
+ import { Orientation } from "@gtkx/ffi/gtk";
756
+
757
+ interface Todo {
758
+ id: number;
759
+ text: string;
760
+ completed: boolean;
761
+ }
762
+
763
+ interface TodoItemProps {
764
+ todo: Todo;
765
+ onToggle: (id: number) => void;
766
+ onDelete: (id: number) => void;
767
+ }
768
+
769
+ export const TodoItem = ({ todo, onToggle, onDelete }: TodoItemProps) => (
770
+ <Box orientation={Orientation.HORIZONTAL} spacing={8}>
771
+ <CheckButton active={todo.completed} onToggled={() => onToggle(todo.id)} />
772
+ <Label.Root label={todo.text} hexpand cssClasses={todo.completed ? ["dim-label"] : []} />
773
+ <Button iconName="edit-delete-symbolic" onClicked={() => onDelete(todo.id)} cssClasses={["flat"]} />
774
+ </Box>
775
+ );
776
+ \`\`\`
777
+ `;
778
+ };
100
779
  const generateExampleTest = (testing) => {
101
780
  const imports = testing === "vitest"
102
781
  ? `import { describe, it, expect, afterEach } from "vitest";`
@@ -203,6 +882,11 @@ const suggestAppId = (name) => {
203
882
  const sanitized = name.replace(/-/g, "");
204
883
  return `org.gtkx.${sanitized}`;
205
884
  };
885
+ /**
886
+ * Creates a new GTKX application with interactive prompts.
887
+ * Scaffolds project structure, installs dependencies, and sets up configuration.
888
+ * @param options - Pre-filled options to skip interactive prompts
889
+ */
206
890
  export const createApp = async (options = {}) => {
207
891
  p.intro("Create GTKX App");
208
892
  const name = options.name ??
@@ -274,6 +958,15 @@ export const createApp = async (options = {}) => {
274
958
  p.cancel("Operation cancelled");
275
959
  process.exit(0);
276
960
  }
961
+ const claudeSkills = options.claudeSkills ??
962
+ (await p.confirm({
963
+ message: "Include Claude Code skills?",
964
+ initialValue: true,
965
+ }));
966
+ if (p.isCancel(claudeSkills)) {
967
+ p.cancel("Operation cancelled");
968
+ process.exit(0);
969
+ }
277
970
  const projectPath = resolve(process.cwd(), name);
278
971
  const s = p.spinner();
279
972
  s.start("Creating project structure...");
@@ -287,6 +980,13 @@ export const createApp = async (options = {}) => {
287
980
  writeFileSync(join(projectPath, "src", "app.tsx"), generateAppTsx(name, appId));
288
981
  writeFileSync(join(projectPath, "src", "index.tsx"), generateIndexTsx());
289
982
  writeFileSync(join(projectPath, ".gitignore"), generateGitignore());
983
+ if (claudeSkills) {
984
+ const skillsDir = join(projectPath, ".claude", "skills", "developing-gtkx-apps");
985
+ mkdirSync(skillsDir, { recursive: true });
986
+ writeFileSync(join(skillsDir, "SKILL.md"), generateSkillMd());
987
+ writeFileSync(join(skillsDir, "WIDGETS.md"), generateWidgetsMd());
988
+ writeFileSync(join(skillsDir, "EXAMPLES.md"), generateExamplesMd());
989
+ }
290
990
  if (testing === "vitest") {
291
991
  writeFileSync(join(projectPath, "vitest.config.ts"), generateVitestConfig());
292
992
  writeFileSync(join(projectPath, "tests", "app.test.tsx"), generateExampleTest(testing));
@@ -1,8 +1,11 @@
1
1
  import { type InlineConfig, type ViteDevServer } from "vite";
2
+ /**
3
+ * Options for creating a GTKX development server.
4
+ */
2
5
  export interface DevServerOptions {
3
- /** Path to the app entry file (e.g., "./src/app.tsx") */
6
+ /** Path to the app entry file (e.g., "./src/app.tsx"). */
4
7
  entry: string;
5
- /** Vite configuration overrides */
8
+ /** Vite configuration overrides for customizing the dev server. */
6
9
  vite?: InlineConfig;
7
10
  }
8
11
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/cli",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for GTKX - create and develop GTK4 React applications",
5
5
  "keywords": [
6
6
  "gtk",
@@ -47,8 +47,8 @@
47
47
  "@vitejs/plugin-react": "5.1.2",
48
48
  "citty": "0.1.6",
49
49
  "vite": "7.2.7",
50
- "@gtkx/ffi": "0.3.5",
51
- "@gtkx/react": "0.3.5"
50
+ "@gtkx/ffi": "0.4.0",
51
+ "@gtkx/react": "0.4.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "react": "^19"