@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.
- package/dist/builder.d.ts +55 -0
- package/dist/builder.js +60 -0
- package/dist/cli.js +31 -0
- package/dist/create.js +2 -1
- package/dist/dev-server.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp-client.js +25 -33
- package/dist/vite-plugin-gtkx-assets.d.ts +10 -0
- package/dist/vite-plugin-gtkx-assets.js +25 -0
- package/dist/vite-plugin-gtkx-built-url.d.ts +17 -0
- package/dist/vite-plugin-gtkx-built-url.js +42 -0
- package/dist/vite-plugin-gtkx-native.d.ts +13 -0
- package/dist/vite-plugin-gtkx-native.js +52 -0
- package/package.json +12 -8
- package/templates/claude/EXAMPLES.md.ejs +24 -25
- package/templates/claude/SKILL.md.ejs +4 -5
- package/templates/claude/WIDGETS.md.ejs +40 -41
- package/templates/package.json.ejs +2 -2
- package/templates/src/vite-env.d.ts.ejs +1 -0
|
@@ -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>;
|
package/dist/builder.js
ADDED
|
@@ -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");
|
package/dist/dev-server.js
CHANGED
|
@@ -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
package/dist/index.js
CHANGED
package/dist/mcp-client.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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.
|
|
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.
|
|
56
|
-
"@swc/core": "^1.15.
|
|
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.
|
|
62
|
-
"@gtkx/mcp": "0.
|
|
63
|
-
"@gtkx/react": "0.
|
|
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.
|
|
73
|
+
"@gtkx/testing": "0.18.1"
|
|
70
74
|
},
|
|
71
75
|
"peerDependencies": {
|
|
72
76
|
"react": "^19",
|
|
73
|
-
"@gtkx/testing": "0.
|
|
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.
|
|
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.
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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.
|
|
222
|
+
<x.ContainerSlot for={GtkHeaderBar} id="packStart">
|
|
223
223
|
{page !== "home" && <GtkButton iconName="go-previous-symbolic" onClicked={() => setPage("home")} />}
|
|
224
|
-
</x.
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
604
|
+
<x.ContainerSlot for={AdwActionRow} id="addPrefix">
|
|
605
605
|
<GtkImage iconName={item.icon} />
|
|
606
|
-
</x.
|
|
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.
|
|
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
|
|
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
|
-
<
|
|
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.
|
|
678
|
+
<x.ListItem key={file.id} id={file.id} value={file}>
|
|
679
679
|
{file.children?.map((child) => (
|
|
680
|
-
<x.
|
|
680
|
+
<x.ListItem key={child.id} id={child.id} value={child} />
|
|
681
681
|
))}
|
|
682
|
-
</x.
|
|
682
|
+
</x.ListItem>
|
|
683
683
|
))}
|
|
684
|
-
</
|
|
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
|
-
###
|
|
61
|
+
### Container Slots (HeaderBar, ActionBar, ToolbarView, ActionRow, ExpanderRow)
|
|
62
62
|
|
|
63
63
|
```tsx
|
|
64
64
|
<GtkHeaderBar>
|
|
65
|
-
<x.
|
|
66
|
-
<x.
|
|
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
|
-
###
|
|
135
|
+
### GtkListView
|
|
136
136
|
High-performance scrollable list with selection.
|
|
137
137
|
|
|
138
138
|
```tsx
|
|
139
|
-
<
|
|
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
|
-
</
|
|
148
|
+
</GtkListView>
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
###
|
|
151
|
+
### GtkGridView
|
|
152
152
|
Grid-based virtual scrolling.
|
|
153
153
|
|
|
154
154
|
```tsx
|
|
155
|
-
<
|
|
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
|
-
</
|
|
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.
|
|
198
|
+
{options.map(opt => <x.ListItem key={opt.id} id={opt.id} value={opt.label} />)}
|
|
199
199
|
</GtkDropDown>
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
###
|
|
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
|
-
<
|
|
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.
|
|
221
|
+
<x.ListItem key={file.id} id={file.id} value={file}>
|
|
222
222
|
{file.children?.map(child => (
|
|
223
|
-
<x.
|
|
223
|
+
<x.ListItem key={child.id} id={child.id} value={child} />
|
|
224
224
|
))}
|
|
225
|
-
</x.
|
|
225
|
+
</x.ListItem>
|
|
226
226
|
))}
|
|
227
|
-
</
|
|
227
|
+
</GtkListView>
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
-
**
|
|
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 `
|
|
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.
|
|
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.
|
|
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.
|
|
369
|
-
<x.
|
|
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.
|
|
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.
|
|
448
|
+
</x.ContainerSlot>
|
|
449
449
|
<MainContent />
|
|
450
|
-
<x.
|
|
450
|
+
<x.ContainerSlot for={AdwToolbarView} id="addBottomBar">
|
|
451
451
|
<GtkActionBar />
|
|
452
|
-
</x.
|
|
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.
|
|
481
|
+
<x.ContainerSlot for={AdwActionRow} id="addPrefix">
|
|
482
482
|
<GtkImage iconName="preferences-color-symbolic" />
|
|
483
|
-
</x.
|
|
484
|
-
<x.
|
|
483
|
+
</x.ContainerSlot>
|
|
484
|
+
<x.ContainerSlot for={AdwActionRow} id="addSuffix">
|
|
485
485
|
<GtkImage iconName="go-next-symbolic" valign={Gtk.Align.CENTER} />
|
|
486
|
-
</x.
|
|
486
|
+
</x.ContainerSlot>
|
|
487
487
|
</AdwActionRow>
|
|
488
488
|
</AdwPreferencesGroup>
|
|
489
489
|
</AdwPreferencesPage>
|
|
490
490
|
```
|
|
491
491
|
|
|
492
|
-
**ActionRow children:** Use `x.
|
|
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.
|
|
499
|
+
<x.ContainerSlot for={AdwExpanderRow} id="addAction">
|
|
500
500
|
<GtkButton iconName="emblem-system-symbolic" cssClasses={["flat"]} />
|
|
501
|
-
</x.
|
|
502
|
-
<x.
|
|
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.
|
|
505
|
+
</x.ContainerSlot>
|
|
506
506
|
</AdwExpanderRow>
|
|
507
507
|
```
|
|
508
508
|
|
|
509
|
-
**ExpanderRow slots:** `x.
|
|
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.
|
|
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.
|
|
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:** `
|
|
646
|
+
**Props:** `initial`, `animate`, `exit`, `transition` (`AnimationTransition` discriminated union), `animateOnMount`, `onAnimationStart`, `onAnimationComplete`
|
|
648
647
|
|
|
649
|
-
**Spring transition:** `damping
|
|
648
|
+
**Spring transition:** `{ mode: "spring", damping, stiffness, mass, initialVelocity, clamp, delay }`
|
|
650
649
|
|
|
651
|
-
**Timed transition:** `duration
|
|
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": "
|
|
9
|
-
"start": "node dist/
|
|
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" />
|