@gtkx/cli 0.17.3 → 0.18.1

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.
@@ -0,0 +1,55 @@
1
+ import { type InlineConfig } from "vite";
2
+ /**
3
+ * Options for building a GTKX application for production.
4
+ */
5
+ export type BuildOptions = {
6
+ /** Path to the entry file (e.g., "src/index.tsx") */
7
+ entry: string;
8
+ /**
9
+ * Base path for resolving asset imports at runtime, relative to the
10
+ * executable directory.
11
+ *
12
+ * When set, asset imports resolve to
13
+ * `path.join(path.dirname(process.execPath), assetBase, filename)`.
14
+ * This is useful for FHS-compliant packaging where assets live under
15
+ * a `share/` directory rather than next to the binary.
16
+ *
17
+ * When omitted, assets resolve relative to the bundle via
18
+ * `import.meta.url`, which works when assets are co-located with
19
+ * the executable (e.g., in `bin/assets/`).
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * await build({
24
+ * entry: "./src/index.tsx",
25
+ * assetBase: "../share/my-app",
26
+ * });
27
+ * ```
28
+ */
29
+ assetBase?: string;
30
+ /** Additional Vite configuration */
31
+ vite?: InlineConfig;
32
+ };
33
+ /**
34
+ * Builds a GTKX application for production using Vite's SSR build mode.
35
+ *
36
+ * Produces a single minified ESM bundle at `dist/bundle.js` with all
37
+ * dependencies inlined. The native `.node` binary is copied into the
38
+ * output directory as `gtkx.node`, making the bundle fully self-contained
39
+ * with no `node_modules` dependency at runtime.
40
+ *
41
+ * @param options - Build configuration including entry point and Vite options
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { build } from "@gtkx/cli";
46
+ *
47
+ * await build({
48
+ * entry: "./src/index.tsx",
49
+ * vite: { root: process.cwd() },
50
+ * });
51
+ * ```
52
+ *
53
+ * @see {@link BuildOptions} for configuration options
54
+ */
55
+ export declare const build: (options: BuildOptions) => Promise<void>;
@@ -0,0 +1,60 @@
1
+ import { build as viteBuild } from "vite";
2
+ import { gtkxAssets } from "./vite-plugin-gtkx-assets.js";
3
+ import { gtkxBuiltUrl } from "./vite-plugin-gtkx-built-url.js";
4
+ import { gtkxNative } from "./vite-plugin-gtkx-native.js";
5
+ /**
6
+ * Builds a GTKX application for production using Vite's SSR build mode.
7
+ *
8
+ * Produces a single minified ESM bundle at `dist/bundle.js` with all
9
+ * dependencies inlined. The native `.node` binary is copied into the
10
+ * output directory as `gtkx.node`, making the bundle fully self-contained
11
+ * with no `node_modules` dependency at runtime.
12
+ *
13
+ * @param options - Build configuration including entry point and Vite options
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { build } from "@gtkx/cli";
18
+ *
19
+ * await build({
20
+ * entry: "./src/index.tsx",
21
+ * vite: { root: process.cwd() },
22
+ * });
23
+ * ```
24
+ *
25
+ * @see {@link BuildOptions} for configuration options
26
+ */
27
+ export const build = async (options) => {
28
+ const { entry, assetBase, vite: viteConfig } = options;
29
+ const root = viteConfig?.root ?? process.cwd();
30
+ await viteBuild({
31
+ ...viteConfig,
32
+ plugins: [...(viteConfig?.plugins ?? []), gtkxAssets(), gtkxBuiltUrl(assetBase), gtkxNative(root)],
33
+ build: {
34
+ ...viteConfig?.build,
35
+ ssr: entry,
36
+ ssrEmitAssets: true,
37
+ assetsInlineLimit: 0,
38
+ outDir: viteConfig?.build?.outDir ?? "dist",
39
+ minify: true,
40
+ rollupOptions: {
41
+ ...viteConfig?.build?.rollupOptions,
42
+ output: {
43
+ ...(viteConfig?.build?.rollupOptions?.output ?? {}),
44
+ entryFileNames: "bundle.js",
45
+ },
46
+ },
47
+ },
48
+ define: {
49
+ ...viteConfig?.define,
50
+ "process.env.NODE_ENV": JSON.stringify("production"),
51
+ },
52
+ ssr: {
53
+ ...viteConfig?.ssr,
54
+ noExternal: true,
55
+ },
56
+ experimental: {
57
+ ...viteConfig?.experimental,
58
+ },
59
+ });
60
+ };
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { resolve } from "node:path";
6
6
  import { events } from "@gtkx/ffi";
7
7
  import { render } from "@gtkx/react";
8
8
  import { defineCommand, runMain } from "citty";
9
+ import { build } from "./builder.js";
9
10
  import { createApp } from "./create.js";
10
11
  import { createDevServer } from "./dev-server.js";
11
12
  import { startMcpClient, stopMcpClient } from "./mcp-client.js";
@@ -49,6 +50,35 @@ const dev = defineCommand({
49
50
  console.log("[gtkx] HMR enabled - watching for changes...");
50
51
  },
51
52
  });
53
+ const buildCmd = defineCommand({
54
+ meta: {
55
+ name: "build",
56
+ description: "Build application for production",
57
+ },
58
+ args: {
59
+ entry: {
60
+ type: "positional",
61
+ description: "Entry file (default: src/index.tsx)",
62
+ required: false,
63
+ },
64
+ "asset-base": {
65
+ type: "string",
66
+ description: "Asset base path relative to executable directory (e.g., ../share/my-app)",
67
+ },
68
+ },
69
+ async run({ args }) {
70
+ const entry = resolve(process.cwd(), args.entry ?? "src/index.tsx");
71
+ console.log(`[gtkx] Building ${entry}`);
72
+ await build({
73
+ entry,
74
+ assetBase: args["asset-base"],
75
+ vite: {
76
+ root: process.cwd(),
77
+ },
78
+ });
79
+ console.log("[gtkx] Build complete: dist/bundle.js");
80
+ },
81
+ });
52
82
  const create = defineCommand({
53
83
  meta: {
54
84
  name: "create",
@@ -95,6 +125,7 @@ const main = defineCommand({
95
125
  },
96
126
  subCommands: {
97
127
  dev,
128
+ build: buildCmd,
98
129
  create,
99
130
  },
100
131
  });
package/dist/create.js CHANGED
@@ -4,7 +4,7 @@ import { join, resolve } from "node:path";
4
4
  import * as p from "@clack/prompts";
5
5
  import { renderFile } from "./templates.js";
6
6
  const DEPENDENCIES = ["@gtkx/css", "@gtkx/ffi", "@gtkx/react", "react"];
7
- const DEV_DEPENDENCIES = ["@gtkx/cli", "@types/react", "typescript"];
7
+ const DEV_DEPENDENCIES = ["@gtkx/cli", "@types/react", "typescript", "vite"];
8
8
  const TESTING_DEV_DEPENDENCIES = ["@gtkx/testing", "@gtkx/vitest", "vitest"];
9
9
  const createTemplateContext = (name, appId, testing) => {
10
10
  const title = name
@@ -126,6 +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
130
  writeFileSync(join(projectPath, ".gitignore"), renderFile("gitignore.ejs", context));
130
131
  if (claudeSkills) {
131
132
  const skillsDir = join(projectPath, ".claude", "skills", "developing-gtkx-apps");
@@ -3,6 +3,7 @@ import { events } from "@gtkx/ffi";
3
3
  import { setHotReloading, update } from "@gtkx/react";
4
4
  import { createServer } from "vite";
5
5
  import { isReactRefreshBoundary, performRefresh } from "./refresh-runtime.js";
6
+ import { gtkxAssets } from "./vite-plugin-gtkx-assets.js";
6
7
  import { gtkxRefresh } from "./vite-plugin-gtkx-refresh.js";
7
8
  import { swcSsrRefresh } from "./vite-plugin-swc-ssr-refresh.js";
8
9
  /**
@@ -37,6 +38,7 @@ export const createDevServer = async (options) => {
37
38
  ...viteConfig,
38
39
  appType: "custom",
39
40
  plugins: [
41
+ gtkxAssets(),
40
42
  swcSsrRefresh(),
41
43
  gtkxRefresh(),
42
44
  {
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export { type BuildOptions, build } from "./builder.js";
1
2
  export { createApp } from "./create.js";
2
3
  export { createDevServer, type DevServerOptions } from "./dev-server.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
+ export { build } from "./builder.js";
1
2
  export { createApp } from "./create.js";
2
3
  export { createDevServer } from "./dev-server.js";
@@ -1,9 +1,19 @@
1
1
  import * as net from "node:net";
2
- import { getNativeId, getNativeInterface } from "@gtkx/ffi";
2
+ import { getNativeInterface } from "@gtkx/ffi";
3
+ import * as Gio from "@gtkx/ffi/gio";
3
4
  import { Value } from "@gtkx/ffi/gobject";
4
5
  import * as Gtk from "@gtkx/ffi/gtk";
5
6
  import { DEFAULT_SOCKET_PATH, IpcRequestSchema, IpcResponseSchema, McpError, McpErrorCode, methodNotFoundError, widgetNotFoundError, } from "@gtkx/mcp";
6
- import { getApplication } from "@gtkx/react";
7
+ const widgetIdMap = new WeakMap();
8
+ let nextWidgetId = 0;
9
+ const getWidgetId = (widget) => {
10
+ let id = widgetIdMap.get(widget);
11
+ if (!id) {
12
+ id = String(nextWidgetId++);
13
+ widgetIdMap.set(widget, id);
14
+ }
15
+ return id;
16
+ };
7
17
  let testingModule = null;
8
18
  let testingLoadError = null;
9
19
  const loadTestingModule = async () => {
@@ -28,34 +38,16 @@ const formatRole = (role) => {
28
38
  return Gtk.AccessibleRole[role] ?? String(role);
29
39
  };
30
40
  const getWidgetText = (widget) => {
31
- const role = widget.getAccessibleRole();
32
- if (role === undefined)
33
- return null;
34
- switch (role) {
35
- case Gtk.AccessibleRole.BUTTON:
36
- case Gtk.AccessibleRole.LINK:
37
- case Gtk.AccessibleRole.TAB:
38
- return widget.getLabel?.() ?? widget.getLabel?.() ?? null;
39
- case Gtk.AccessibleRole.TOGGLE_BUTTON:
40
- return widget.getLabel?.() ?? null;
41
- case Gtk.AccessibleRole.CHECKBOX:
42
- case Gtk.AccessibleRole.RADIO:
43
- return widget.getLabel?.() ?? null;
44
- case Gtk.AccessibleRole.LABEL:
45
- return widget.getLabel?.() ?? widget.getText?.() ?? null;
46
- case Gtk.AccessibleRole.TEXT_BOX:
47
- case Gtk.AccessibleRole.SEARCH_BOX:
48
- case Gtk.AccessibleRole.SPIN_BUTTON:
49
- return getNativeInterface(widget, Gtk.Editable)?.getText() ?? null;
50
- case Gtk.AccessibleRole.GROUP:
51
- return widget.getLabel?.() ?? null;
52
- case Gtk.AccessibleRole.WINDOW:
53
- case Gtk.AccessibleRole.DIALOG:
54
- case Gtk.AccessibleRole.ALERT_DIALOG:
55
- return widget.getTitle() ?? null;
56
- default:
57
- return null;
41
+ if ("getLabel" in widget && typeof widget.getLabel === "function") {
42
+ return widget.getLabel() ?? null;
43
+ }
44
+ if ("getText" in widget && typeof widget.getText === "function") {
45
+ return widget.getText() ?? null;
46
+ }
47
+ if ("getTitle" in widget && typeof widget.getTitle === "function") {
48
+ return widget.getTitle() ?? null;
58
49
  }
50
+ return getNativeInterface(widget, Gtk.Editable)?.getText() ?? null;
59
51
  };
60
52
  const serializeWidget = (widget) => {
61
53
  const children = [];
@@ -66,7 +58,7 @@ const serializeWidget = (widget) => {
66
58
  }
67
59
  const text = getWidgetText(widget);
68
60
  return {
69
- id: String(getNativeId(widget.handle)),
61
+ id: getWidgetId(widget),
70
62
  type: widget.constructor.name,
71
63
  role: formatRole(widget.getAccessibleRole()),
72
64
  name: widget.getName() || null,
@@ -80,7 +72,7 @@ const serializeWidget = (widget) => {
80
72
  };
81
73
  const widgetRegistry = new Map();
82
74
  const registerWidgets = (widget) => {
83
- const idStr = String(getNativeId(widget.handle));
75
+ const idStr = getWidgetId(widget);
84
76
  widgetRegistry.set(idStr, widget);
85
77
  let child = widget.getFirstChild();
86
78
  while (child) {
@@ -282,7 +274,7 @@ class McpClient {
282
274
  }
283
275
  }
284
276
  async executeMethod(method, params) {
285
- const app = getApplication();
277
+ const app = Gio.Application.getDefault();
286
278
  if (!app) {
287
279
  throw new Error("Application not initialized");
288
280
  }
@@ -292,7 +284,7 @@ class McpClient {
292
284
  const windows = Gtk.Window.listToplevels();
293
285
  return {
294
286
  windows: windows.map((w) => ({
295
- id: String(getNativeId(w.handle)),
287
+ id: getWidgetId(w),
296
288
  title: w.getTitle?.() ?? null,
297
289
  })),
298
290
  };
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from "vite";
2
+ /**
3
+ * Vite plugin that resolves static asset imports to filesystem paths.
4
+ *
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
+ */
10
+ export declare function gtkxAssets(): Plugin;
@@ -0,0 +1,25 @@
1
+ const ASSET_RE = /\.(png|jpe?g|gif|svg|webp|webm|mp4|ogg|mp3|wav|flac|aac|woff2?|eot|ttf|otf|ico|avif)$/i;
2
+ /**
3
+ * Vite plugin that resolves static asset imports to filesystem paths.
4
+ *
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
+ */
10
+ export function gtkxAssets() {
11
+ let isBuild = false;
12
+ return {
13
+ name: "gtkx:assets",
14
+ enforce: "pre",
15
+ configResolved(config) {
16
+ isBuild = config.command === "build";
17
+ },
18
+ load(id) {
19
+ if (isBuild || !ASSET_RE.test(id)) {
20
+ return;
21
+ }
22
+ return `export default ${JSON.stringify(id)};`;
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,17 @@
1
+ import type { Plugin } from "vite";
2
+ /**
3
+ * Vite plugin that configures `renderBuiltUrl` for resolving asset imports
4
+ * to filesystem paths at runtime.
5
+ *
6
+ * When `assetBase` is provided, assets resolve relative to the executable
7
+ * directory using `path.join(path.dirname(process.execPath), assetBase, filename)`.
8
+ * This supports FHS-compliant layouts where assets live in `../share/<app>/`.
9
+ *
10
+ * When `assetBase` is omitted, assets resolve relative to the bundle file
11
+ * via `import.meta.url`, which works when assets are co-located with the
12
+ * executable.
13
+ *
14
+ * Only applies when the user has not already configured
15
+ * `experimental.renderBuiltUrl` in their Vite config.
16
+ */
17
+ export declare function gtkxBuiltUrl(assetBase?: string): Plugin;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Vite plugin that configures `renderBuiltUrl` for resolving asset imports
3
+ * to filesystem paths at runtime.
4
+ *
5
+ * When `assetBase` is provided, assets resolve relative to the executable
6
+ * directory using `path.join(path.dirname(process.execPath), assetBase, filename)`.
7
+ * This supports FHS-compliant layouts where assets live in `../share/<app>/`.
8
+ *
9
+ * When `assetBase` is omitted, assets resolve relative to the bundle file
10
+ * via `import.meta.url`, which works when assets are co-located with the
11
+ * executable.
12
+ *
13
+ * Only applies when the user has not already configured
14
+ * `experimental.renderBuiltUrl` in their Vite config.
15
+ */
16
+ export function gtkxBuiltUrl(assetBase) {
17
+ return {
18
+ name: "gtkx:built-url",
19
+ config(userConfig) {
20
+ if (userConfig.experimental?.renderBuiltUrl) {
21
+ return;
22
+ }
23
+ return {
24
+ experimental: {
25
+ renderBuiltUrl(filename, { type }) {
26
+ if (type !== "asset") {
27
+ return;
28
+ }
29
+ if (assetBase) {
30
+ return {
31
+ runtime: `require("path").join(require("path").dirname(process.execPath),${JSON.stringify(assetBase)},${JSON.stringify(filename)})`,
32
+ };
33
+ }
34
+ return {
35
+ runtime: `new URL(${JSON.stringify(`./${filename}`)}, import.meta.url).pathname`,
36
+ };
37
+ },
38
+ },
39
+ };
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,13 @@
1
+ import type { Plugin } from "vite";
2
+ /**
3
+ * Vite plugin that embeds the native `.node` binary into the build output.
4
+ *
5
+ * During production builds, resolves the platform-specific `.node` binary,
6
+ * copies it into the output directory as `gtkx.node`, and transforms the
7
+ * `loadNativeBinding` function in `@gtkx/native` to load `./gtkx.node`
8
+ * directly. This makes the bundle self-contained with no `node_modules`
9
+ * dependency at runtime.
10
+ *
11
+ * @param root - Project root directory used to resolve native packages
12
+ */
13
+ export declare function gtkxNative(root: string): Plugin;
@@ -0,0 +1,52 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { arch, platform } from "node:os";
4
+ import { join } from "node:path";
5
+ const LOAD_NATIVE_BINDING_RE = /function loadNativeBinding\(\) \{[\s\S]*?\n\}/;
6
+ const NODE_OS_IMPORT_RE = /import\s*\{[^}]*\}\s*from\s*["']node:os["'];?\n?/;
7
+ /**
8
+ * Vite plugin that embeds the native `.node` binary into the build output.
9
+ *
10
+ * During production builds, resolves the platform-specific `.node` binary,
11
+ * copies it into the output directory as `gtkx.node`, and transforms the
12
+ * `loadNativeBinding` function in `@gtkx/native` to load `./gtkx.node`
13
+ * directly. This makes the bundle self-contained with no `node_modules`
14
+ * dependency at runtime.
15
+ *
16
+ * @param root - Project root directory used to resolve native packages
17
+ */
18
+ export function gtkxNative(root) {
19
+ const projectRequire = createRequire(join(root, "package.json"));
20
+ let nativeIndexPath;
21
+ return {
22
+ name: "gtkx:native",
23
+ enforce: "pre",
24
+ buildStart() {
25
+ const currentPlatform = platform();
26
+ const currentArch = arch();
27
+ const packageName = `@gtkx/native-linux-${currentArch}`;
28
+ if (currentPlatform !== "linux") {
29
+ throw new Error(`Unsupported build platform: ${currentPlatform}. Only Linux is supported.`);
30
+ }
31
+ if (currentArch !== "x64" && currentArch !== "arm64") {
32
+ throw new Error(`Unsupported build architecture: ${currentArch}. Only x64 and arm64 are supported.`);
33
+ }
34
+ nativeIndexPath = projectRequire.resolve("@gtkx/native");
35
+ const nodePath = projectRequire.resolve(`${packageName}/index.node`);
36
+ const source = readFileSync(nodePath);
37
+ this.emitFile({
38
+ type: "asset",
39
+ fileName: "gtkx.node",
40
+ source,
41
+ });
42
+ },
43
+ transform(code, id) {
44
+ if (id !== nativeIndexPath) {
45
+ return;
46
+ }
47
+ return code
48
+ .replace(NODE_OS_IMPORT_RE, "")
49
+ .replace(LOAD_NATIVE_BINDING_RE, 'function loadNativeBinding() { return require("./gtkx.node"); }');
50
+ },
51
+ };
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/cli",
3
- "version": "0.17.3",
3
+ "version": "0.18.1",
4
4
  "description": "CLI for GTKX - create and develop GTK4 React applications",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -34,6 +34,10 @@
34
34
  "types": "./dist/index.d.ts",
35
35
  "default": "./dist/index.js"
36
36
  },
37
+ "./builder": {
38
+ "types": "./dist/builder.d.ts",
39
+ "default": "./dist/builder.js"
40
+ },
37
41
  "./dev-server": {
38
42
  "types": "./dist/dev-server.d.ts",
39
43
  "default": "./dist/dev-server.js"
@@ -52,25 +56,25 @@
52
56
  "templates"
53
57
  ],
54
58
  "dependencies": {
55
- "@clack/prompts": "^0.11.0",
56
- "@swc/core": "^1.15.10",
59
+ "@clack/prompts": "^1.0.0",
60
+ "@swc/core": "^1.15.11",
57
61
  "citty": "^0.2.0",
58
62
  "ejs": "^4.0.1",
59
63
  "react-refresh": "^0.18.0",
60
64
  "vite": "^7.3.1",
61
- "@gtkx/ffi": "0.17.3",
62
- "@gtkx/mcp": "0.17.3",
63
- "@gtkx/react": "0.17.3"
65
+ "@gtkx/ffi": "0.18.1",
66
+ "@gtkx/mcp": "0.18.1",
67
+ "@gtkx/react": "0.18.1"
64
68
  },
65
69
  "devDependencies": {
66
70
  "@types/ejs": "^3.1.5",
67
71
  "@types/react-refresh": "^0.14.7",
68
72
  "memfs": "^4.56.10",
69
- "@gtkx/testing": "0.17.3"
73
+ "@gtkx/testing": "0.18.1"
70
74
  },
71
75
  "peerDependencies": {
72
76
  "react": "^19",
73
- "@gtkx/testing": "0.17.3"
77
+ "@gtkx/testing": "0.18.1"
74
78
  },
75
79
  "peerDependenciesMeta": {
76
80
  "@gtkx/testing": {
@@ -34,13 +34,13 @@ import {
34
34
  export const App = () => (
35
35
  <AdwApplicationWindow title="My App" defaultWidth={800} defaultHeight={600} onClose={quit}>
36
36
  <AdwToolbarView>
37
- <x.ToolbarTop>
37
+ <x.ContainerSlot for={AdwToolbarView} id="addTopBar">
38
38
  <AdwHeaderBar>
39
39
  <x.Slot for={AdwHeaderBar} id="titleWidget">
40
40
  <AdwWindowTitle title="My App" subtitle="Welcome" />
41
41
  </x.Slot>
42
42
  </AdwHeaderBar>
43
- </x.ToolbarTop>
43
+ </x.ContainerSlot>
44
44
  <AdwStatusPage
45
45
  iconName="applications-system-symbolic"
46
46
  title="Welcome"
@@ -132,7 +132,7 @@ const TodoList = () => {
132
132
  <GtkButton label="Add" onClicked={addTodo} cssClasses={["suggested-action"]} />
133
133
  </GtkBox>
134
134
  <GtkScrolledWindow vexpand cssClasses={["card"]}>
135
- <x.ListView<Todo>
135
+ <GtkListView
136
136
  renderItem={(todo) => (
137
137
  <GtkBox spacing={8} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
138
138
  <GtkLabel label={todo?.text ?? ""} hexpand halign={Gtk.Align.START} />
@@ -143,7 +143,7 @@ const TodoList = () => {
143
143
  {todos.map((todo) => (
144
144
  <x.ListItem key={todo.id} id={todo.id} value={todo} />
145
145
  ))}
146
- </x.ListView>
146
+ </GtkListView>
147
147
  </GtkScrolledWindow>
148
148
  </GtkBox>
149
149
  );
@@ -179,7 +179,7 @@ const SidebarNav = () => {
179
179
  <GtkPaned position={200}>
180
180
  <x.Slot for={GtkPaned} id="startChild">
181
181
  <GtkScrolledWindow cssClasses={["sidebar"]}>
182
- <x.ListView<Page>
182
+ <GtkListView
183
183
  selected={[currentPage]}
184
184
  selectionMode={Gtk.SelectionMode.SINGLE}
185
185
  onSelectionChanged={(ids) => setCurrentPage(ids[0])}
@@ -190,7 +190,7 @@ const SidebarNav = () => {
190
190
  {pages.map((page) => (
191
191
  <x.ListItem key={page.id} id={page.id} value={page} />
192
192
  ))}
193
- </x.ListView>
193
+ </GtkListView>
194
194
  </GtkScrolledWindow>
195
195
  </x.Slot>
196
196
  <x.Slot for={GtkPaned} id="endChild">
@@ -219,9 +219,9 @@ const AppWithNavigation = () => {
219
219
  <GtkApplicationWindow title="App" defaultWidth={600} defaultHeight={400} onClose={quit}>
220
220
  <x.Slot for={GtkWindow} id="titlebar">
221
221
  <GtkHeaderBar>
222
- <x.PackStart>
222
+ <x.ContainerSlot for={GtkHeaderBar} id="packStart">
223
223
  {page !== "home" && <GtkButton iconName="go-previous-symbolic" onClicked={() => setPage("home")} />}
224
- </x.PackStart>
224
+ </x.ContainerSlot>
225
225
  <x.Slot for={GtkHeaderBar} id="titleWidget">
226
226
  <GtkLabel label={page === "home" ? "Home" : "Details"} cssClasses={["title"]} />
227
227
  </x.Slot>
@@ -441,7 +441,7 @@ const AsyncList = () => {
441
441
 
442
442
  return (
443
443
  <GtkScrolledWindow vexpand>
444
- <x.ListView<User>
444
+ <GtkListView
445
445
  renderItem={(user) => (
446
446
  <GtkBox orientation={Gtk.Orientation.VERTICAL} marginStart={12} marginTop={8} marginBottom={8}>
447
447
  <GtkLabel label={user?.name ?? ""} halign={Gtk.Align.START} cssClasses={["heading"]} />
@@ -452,7 +452,7 @@ const AsyncList = () => {
452
452
  {users.map((user) => (
453
453
  <x.ListItem key={user.id} id={user.id} value={user} />
454
454
  ))}
455
- </x.ListView>
455
+ </GtkListView>
456
456
  </GtkScrolledWindow>
457
457
  );
458
458
  };
@@ -528,7 +528,7 @@ const NavigationDemo = () => {
528
528
  <AdwNavigationView history={history} onHistoryChanged={setHistory}>
529
529
  <x.NavigationPage for={AdwNavigationView} id="home" title="Home">
530
530
  <AdwToolbarView>
531
- <x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
531
+ <x.ContainerSlot for={AdwToolbarView} id="addTopBar"><AdwHeaderBar /></x.ContainerSlot>
532
532
  <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER}>
533
533
  <GtkLabel label="Welcome!" cssClasses={["title-1"]} />
534
534
  <GtkButton label="Go to Settings" onClicked={() => push("settings")} cssClasses={["suggested-action"]} />
@@ -537,7 +537,7 @@ const NavigationDemo = () => {
537
537
  </x.NavigationPage>
538
538
  <x.NavigationPage for={AdwNavigationView} id="settings" title="Settings" canPop>
539
539
  <AdwToolbarView>
540
- <x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
540
+ <x.ContainerSlot for={AdwToolbarView} id="addTopBar"><AdwHeaderBar /></x.ContainerSlot>
541
541
  <GtkLabel label="Settings page content" vexpand />
542
542
  </AdwToolbarView>
543
543
  </x.NavigationPage>
@@ -589,7 +589,7 @@ const SplitViewDemo = () => {
589
589
  <AdwNavigationSplitView sidebarWidthFraction={0.33} minSidebarWidth={200} maxSidebarWidth={300}>
590
590
  <x.NavigationPage for={AdwNavigationSplitView} id="sidebar" title="Mail">
591
591
  <AdwToolbarView>
592
- <x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
592
+ <x.ContainerSlot for={AdwToolbarView} id="addTopBar"><AdwHeaderBar /></x.ContainerSlot>
593
593
  <GtkScrolledWindow vexpand>
594
594
  <GtkListBox
595
595
  cssClasses={["navigation-sidebar"]}
@@ -601,9 +601,9 @@ const SplitViewDemo = () => {
601
601
  >
602
602
  {items.map((item) => (
603
603
  <AdwActionRow key={item.id} title={item.title}>
604
- <x.ActionRowPrefix>
604
+ <x.ContainerSlot for={AdwActionRow} id="addPrefix">
605
605
  <GtkImage iconName={item.icon} />
606
- </x.ActionRowPrefix>
606
+ </x.ContainerSlot>
607
607
  </AdwActionRow>
608
608
  ))}
609
609
  </GtkListBox>
@@ -613,7 +613,7 @@ const SplitViewDemo = () => {
613
613
 
614
614
  <x.NavigationPage for={AdwNavigationSplitView} id="content" title={selected?.title ?? ""}>
615
615
  <AdwToolbarView>
616
- <x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
616
+ <x.ContainerSlot for={AdwToolbarView} id="addTopBar"><AdwHeaderBar /></x.ContainerSlot>
617
617
  <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} vexpand>
618
618
  <GtkImage iconName={selected?.icon ?? ""} iconSize={Gtk.IconSize.LARGE} />
619
619
  <GtkLabel label={selected?.title ?? ""} cssClasses={["title-2"]} />
@@ -628,11 +628,11 @@ const SplitViewDemo = () => {
628
628
 
629
629
  ---
630
630
 
631
- ## File Browser with TreeListView
631
+ ## File Browser with GtkListView (tree)
632
632
 
633
633
  ```tsx
634
634
  import * as Gtk from "@gtkx/ffi/gtk";
635
- import { GtkBox, GtkImage, GtkLabel, GtkScrolledWindow, x } from "@gtkx/react";
635
+ import { GtkBox, GtkImage, GtkLabel, GtkListView, GtkScrolledWindow, x } from "@gtkx/react";
636
636
  import { useState } from "react";
637
637
 
638
638
  interface FileNode {
@@ -660,7 +660,7 @@ const FileBrowser = () => {
660
660
 
661
661
  return (
662
662
  <GtkScrolledWindow vexpand cssClasses={["card"]}>
663
- <x.TreeListView<FileNode>
663
+ <GtkListView
664
664
  estimatedItemHeight={36}
665
665
  vexpand
666
666
  autoexpand={false}
@@ -675,13 +675,13 @@ const FileBrowser = () => {
675
675
  )}
676
676
  >
677
677
  {files.map((file) => (
678
- <x.TreeListItem key={file.id} id={file.id} value={file}>
678
+ <x.ListItem key={file.id} id={file.id} value={file}>
679
679
  {file.children?.map((child) => (
680
- <x.TreeListItem key={child.id} id={child.id} value={child} />
680
+ <x.ListItem key={child.id} id={child.id} value={child} />
681
681
  ))}
682
- </x.TreeListItem>
682
+ </x.ListItem>
683
683
  ))}
684
- </x.TreeListView>
684
+ </GtkListView>
685
685
  </GtkScrolledWindow>
686
686
  );
687
687
  };
@@ -735,11 +735,10 @@ const AnimatedCard = () => {
735
735
  <GtkButton label={visible ? "Hide Card" : "Show Card"} onClicked={() => setVisible(!visible)} halign={Gtk.Align.START} />
736
736
  {visible && (
737
737
  <x.Animation
738
- mode="spring"
739
738
  initial={{ opacity: 0, scale: 0.8, translateY: -20 }}
740
739
  animate={{ opacity: 1, scale: 1, translateY: 0 }}
741
740
  exit={{ opacity: 0, scale: 0.8, translateY: 20 }}
742
- transition={{ damping: 0.7, stiffness: 200 }}
741
+ transition={{ mode: "spring", damping: 0.7, stiffness: 200 }}
743
742
  animateOnMount
744
743
  >
745
744
  <GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={8} cssClasses={["card"]} marginStart={12} marginEnd={12} marginTop={8} marginBottom={8}>
@@ -58,12 +58,12 @@ Some widgets require children in specific slots:
58
58
  </GtkPaned>
59
59
  ```
60
60
 
61
- ### Packing (HeaderBar, ActionBar)
61
+ ### Container Slots (HeaderBar, ActionBar, ToolbarView, ActionRow, ExpanderRow)
62
62
 
63
63
  ```tsx
64
64
  <GtkHeaderBar>
65
- <x.PackStart><GtkButton iconName="go-previous-symbolic" /></x.PackStart>
66
- <x.PackEnd><GtkMenuButton iconName="open-menu-symbolic" /></x.PackEnd>
65
+ <x.ContainerSlot for={GtkHeaderBar} id="packStart"><GtkButton iconName="go-previous-symbolic" /></x.ContainerSlot>
66
+ <x.ContainerSlot for={GtkHeaderBar} id="packEnd"><GtkMenuButton iconName="open-menu-symbolic" /></x.ContainerSlot>
67
67
  </GtkHeaderBar>
68
68
  ```
69
69
 
@@ -71,10 +71,9 @@ Some widgets require children in specific slots:
71
71
 
72
72
  ```tsx
73
73
  <x.Animation
74
- mode="spring"
75
74
  initial={{ opacity: 0, scale: 0.8 }}
76
75
  animate={{ opacity: 1, scale: 1 }}
77
- transition={{ damping: 0.8, stiffness: 200 }}
76
+ transition={{ mode: "spring", damping: 0.8, stiffness: 200 }}
78
77
  animateOnMount
79
78
  >
80
79
  <GtkLabel label="Animated!" />
@@ -132,11 +132,11 @@ Scrollable container.
132
132
 
133
133
  All virtual list widgets use `ListItem` children and a `renderItem` function.
134
134
 
135
- ### ListView
135
+ ### GtkListView
136
136
  High-performance scrollable list with selection.
137
137
 
138
138
  ```tsx
139
- <x.ListView<Item>
139
+ <GtkListView
140
140
  estimatedItemHeight={48}
141
141
  vexpand
142
142
  selected={selectedId ? [selectedId] : []}
@@ -145,14 +145,14 @@ High-performance scrollable list with selection.
145
145
  renderItem={(item) => <GtkLabel label={item?.name ?? ""} />}
146
146
  >
147
147
  {items.map(item => <x.ListItem key={item.id} id={item.id} value={item} />)}
148
- </x.ListView>
148
+ </GtkListView>
149
149
  ```
150
150
 
151
- ### GridView
151
+ ### GtkGridView
152
152
  Grid-based virtual scrolling.
153
153
 
154
154
  ```tsx
155
- <x.GridView<Item>
155
+ <GtkGridView
156
156
  estimatedItemHeight={100}
157
157
  minColumns={2}
158
158
  maxColumns={4}
@@ -164,7 +164,7 @@ Grid-based virtual scrolling.
164
164
  )}
165
165
  >
166
166
  {items.map(item => <x.ListItem key={item.id} id={item.id} value={item} />)}
167
- </x.GridView>
167
+ </GtkGridView>
168
168
  ```
169
169
 
170
170
  ### GtkColumnView
@@ -195,15 +195,15 @@ Selection dropdown.
195
195
 
196
196
  ```tsx
197
197
  <GtkDropDown selectedId={selectedId} onSelectionChanged={setSelectedId}>
198
- {options.map(opt => <x.SimpleListItem key={opt.id} id={opt.id} value={opt.label} />)}
198
+ {options.map(opt => <x.ListItem key={opt.id} id={opt.id} value={opt.label} />)}
199
199
  </GtkDropDown>
200
200
  ```
201
201
 
202
- ### TreeListView
203
- Hierarchical tree with expand/collapse.
202
+ ### GtkListView (tree mode)
203
+ Hierarchical tree with expand/collapse. Nesting `x.ListItem` children triggers tree behavior.
204
204
 
205
205
  ```tsx
206
- <x.TreeListView<FileNode>
206
+ <GtkListView
207
207
  estimatedItemHeight={48}
208
208
  vexpand
209
209
  autoexpand={false}
@@ -218,16 +218,16 @@ Hierarchical tree with expand/collapse.
218
218
  )}
219
219
  >
220
220
  {files.map(file => (
221
- <x.TreeListItem key={file.id} id={file.id} value={file}>
221
+ <x.ListItem key={file.id} id={file.id} value={file}>
222
222
  {file.children?.map(child => (
223
- <x.TreeListItem key={child.id} id={child.id} value={child} />
223
+ <x.ListItem key={child.id} id={child.id} value={child} />
224
224
  ))}
225
- </x.TreeListItem>
225
+ </x.ListItem>
226
226
  ))}
227
- </x.TreeListView>
227
+ </GtkListView>
228
228
  ```
229
229
 
230
- **TreeListItem props:** `id`, `value`, `indentForDepth`, `indentForIcon`, `hideExpander`, nested `children`
230
+ **ListItem tree props:** `id`, `value`, `indentForDepth`, `indentForIcon`, `hideExpander`, nested `children`
231
231
 
232
232
  ---
233
233
 
@@ -348,15 +348,15 @@ Progress/level indicator with customizable thresholds.
348
348
  ## Header & Action Bars
349
349
 
350
350
  ### GtkHeaderBar
351
- Title bar with packed widgets. Use `Pack.Start`, `Pack.End`, and `Slot` for titleWidget.
351
+ Title bar with packed widgets. Use `x.ContainerSlot` for packing and `x.Slot` for titleWidget.
352
352
 
353
353
  ```tsx
354
354
  <GtkHeaderBar>
355
- <x.PackStart><GtkButton iconName="go-previous-symbolic" /></x.PackStart>
355
+ <x.ContainerSlot for={GtkHeaderBar} id="packStart"><GtkButton iconName="go-previous-symbolic" /></x.ContainerSlot>
356
356
  <x.Slot for={GtkHeaderBar} id="titleWidget">
357
357
  <GtkLabel label="Title" cssClasses={["title"]} />
358
358
  </x.Slot>
359
- <x.PackEnd><GtkMenuButton iconName="open-menu-symbolic" /></x.PackEnd>
359
+ <x.ContainerSlot for={GtkHeaderBar} id="packEnd"><GtkMenuButton iconName="open-menu-symbolic" /></x.ContainerSlot>
360
360
  </GtkHeaderBar>
361
361
  ```
362
362
 
@@ -365,8 +365,8 @@ Bottom action bar.
365
365
 
366
366
  ```tsx
367
367
  <GtkActionBar>
368
- <x.PackStart><GtkButton label="Cancel" /></x.PackStart>
369
- <x.PackEnd><GtkButton label="Save" cssClasses={["suggested-action"]} /></x.PackEnd>
368
+ <x.ContainerSlot for={GtkActionBar} id="packStart"><GtkButton label="Cancel" /></x.ContainerSlot>
369
+ <x.ContainerSlot for={GtkActionBar} id="packEnd"><GtkButton label="Save" cssClasses={["suggested-action"]} /></x.ContainerSlot>
370
370
  </GtkActionBar>
371
371
  ```
372
372
 
@@ -439,17 +439,17 @@ Modern app structure.
439
439
  ```tsx
440
440
  <AdwApplicationWindow title="App" defaultWidth={800} defaultHeight={600} onClose={quit}>
441
441
  <AdwToolbarView>
442
- <x.ToolbarTop>
442
+ <x.ContainerSlot for={AdwToolbarView} id="addTopBar">
443
443
  <AdwHeaderBar>
444
444
  <x.Slot for={AdwHeaderBar} id="titleWidget">
445
445
  <AdwWindowTitle title="App" subtitle="Description" />
446
446
  </x.Slot>
447
447
  </AdwHeaderBar>
448
- </x.ToolbarTop>
448
+ </x.ContainerSlot>
449
449
  <MainContent />
450
- <x.ToolbarBottom>
450
+ <x.ContainerSlot for={AdwToolbarView} id="addBottomBar">
451
451
  <GtkActionBar />
452
- </x.ToolbarBottom>
452
+ </x.ContainerSlot>
453
453
  </AdwToolbarView>
454
454
  </AdwApplicationWindow>
455
455
  ```
@@ -478,35 +478,35 @@ Settings UI.
478
478
  <AdwPreferencesGroup title="Appearance" description="Customize look">
479
479
  <AdwSwitchRow title="Dark Mode" active={dark} onActivated={() => setDark(!dark)} />
480
480
  <AdwActionRow title="Theme" subtitle="Select color">
481
- <x.ActionRowPrefix>
481
+ <x.ContainerSlot for={AdwActionRow} id="addPrefix">
482
482
  <GtkImage iconName="preferences-color-symbolic" />
483
- </x.ActionRowPrefix>
484
- <x.ActionRowSuffix>
483
+ </x.ContainerSlot>
484
+ <x.ContainerSlot for={AdwActionRow} id="addSuffix">
485
485
  <GtkImage iconName="go-next-symbolic" valign={Gtk.Align.CENTER} />
486
- </x.ActionRowSuffix>
486
+ </x.ContainerSlot>
487
487
  </AdwActionRow>
488
488
  </AdwPreferencesGroup>
489
489
  </AdwPreferencesPage>
490
490
  ```
491
491
 
492
- **ActionRow children:** Use `x.ActionRowPrefix` for left widgets, `x.ActionRowSuffix` for right widgets, or `x.Slot for={AdwActionRow} id="activatableWidget"` for clickable suffix.
492
+ **ActionRow children:** Use `x.ContainerSlot for={AdwActionRow} id="addPrefix"` for left widgets, `x.ContainerSlot for={AdwActionRow} id="addSuffix"` for right widgets, or `x.Slot for={AdwActionRow} id="activatableWidget"` for clickable suffix.
493
493
 
494
494
  ### AdwExpanderRow
495
495
  Expandable settings row with optional action widget.
496
496
 
497
497
  ```tsx
498
498
  <AdwExpanderRow title="Advanced" subtitle="More options">
499
- <x.ExpanderRowAction>
499
+ <x.ContainerSlot for={AdwExpanderRow} id="addAction">
500
500
  <GtkButton iconName="emblem-system-symbolic" cssClasses={["flat"]} />
501
- </x.ExpanderRowAction>
502
- <x.ExpanderRowRow>
501
+ </x.ContainerSlot>
502
+ <x.ContainerSlot for={AdwExpanderRow} id="addRow">
503
503
  <AdwSwitchRow title="Option 1" active />
504
504
  <AdwSwitchRow title="Option 2" />
505
- </x.ExpanderRowRow>
505
+ </x.ContainerSlot>
506
506
  </AdwExpanderRow>
507
507
  ```
508
508
 
509
- **ExpanderRow slots:** `x.ExpanderRowRow` for nested rows, `x.ExpanderRowAction` for header action widget. Direct children also work for simple cases.
509
+ **ExpanderRow slots:** `x.ContainerSlot for={AdwExpanderRow} id="addRow"` for nested rows, `x.ContainerSlot for={AdwExpanderRow} id="addAction"` for header action widget. Direct children also work for simple cases.
510
510
 
511
511
  ### AdwEntryRow / AdwPasswordEntryRow
512
512
  Input in list row.
@@ -560,7 +560,7 @@ const [selected, setSelected] = useState(items[0]);
560
560
  <AdwNavigationSplitView sidebarWidthFraction={0.33} minSidebarWidth={200} maxSidebarWidth={300}>
561
561
  <x.NavigationPage for={AdwNavigationSplitView} id="sidebar" title="Sidebar">
562
562
  <AdwToolbarView>
563
- <x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
563
+ <x.ContainerSlot for={AdwToolbarView} id="addTopBar"><AdwHeaderBar /></x.ContainerSlot>
564
564
  <GtkListBox cssClasses={["navigation-sidebar"]} onRowSelected={(row) => {
565
565
  if (!row) return;
566
566
  const item = items[row.getIndex()];
@@ -573,7 +573,7 @@ const [selected, setSelected] = useState(items[0]);
573
573
 
574
574
  <x.NavigationPage for={AdwNavigationSplitView} id="content" title={selected?.title ?? ""}>
575
575
  <AdwToolbarView>
576
- <x.ToolbarTop><AdwHeaderBar /></x.ToolbarTop>
576
+ <x.ContainerSlot for={AdwToolbarView} id="addTopBar"><AdwHeaderBar /></x.ContainerSlot>
577
577
  <GtkLabel label={selected?.title ?? ""} />
578
578
  </AdwToolbarView>
579
579
  </x.NavigationPage>
@@ -633,10 +633,9 @@ Wrap widgets in `x.Animation` for declarative animations with spring or timed tr
633
633
 
634
634
  ```tsx
635
635
  <x.Animation
636
- mode="spring"
637
636
  initial={{ opacity: 0, scale: 0.8 }}
638
637
  animate={{ opacity: 1, scale: 1 }}
639
- transition={{ damping: 0.8, stiffness: 200, mass: 1 }}
638
+ transition={{ mode: "spring", damping: 0.8, stiffness: 200, mass: 1 }}
640
639
  animateOnMount
641
640
  onAnimationComplete={() => console.log("done")}
642
641
  >
@@ -644,11 +643,11 @@ Wrap widgets in `x.Animation` for declarative animations with spring or timed tr
644
643
  </x.Animation>
645
644
  ```
646
645
 
647
- **Props:** `mode` (`"spring"` | `"timed"`), `initial`, `animate`, `exit`, `transition`, `animateOnMount`, `onAnimationStart`, `onAnimationComplete`
646
+ **Props:** `initial`, `animate`, `exit`, `transition` (`AnimationTransition` discriminated union), `animateOnMount`, `onAnimationStart`, `onAnimationComplete`
648
647
 
649
- **Spring transition:** `damping`, `stiffness`, `mass`, `initialVelocity`, `clamp`, `delay`
648
+ **Spring transition:** `{ mode: "spring", damping, stiffness, mass, initialVelocity, clamp, delay }`
650
649
 
651
- **Timed transition:** `duration`, `easing` (from `Adw.Easing`), `delay`, `repeat`, `reverse`, `alternate`
650
+ **Timed transition:** `{ mode: "timed", duration, easing (from Adw.Easing), delay, repeat, reverse, alternate }`
652
651
 
653
652
  ---
654
653
 
@@ -5,8 +5,8 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "gtkx dev src/dev.tsx",
8
- "build": "tsc -b",
9
- "start": "node dist/index.js"<% if (testing === 'vitest') { %>,
8
+ "build": "gtkx build",
9
+ "start": "node dist/bundle.js"<% if (testing === 'vitest') { %>,
10
10
  "test": "vitest"<% } %>
11
11
  },
12
12
  "gtkx": {
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />