@gtkx/cli 0.18.9 → 0.20.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.js CHANGED
@@ -126,7 +126,7 @@ const scaffoldProject = (projectPath, resolved) => {
126
126
  writeFileSync(join(projectPath, "src", "app.tsx"), renderFile("src/app.tsx.ejs", context));
127
127
  writeFileSync(join(projectPath, "src", "dev.tsx"), renderFile("src/dev.tsx.ejs", context));
128
128
  writeFileSync(join(projectPath, "src", "index.tsx"), renderFile("src/index.tsx.ejs", context));
129
- writeFileSync(join(projectPath, "src", "vite-env.d.ts"), renderFile("src/vite-env.d.ts.ejs", context));
129
+ writeFileSync(join(projectPath, "src", "gtkx-env.d.ts"), renderFile("src/gtkx-env.d.ts.ejs", context));
130
130
  writeFileSync(join(projectPath, ".gitignore"), renderFile("gitignore.ejs", context));
131
131
  if (claudeSkills) {
132
132
  const skillsDir = join(projectPath, ".claude", "skills", "developing-gtkx-apps");
@@ -1,11 +1,20 @@
1
1
  import type { Plugin } from "vite";
2
2
  /**
3
- * Vite plugin that resolves static asset imports to filesystem paths.
3
+ * Vite plugin that resolves static asset imports to filesystem paths
4
+ * and handles CSS imports for GTK applications.
4
5
  *
5
- * In dev mode, asset imports resolve to the absolute source file path.
6
- * In build mode, Vite's built-in asset pipeline handles emission and
7
- * hashing; the `renderBuiltUrl` config in the builder converts the
8
- * URL to a filesystem path via `import.meta.url`.
6
+ * **Non-CSS assets:** In dev mode, asset imports resolve to the absolute
7
+ * source file path. In build mode, Vite's built-in asset pipeline handles
8
+ * emission and hashing; the `renderBuiltUrl` config in the builder
9
+ * converts the URL to a filesystem path via `import.meta.url`.
10
+ *
11
+ * **CSS imports (`import "./style.css"`):** Transformed into a module that
12
+ * calls `injectGlobal` from `@gtkx/css` with the file's contents, injecting
13
+ * the styles into the GTK CSS provider at runtime.
14
+ *
15
+ * **CSS URL imports (`import path from "./style.css?url"`):** Handled by
16
+ * Vite's built-in `?url` mechanism, which emits the file as an asset and
17
+ * resolves it to a filesystem path via `renderBuiltUrl`.
9
18
  */
10
19
  export declare function gtkxAssets(): Plugin;
11
20
  //# sourceMappingURL=vite-plugin-gtkx-assets.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin-gtkx-assets.d.ts","sourceRoot":"","sources":["../src/vite-plugin-gtkx-assets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAInC;;;;;;;GAOG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAmBnC"}
1
+ {"version":3,"file":"vite-plugin-gtkx-assets.d.ts","sourceRoot":"","sources":["../src/vite-plugin-gtkx-assets.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAMnC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,IAAI,MAAM,CA4CnC"}
@@ -1,21 +1,52 @@
1
- const ASSET_RE = /\.(png|jpe?g|gif|svg|webp|webm|mp4|ogg|mp3|wav|flac|aac|woff2?|eot|ttf|otf|ico|avif)$/i;
1
+ import { readFileSync } from "node:fs";
2
+ const ASSET_RE = /\.(png|jpe?g|gif|svg|webp|webm|mp4|ogg|mp3|wav|flac|aac|woff2?|eot|ttf|otf|ico|avif|data)$/i;
3
+ const CSS_RE = /\.css$/i;
4
+ const VIRTUAL_PREFIX = "\0gtkx:";
2
5
  /**
3
- * Vite plugin that resolves static asset imports to filesystem paths.
6
+ * Vite plugin that resolves static asset imports to filesystem paths
7
+ * and handles CSS imports for GTK applications.
4
8
  *
5
- * In dev mode, asset imports resolve to the absolute source file path.
6
- * In build mode, Vite's built-in asset pipeline handles emission and
7
- * hashing; the `renderBuiltUrl` config in the builder converts the
8
- * URL to a filesystem path via `import.meta.url`.
9
+ * **Non-CSS assets:** In dev mode, asset imports resolve to the absolute
10
+ * source file path. In build mode, Vite's built-in asset pipeline handles
11
+ * emission and hashing; the `renderBuiltUrl` config in the builder
12
+ * converts the URL to a filesystem path via `import.meta.url`.
13
+ *
14
+ * **CSS imports (`import "./style.css"`):** Transformed into a module that
15
+ * calls `injectGlobal` from `@gtkx/css` with the file's contents, injecting
16
+ * the styles into the GTK CSS provider at runtime.
17
+ *
18
+ * **CSS URL imports (`import path from "./style.css?url"`):** Handled by
19
+ * Vite's built-in `?url` mechanism, which emits the file as an asset and
20
+ * resolves it to a filesystem path via `renderBuiltUrl`.
9
21
  */
10
22
  export function gtkxAssets() {
11
23
  let isBuild = false;
12
24
  return {
13
25
  name: "gtkx:assets",
14
26
  enforce: "pre",
27
+ config() {
28
+ return {
29
+ assetsInclude: [ASSET_RE],
30
+ };
31
+ },
15
32
  configResolved(config) {
16
33
  isBuild = config.command === "build";
17
34
  },
35
+ async resolveId(source, importer, options) {
36
+ if (!CSS_RE.test(source)) {
37
+ return;
38
+ }
39
+ const resolved = await this.resolve(source, importer, { ...options, skipSelf: true });
40
+ if (!resolved || resolved.external)
41
+ return;
42
+ return `${VIRTUAL_PREFIX + resolved.id}?inject`;
43
+ },
18
44
  load(id) {
45
+ if (id.startsWith(VIRTUAL_PREFIX) && id.endsWith("?inject")) {
46
+ const filePath = id.slice(VIRTUAL_PREFIX.length, -"?inject".length);
47
+ const content = readFileSync(filePath, "utf-8");
48
+ return [`import { injectGlobal } from "@gtkx/css";`, `injectGlobal(${JSON.stringify(content)});`].join("\n");
49
+ }
19
50
  if (isBuild || !ASSET_RE.test(id)) {
20
51
  return;
21
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin-gtkx-assets.js","sourceRoot":"","sources":["../src/vite-plugin-gtkx-assets.ts"],"names":[],"mappings":"AAEA,MAAM,QAAQ,GAAG,wFAAwF,CAAC;AAE1G;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU;IACtB,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,OAAO;QACH,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,KAAK;QAEd,cAAc,CAAC,MAAM;YACjB,OAAO,GAAG,MAAM,CAAC,OAAO,KAAK,OAAO,CAAC;QACzC,CAAC;QAED,IAAI,CAAC,EAAE;YACH,IAAI,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBAChC,OAAO;YACX,CAAC;YAED,OAAO,kBAAkB,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC;QACnD,CAAC;KACJ,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"vite-plugin-gtkx-assets.js","sourceRoot":"","sources":["../src/vite-plugin-gtkx-assets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC,MAAM,QAAQ,GAAG,6FAA6F,CAAC;AAC/G,MAAM,MAAM,GAAG,SAAS,CAAC;AACzB,MAAM,cAAc,GAAG,SAAS,CAAC;AAEjC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,UAAU;IACtB,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,OAAO;QACH,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,KAAK;QAEd,MAAM;YACF,OAAO;gBACH,aAAa,EAAE,CAAC,QAAQ,CAAC;aAC5B,CAAC;QACN,CAAC;QAED,cAAc,CAAC,MAAM;YACjB,OAAO,GAAG,MAAM,CAAC,OAAO,KAAK,OAAO,CAAC;QACzC,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO;YACrC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBACvB,OAAO;YACX,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACtF,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ;gBAAE,OAAO;YAE3C,OAAO,GAAG,cAAc,GAAG,QAAQ,CAAC,EAAE,SAAS,CAAC;QACpD,CAAC;QAED,IAAI,CAAC,EAAE;YACH,IAAI,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC1D,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBACpE,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAChD,OAAO,CAAC,2CAA2C,EAAE,gBAAgB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAClG,IAAI,CACP,CAAC;YACN,CAAC;YAED,IAAI,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBAChC,OAAO;YACX,CAAC;YAED,OAAO,kBAAkB,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC;QACnD,CAAC;KACJ,CAAC;AACN,CAAC"}
package/env.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module "*.css?url" {
4
+ const path: string;
5
+ export default path;
6
+ }
7
+
8
+ declare module "*.data" {
9
+ const path: string;
10
+ export default path;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/cli",
3
- "version": "0.18.9",
3
+ "version": "0.20.0",
4
4
  "description": "CLI for GTKX - create and develop GTK4 React applications",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -45,7 +45,8 @@
45
45
  "./refresh-runtime": {
46
46
  "types": "./dist/refresh-runtime.d.ts",
47
47
  "default": "./dist/refresh-runtime.js"
48
- }
48
+ },
49
+ "./env": "./env.d.ts"
49
50
  },
50
51
  "bin": {
51
52
  "gtkx": "bin/gtkx.js"
@@ -54,29 +55,30 @@
54
55
  "files": [
55
56
  "bin",
56
57
  "dist",
58
+ "env.d.ts",
57
59
  "src",
58
60
  "templates"
59
61
  ],
60
62
  "dependencies": {
61
- "@clack/prompts": "^1.0.0",
63
+ "@clack/prompts": "^1.0.1",
62
64
  "@swc/core": "^1.15.11",
63
- "citty": "^0.2.0",
65
+ "citty": "^0.2.1",
64
66
  "ejs": "^4.0.1",
65
67
  "react-refresh": "^0.18.0",
66
68
  "vite": "^7.3.1",
67
- "@gtkx/ffi": "0.18.9",
68
- "@gtkx/mcp": "0.18.9",
69
- "@gtkx/react": "0.18.9"
69
+ "@gtkx/ffi": "0.20.0",
70
+ "@gtkx/mcp": "0.20.0",
71
+ "@gtkx/react": "0.20.0"
70
72
  },
71
73
  "devDependencies": {
72
74
  "@types/ejs": "^3.1.5",
73
75
  "@types/react-refresh": "^0.14.7",
74
76
  "memfs": "^4.56.10",
75
- "@gtkx/testing": "0.18.9"
77
+ "@gtkx/testing": "0.20.0"
76
78
  },
77
79
  "peerDependencies": {
78
80
  "react": "^19",
79
- "@gtkx/testing": "0.18.9"
81
+ "@gtkx/testing": "0.20.0"
80
82
  },
81
83
  "peerDependenciesMeta": {
82
84
  "@gtkx/testing": {
@@ -85,6 +87,7 @@
85
87
  },
86
88
  "scripts": {
87
89
  "build": "tsc -b",
88
- "test": "vitest run"
90
+ "test": "vitest run",
91
+ "typecheck": "tsc -b --emitDeclarationOnly"
89
92
  }
90
93
  }
package/src/create.ts CHANGED
@@ -196,7 +196,7 @@ const scaffoldProject = (projectPath: string, resolved: ResolvedOptions): void =
196
196
  writeFileSync(join(projectPath, "src", "app.tsx"), renderFile("src/app.tsx.ejs", context));
197
197
  writeFileSync(join(projectPath, "src", "dev.tsx"), renderFile("src/dev.tsx.ejs", context));
198
198
  writeFileSync(join(projectPath, "src", "index.tsx"), renderFile("src/index.tsx.ejs", context));
199
- writeFileSync(join(projectPath, "src", "vite-env.d.ts"), renderFile("src/vite-env.d.ts.ejs", context));
199
+ writeFileSync(join(projectPath, "src", "gtkx-env.d.ts"), renderFile("src/gtkx-env.d.ts.ejs", context));
200
200
  writeFileSync(join(projectPath, ".gitignore"), renderFile("gitignore.ejs", context));
201
201
 
202
202
  if (claudeSkills) {
@@ -1,14 +1,26 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import type { Plugin } from "vite";
2
3
 
3
- const ASSET_RE = /\.(png|jpe?g|gif|svg|webp|webm|mp4|ogg|mp3|wav|flac|aac|woff2?|eot|ttf|otf|ico|avif)$/i;
4
+ const ASSET_RE = /\.(png|jpe?g|gif|svg|webp|webm|mp4|ogg|mp3|wav|flac|aac|woff2?|eot|ttf|otf|ico|avif|data)$/i;
5
+ const CSS_RE = /\.css$/i;
6
+ const VIRTUAL_PREFIX = "\0gtkx:";
4
7
 
5
8
  /**
6
- * Vite plugin that resolves static asset imports to filesystem paths.
9
+ * Vite plugin that resolves static asset imports to filesystem paths
10
+ * and handles CSS imports for GTK applications.
7
11
  *
8
- * In dev mode, asset imports resolve to the absolute source file path.
9
- * In build mode, Vite's built-in asset pipeline handles emission and
10
- * hashing; the `renderBuiltUrl` config in the builder converts the
11
- * URL to a filesystem path via `import.meta.url`.
12
+ * **Non-CSS assets:** In dev mode, asset imports resolve to the absolute
13
+ * source file path. In build mode, Vite's built-in asset pipeline handles
14
+ * emission and hashing; the `renderBuiltUrl` config in the builder
15
+ * converts the URL to a filesystem path via `import.meta.url`.
16
+ *
17
+ * **CSS imports (`import "./style.css"`):** Transformed into a module that
18
+ * calls `injectGlobal` from `@gtkx/css` with the file's contents, injecting
19
+ * the styles into the GTK CSS provider at runtime.
20
+ *
21
+ * **CSS URL imports (`import path from "./style.css?url"`):** Handled by
22
+ * Vite's built-in `?url` mechanism, which emits the file as an asset and
23
+ * resolves it to a filesystem path via `renderBuiltUrl`.
12
24
  */
13
25
  export function gtkxAssets(): Plugin {
14
26
  let isBuild = false;
@@ -17,11 +29,36 @@ export function gtkxAssets(): Plugin {
17
29
  name: "gtkx:assets",
18
30
  enforce: "pre",
19
31
 
32
+ config() {
33
+ return {
34
+ assetsInclude: [ASSET_RE],
35
+ };
36
+ },
37
+
20
38
  configResolved(config) {
21
39
  isBuild = config.command === "build";
22
40
  },
23
41
 
42
+ async resolveId(source, importer, options) {
43
+ if (!CSS_RE.test(source)) {
44
+ return;
45
+ }
46
+
47
+ const resolved = await this.resolve(source, importer, { ...options, skipSelf: true });
48
+ if (!resolved || resolved.external) return;
49
+
50
+ return `${VIRTUAL_PREFIX + resolved.id}?inject`;
51
+ },
52
+
24
53
  load(id) {
54
+ if (id.startsWith(VIRTUAL_PREFIX) && id.endsWith("?inject")) {
55
+ const filePath = id.slice(VIRTUAL_PREFIX.length, -"?inject".length);
56
+ const content = readFileSync(filePath, "utf-8");
57
+ return [`import { injectGlobal } from "@gtkx/css";`, `injectGlobal(${JSON.stringify(content)});`].join(
58
+ "\n",
59
+ );
60
+ }
61
+
25
62
  if (isBuild || !ASSET_RE.test(id)) {
26
63
  return;
27
64
  }
@@ -101,7 +101,7 @@ const LoginForm = () => {
101
101
 
102
102
  ```tsx
103
103
  import * as Gtk from "@gtkx/ffi/gtk";
104
- import { GtkBox, GtkButton, GtkEntry, GtkLabel, GtkScrolledWindow, x } from "@gtkx/react";
104
+ import { GtkBox, GtkButton, GtkEntry, GtkLabel, GtkScrolledWindow } from "@gtkx/react";
105
105
  import { useCallback, useState } from "react";
106
106
 
107
107
  interface Todo {
@@ -133,17 +133,14 @@ const TodoList = () => {
133
133
  </GtkBox>
134
134
  <GtkScrolledWindow vexpand cssClasses={["card"]}>
135
135
  <GtkListView
136
- renderItem={(todo) => (
136
+ items={todos.map((todo) => ({ id: todo.id, value: todo }))}
137
+ renderItem={(todo: Todo) => (
137
138
  <GtkBox spacing={8} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
138
- <GtkLabel label={todo?.text ?? ""} hexpand halign={Gtk.Align.START} />
139
- <GtkButton iconName="edit-delete-symbolic" cssClasses={["flat"]} onClicked={() => todo && deleteTodo(todo.id)} />
139
+ <GtkLabel label={todo.text} hexpand halign={Gtk.Align.START} />
140
+ <GtkButton iconName="edit-delete-symbolic" cssClasses={["flat"]} onClicked={() => deleteTodo(todo.id)} />
140
141
  </GtkBox>
141
142
  )}
142
- >
143
- {todos.map((todo) => (
144
- <x.ListItem key={todo.id} id={todo.id} value={todo} />
145
- ))}
146
- </GtkListView>
143
+ />
147
144
  </GtkScrolledWindow>
148
145
  </GtkBox>
149
146
  );
@@ -158,7 +155,7 @@ const TodoList = () => {
158
155
 
159
156
  ```tsx
160
157
  import * as Gtk from "@gtkx/ffi/gtk";
161
- import { GtkBox, GtkLabel, GtkPaned, GtkScrolledWindow, GtkStack, x } from "@gtkx/react";
158
+ import { GtkBox, GtkLabel, GtkListView, GtkPaned, GtkScrolledWindow, GtkStack, x } from "@gtkx/react";
162
159
  import { useState } from "react";
163
160
 
164
161
  interface Page {
@@ -183,14 +180,11 @@ const SidebarNav = () => {
183
180
  selected={[currentPage]}
184
181
  selectionMode={Gtk.SelectionMode.SINGLE}
185
182
  onSelectionChanged={(ids) => setCurrentPage(ids[0])}
186
- renderItem={(page) => (
187
- <GtkLabel label={page?.name ?? ""} halign={Gtk.Align.START} marginStart={12} marginTop={8} marginBottom={8} />
183
+ items={pages.map((page) => ({ id: page.id, value: page }))}
184
+ renderItem={(page: Page) => (
185
+ <GtkLabel label={page.name} halign={Gtk.Align.START} marginStart={12} marginTop={8} marginBottom={8} />
188
186
  )}
189
- >
190
- {pages.map((page) => (
191
- <x.ListItem key={page.id} id={page.id} value={page} />
192
- ))}
193
- </GtkListView>
187
+ />
194
188
  </GtkScrolledWindow>
195
189
  </x.Slot>
196
190
  <x.Slot for={GtkPaned} id="endChild">
@@ -339,13 +333,16 @@ const FileTable = () => {
339
333
 
340
334
  return (
341
335
  <GtkScrolledWindow vexpand cssClasses={["card"]}>
342
- <GtkColumnView estimatedRowHeight={48} sortColumn={sortColumn} sortOrder={sortOrder} onSortChanged={handleSort}>
343
- <x.ColumnViewColumn<FileItem> title="Name" id="name" expand sortable renderCell={(f) => <GtkLabel label={f?.name ?? ""} />} />
344
- <x.ColumnViewColumn<FileItem> title="Size" id="size" fixedWidth={100} sortable renderCell={(f) => <GtkLabel label={`${f?.size ?? 0} KB`} />} />
345
- <x.ColumnViewColumn<FileItem> title="Modified" id="modified" fixedWidth={120} sortable renderCell={(f) => <GtkLabel label={f?.modified ?? ""} />} />
346
- {sortedFiles.map((file) => (
347
- <x.ListItem key={file.id} id={file.id} value={file} />
348
- ))}
336
+ <GtkColumnView
337
+ estimatedRowHeight={48}
338
+ sortColumn={sortColumn}
339
+ sortOrder={sortOrder}
340
+ onSortChanged={handleSort}
341
+ items={sortedFiles.map((file) => ({ id: file.id, value: file }))}
342
+ >
343
+ <x.ColumnViewColumn title="Name" id="name" expand sortable renderCell={(f: FileItem) => <GtkLabel label={f.name} />} />
344
+ <x.ColumnViewColumn title="Size" id="size" fixedWidth={100} sortable renderCell={(f: FileItem) => <GtkLabel label={`${f.size} KB`} />} />
345
+ <x.ColumnViewColumn title="Modified" id="modified" fixedWidth={120} sortable renderCell={(f: FileItem) => <GtkLabel label={f.modified} />} />
349
346
  </GtkColumnView>
350
347
  </GtkScrolledWindow>
351
348
  );
@@ -398,7 +395,7 @@ const MenuDemo = () => {
398
395
 
399
396
  ```tsx
400
397
  import * as Gtk from "@gtkx/ffi/gtk";
401
- import { AdwSpinner, GtkBox, GtkLabel, GtkScrolledWindow, x } from "@gtkx/react";
398
+ import { AdwSpinner, GtkBox, GtkLabel, GtkListView, GtkScrolledWindow } from "@gtkx/react";
402
399
  import { useEffect, useState } from "react";
403
400
 
404
401
  interface User {
@@ -442,17 +439,14 @@ const AsyncList = () => {
442
439
  return (
443
440
  <GtkScrolledWindow vexpand>
444
441
  <GtkListView
445
- renderItem={(user) => (
442
+ items={users.map((user) => ({ id: user.id, value: user }))}
443
+ renderItem={(user: User) => (
446
444
  <GtkBox orientation={Gtk.Orientation.VERTICAL} marginStart={12} marginTop={8} marginBottom={8}>
447
- <GtkLabel label={user?.name ?? ""} halign={Gtk.Align.START} cssClasses={["heading"]} />
448
- <GtkLabel label={user?.email ?? ""} halign={Gtk.Align.START} cssClasses={["dim-label"]} />
445
+ <GtkLabel label={user.name} halign={Gtk.Align.START} cssClasses={["heading"]} />
446
+ <GtkLabel label={user.email} halign={Gtk.Align.START} cssClasses={["dim-label"]} />
449
447
  </GtkBox>
450
448
  )}
451
- >
452
- {users.map((user) => (
453
- <x.ListItem key={user.id} id={user.id} value={user} />
454
- ))}
455
- </GtkListView>
449
+ />
456
450
  </GtkScrolledWindow>
457
451
  );
458
452
  };
@@ -632,7 +626,7 @@ const SplitViewDemo = () => {
632
626
 
633
627
  ```tsx
634
628
  import * as Gtk from "@gtkx/ffi/gtk";
635
- import { GtkBox, GtkImage, GtkLabel, GtkListView, GtkScrolledWindow, x } from "@gtkx/react";
629
+ import { GtkBox, GtkImage, GtkLabel, GtkListView, GtkScrolledWindow } from "@gtkx/react";
636
630
  import { useState } from "react";
637
631
 
638
632
  interface FileNode {
@@ -667,21 +661,18 @@ const FileBrowser = () => {
667
661
  selectionMode={Gtk.SelectionMode.SINGLE}
668
662
  selected={selected ? [selected] : []}
669
663
  onSelectionChanged={(ids) => setSelected(ids[0] ?? null)}
670
- renderItem={(item) => (
664
+ items={files.map((file) => ({
665
+ id: file.id,
666
+ value: file,
667
+ children: file.children?.map((child) => ({ id: child.id, value: child })),
668
+ }))}
669
+ renderItem={(item: FileNode) => (
671
670
  <GtkBox spacing={8}>
672
- <GtkImage iconName={item?.isDirectory ? "folder-symbolic" : "text-x-generic-symbolic"} />
673
- <GtkLabel label={item?.name ?? ""} halign={Gtk.Align.START} />
671
+ <GtkImage iconName={item.isDirectory ? "folder-symbolic" : "text-x-generic-symbolic"} />
672
+ <GtkLabel label={item.name} halign={Gtk.Align.START} />
674
673
  </GtkBox>
675
674
  )}
676
- >
677
- {files.map((file) => (
678
- <x.ListItem key={file.id} id={file.id} value={file}>
679
- {file.children?.map((child) => (
680
- <x.ListItem key={child.id} id={child.id} value={child} />
681
- ))}
682
- </x.ListItem>
683
- ))}
684
- </GtkListView>
675
+ />
685
676
  </GtkScrolledWindow>
686
677
  );
687
678
  };
@@ -130,7 +130,7 @@ Scrollable container.
130
130
 
131
131
  ## Virtual Lists
132
132
 
133
- All virtual list widgets use `ListItem` children and a `renderItem` function.
133
+ All virtual list widgets use an `items` data prop and a `renderItem` function. Items are `{ id: string, value: T }` objects.
134
134
 
135
135
  ### GtkListView
136
136
  High-performance scrollable list with selection.
@@ -142,10 +142,9 @@ High-performance scrollable list with selection.
142
142
  selected={selectedId ? [selectedId] : []}
143
143
  selectionMode={Gtk.SelectionMode.SINGLE}
144
144
  onSelectionChanged={(ids) => setSelectedId(ids[0])}
145
- renderItem={(item) => <GtkLabel label={item?.name ?? ""} />}
146
- >
147
- {items.map(item => <x.ListItem key={item.id} id={item.id} value={item} />)}
148
- </GtkListView>
145
+ items={items.map(item => ({ id: item.id, value: item }))}
146
+ renderItem={(item: Item) => <GtkLabel label={item.name} />}
147
+ />
149
148
  ```
150
149
 
151
150
  ### GtkGridView
@@ -156,37 +155,41 @@ Grid-based virtual scrolling.
156
155
  estimatedItemHeight={100}
157
156
  minColumns={2}
158
157
  maxColumns={4}
159
- renderItem={(item) => (
158
+ items={items.map(item => ({ id: item.id, value: item }))}
159
+ renderItem={(item: Item) => (
160
160
  <GtkBox orientation={Gtk.Orientation.VERTICAL}>
161
- <GtkImage iconName={item?.icon ?? "image-missing"} />
162
- <GtkLabel label={item?.name ?? ""} />
161
+ <GtkImage iconName={item.icon} />
162
+ <GtkLabel label={item.name} />
163
163
  </GtkBox>
164
164
  )}
165
- >
166
- {items.map(item => <x.ListItem key={item.id} id={item.id} value={item} />)}
167
- </GtkGridView>
165
+ />
168
166
  ```
169
167
 
170
168
  ### GtkColumnView
171
169
  Table with sortable columns.
172
170
 
173
171
  ```tsx
174
- <GtkColumnView estimatedRowHeight={48} sortColumn="name" sortOrder={Gtk.SortType.ASCENDING} onSortChanged={handleSort}>
175
- <x.ColumnViewColumn<Item>
172
+ <GtkColumnView
173
+ estimatedRowHeight={48}
174
+ sortColumn="name"
175
+ sortOrder={Gtk.SortType.ASCENDING}
176
+ onSortChanged={handleSort}
177
+ items={items.map(item => ({ id: item.id, value: item }))}
178
+ >
179
+ <x.ColumnViewColumn
176
180
  title="Name"
177
181
  id="name"
178
182
  expand
179
183
  resizable
180
184
  sortable
181
- renderCell={(item) => <GtkLabel label={item?.name ?? ""} />}
185
+ renderCell={(item: Item) => <GtkLabel label={item.name} />}
182
186
  />
183
- <x.ColumnViewColumn<Item>
187
+ <x.ColumnViewColumn
184
188
  title="Size"
185
189
  id="size"
186
190
  fixedWidth={100}
187
- renderCell={(item) => <GtkLabel label={`${item?.size ?? 0} KB`} />}
191
+ renderCell={(item: Item) => <GtkLabel label={`${item.size} KB`} />}
188
192
  />
189
- {items.map(item => <x.ListItem key={item.id} id={item.id} value={item} />)}
190
193
  </GtkColumnView>
191
194
  ```
192
195
 
@@ -194,13 +197,15 @@ Table with sortable columns.
194
197
  Selection dropdown.
195
198
 
196
199
  ```tsx
197
- <GtkDropDown selectedId={selectedId} onSelectionChanged={setSelectedId}>
198
- {options.map(opt => <x.ListItem key={opt.id} id={opt.id} value={opt.label} />)}
199
- </GtkDropDown>
200
+ <GtkDropDown
201
+ selectedId={selectedId}
202
+ onSelectionChanged={setSelectedId}
203
+ items={options.map(opt => ({ id: opt.id, value: opt.label }))}
204
+ />
200
205
  ```
201
206
 
202
207
  ### GtkListView (tree mode)
203
- Hierarchical tree with expand/collapse. Nesting `x.ListItem` children triggers tree behavior.
208
+ Hierarchical tree with expand/collapse. Items with nested `children` arrays trigger tree behavior.
204
209
 
205
210
  ```tsx
206
211
  <GtkListView
@@ -210,24 +215,21 @@ Hierarchical tree with expand/collapse. Nesting `x.ListItem` children triggers t
210
215
  selectionMode={Gtk.SelectionMode.SINGLE}
211
216
  selected={selectedId ? [selectedId] : []}
212
217
  onSelectionChanged={(ids) => setSelectedId(ids[0])}
213
- renderItem={(item, row) => (
218
+ items={files.map(file => ({
219
+ id: file.id,
220
+ value: file,
221
+ children: file.children?.map(child => ({ id: child.id, value: child })),
222
+ }))}
223
+ renderItem={(item: FileNode, row) => (
214
224
  <GtkBox spacing={8}>
215
- <GtkImage iconName={item?.isDirectory ? "folder-symbolic" : "text-x-generic-symbolic"} />
216
- <GtkLabel label={item?.name ?? ""} />
225
+ <GtkImage iconName={item.isDirectory ? "folder-symbolic" : "text-x-generic-symbolic"} />
226
+ <GtkLabel label={item.name} />
217
227
  </GtkBox>
218
228
  )}
219
- >
220
- {files.map(file => (
221
- <x.ListItem key={file.id} id={file.id} value={file}>
222
- {file.children?.map(child => (
223
- <x.ListItem key={child.id} id={child.id} value={child} />
224
- ))}
225
- </x.ListItem>
226
- ))}
227
- </GtkListView>
229
+ />
228
230
  ```
229
231
 
230
- **ListItem tree props:** `id`, `value`, `indentForDepth`, `indentForIcon`, `hideExpander`, nested `children`
232
+ **ListItem data props:** `id`, `value`, `children` (nested items for tree mode), `hideExpander`, `indentForDepth`, `indentForIcon`, `section` (for sectioned lists)
231
233
 
232
234
  ---
233
235
 
@@ -799,14 +801,14 @@ Text content is provided as direct children. Use `x.TextTag` for formatting and
799
801
 
800
802
  ## Keyboard Shortcuts
801
803
 
802
- Attach shortcuts with `x.ShortcutController` and `x.Shortcut`:
804
+ Attach shortcuts with `<GtkShortcutController>` and `x.Shortcut`:
803
805
 
804
806
  ```tsx
805
807
  <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} focusable>
806
- <x.ShortcutController scope={Gtk.ShortcutScope.LOCAL}>
808
+ <GtkShortcutController scope={Gtk.ShortcutScope.LOCAL}>
807
809
  <x.Shortcut trigger="<Control>equal" onActivate={() => setCount((c) => c + 1)} />
808
810
  <x.Shortcut trigger="<Control>minus" onActivate={() => setCount((c) => c - 1)} />
809
- </x.ShortcutController>
811
+ </GtkShortcutController>
810
812
  <GtkLabel label={`Count: ${count}`} />
811
813
  </GtkBox>
812
814
  ```
@@ -6,6 +6,7 @@
6
6
  "scripts": {
7
7
  "dev": "gtkx dev src/dev.tsx",
8
8
  "build": "gtkx build",
9
+ "typecheck": "tsc --noEmit",
9
10
  "start": "node dist/bundle.js"<% if (testing === 'vitest') { %>,
10
11
  "test": "vitest"<% } %>
11
12
  },
@@ -0,0 +1 @@
1
+ /// <reference types="@gtkx/cli/env" />
@@ -1 +0,0 @@
1
- /// <reference types="vite/client" />