@dex-ai/vue-tui 0.1.10
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/package.json +51 -0
- package/src/app.ts +385 -0
- package/src/components/TuiCheckbox.ts +35 -0
- package/src/components/TuiField.ts +123 -0
- package/src/components/TuiSelect.ts +86 -0
- package/src/components/index.ts +7 -0
- package/src/composables/index.ts +19 -0
- package/src/composables/useFocusList.ts +101 -0
- package/src/composables/useForm.ts +335 -0
- package/src/composables/useSelectList.ts +199 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +131 -0
- package/src/input.ts +603 -0
- package/src/nodes.ts +153 -0
- package/src/panel.ts +51 -0
- package/src/plugin.ts +148 -0
- package/src/register.ts +4 -0
- package/src/render.ts +632 -0
- package/src/renderer.ts +120 -0
- package/src/style.ts +107 -0
- package/src/template.ts +326 -0
- package/src/text-buffer.ts +609 -0
- package/src/theme.ts +44 -0
- package/src/types/form.ts +90 -0
- package/src/widget-renderer.ts +237 -0
- package/src/widget.ts +326 -0
package/src/nodes.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { TerminalStyle } from "./style";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base node — satisfies Vue's RendererNode by allowing any string/symbol key.
|
|
5
|
+
* The index signature is required for Vue's internal bookkeeping.
|
|
6
|
+
*/
|
|
7
|
+
export interface TNode {
|
|
8
|
+
type: "element" | "text" | "comment";
|
|
9
|
+
parent: TElement | null;
|
|
10
|
+
nextSibling: TNode | null;
|
|
11
|
+
[key: string | symbol]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TElement extends TNode {
|
|
15
|
+
type: "element";
|
|
16
|
+
tag: string;
|
|
17
|
+
props: Record<string, unknown>;
|
|
18
|
+
children: TNode[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TText extends TNode {
|
|
22
|
+
type: "text";
|
|
23
|
+
text: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TComment extends TNode {
|
|
27
|
+
type: "comment";
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Factory functions
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export function createElement(tag: string, props?: Record<string, unknown>): TElement {
|
|
36
|
+
return {
|
|
37
|
+
type: "element",
|
|
38
|
+
tag,
|
|
39
|
+
props: props ?? {},
|
|
40
|
+
children: [],
|
|
41
|
+
parent: null,
|
|
42
|
+
nextSibling: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createTextNode(text: string): TText {
|
|
47
|
+
return {
|
|
48
|
+
type: "text",
|
|
49
|
+
text,
|
|
50
|
+
parent: null,
|
|
51
|
+
nextSibling: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createComment(text: string): TComment {
|
|
56
|
+
return {
|
|
57
|
+
type: "comment",
|
|
58
|
+
text,
|
|
59
|
+
parent: null,
|
|
60
|
+
nextSibling: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Tree mutations — maintain nextSibling links automatically
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/** Recalculate the nextSibling links for all children of `parent`. */
|
|
69
|
+
function relinkSiblings(parent: TElement): void {
|
|
70
|
+
const { children } = parent;
|
|
71
|
+
for (let i = 0; i < children.length; i++) {
|
|
72
|
+
const child = children[i];
|
|
73
|
+
const next = children[i + 1];
|
|
74
|
+
if (child !== undefined) {
|
|
75
|
+
child.nextSibling = next ?? null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Insert `child` before `anchor` inside `parent`.
|
|
82
|
+
* If `anchor` is null, appends to the end.
|
|
83
|
+
*/
|
|
84
|
+
export function insertChild(parent: TElement, child: TNode, anchor: TNode | null): void {
|
|
85
|
+
// Remove from previous parent if any
|
|
86
|
+
if (child.parent !== null) {
|
|
87
|
+
removeChild(child.parent, child);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
child.parent = parent;
|
|
91
|
+
|
|
92
|
+
if (anchor === null) {
|
|
93
|
+
parent.children.push(child);
|
|
94
|
+
} else {
|
|
95
|
+
const idx = parent.children.indexOf(anchor);
|
|
96
|
+
if (idx === -1) {
|
|
97
|
+
// Anchor not found — append
|
|
98
|
+
parent.children.push(child);
|
|
99
|
+
} else {
|
|
100
|
+
parent.children.splice(idx, 0, child);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
relinkSiblings(parent);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Remove `child` from `parent`, repairing sibling links. */
|
|
108
|
+
export function removeChild(parent: TElement, child: TNode): void {
|
|
109
|
+
const idx = parent.children.indexOf(child);
|
|
110
|
+
if (idx !== -1) {
|
|
111
|
+
parent.children.splice(idx, 1);
|
|
112
|
+
child.parent = null;
|
|
113
|
+
child.nextSibling = null;
|
|
114
|
+
relinkSiblings(parent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Style extraction helper (used by renderer + render)
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/** Extract the `style` prop from a TElement as a TerminalStyle, merging shorthand props. */
|
|
123
|
+
export function getStyle(el: TElement): TerminalStyle {
|
|
124
|
+
const s = el.props["style"];
|
|
125
|
+
let style: TerminalStyle = {};
|
|
126
|
+
|
|
127
|
+
if (s !== null && typeof s === "object" && !Array.isArray(s)) {
|
|
128
|
+
style = { ...(s as TerminalStyle) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Merge shorthand attribute props into style
|
|
132
|
+
if (el.props["bold"] === "" || el.props["bold"] === true) {
|
|
133
|
+
style.fontWeight = "bold";
|
|
134
|
+
}
|
|
135
|
+
if (el.props["dim"] === "" || el.props["dim"] === true) {
|
|
136
|
+
style.opacity = 0.5;
|
|
137
|
+
}
|
|
138
|
+
if (el.props["italic"] === "" || el.props["italic"] === true) {
|
|
139
|
+
style.fontStyle = "italic";
|
|
140
|
+
}
|
|
141
|
+
if (el.props["underline"] === "" || el.props["underline"] === true) {
|
|
142
|
+
style.textDecoration = "underline";
|
|
143
|
+
}
|
|
144
|
+
if (typeof el.props["color"] === "string" && el.props["color"] !== "") {
|
|
145
|
+
style.color = el.props["color"] as string;
|
|
146
|
+
}
|
|
147
|
+
if (typeof el.props["bg"] === "string" && el.props["bg"] !== "") {
|
|
148
|
+
const bg = el.props["bg"] as string;
|
|
149
|
+
style.backgroundColor = bg.startsWith("bg-") ? bg : `bg-${bg}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return style;
|
|
153
|
+
}
|
package/src/panel.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel types — UI extensibility contracts for terminal panels.
|
|
3
|
+
*
|
|
4
|
+
* These types allow extensions to contribute panel content without
|
|
5
|
+
* depending on the CLI host package.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WidgetNode } from "./widget";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Context passed to panel render() methods.
|
|
12
|
+
*/
|
|
13
|
+
export interface PanelContext {
|
|
14
|
+
/** Terminal width (columns). Useful for sizing progress bars, tables. */
|
|
15
|
+
readonly width: number;
|
|
16
|
+
/** Terminal height (rows). Useful for limiting content. */
|
|
17
|
+
readonly height: number;
|
|
18
|
+
/** Shared agent state map. Read-only access to state from other extensions. */
|
|
19
|
+
readonly state: ReadonlyMap<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A DynamicPanel contributes content to the activity zone.
|
|
24
|
+
*
|
|
25
|
+
* Unlike interactive panels (modal, capture keys), a DynamicPanel is:
|
|
26
|
+
* - Persistent (shown alongside normal flow)
|
|
27
|
+
* - Non-interactive (doesn't capture input)
|
|
28
|
+
* - Priority-based (highest priority visible panel wins)
|
|
29
|
+
*/
|
|
30
|
+
export interface DynamicPanel {
|
|
31
|
+
/** Stable identifier. */
|
|
32
|
+
readonly id: string;
|
|
33
|
+
|
|
34
|
+
/** Human-readable label. */
|
|
35
|
+
readonly label?: string;
|
|
36
|
+
|
|
37
|
+
/** Priority: higher number wins over lower. Default 0. */
|
|
38
|
+
readonly priority?: number;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Should this panel be visible right now?
|
|
42
|
+
* Called on each render tick.
|
|
43
|
+
*/
|
|
44
|
+
visible(ctx: PanelContext): boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Render panel content as a WidgetNode tree.
|
|
48
|
+
* Called only when visible() returns true.
|
|
49
|
+
*/
|
|
50
|
+
render(ctx: PanelContext): WidgetNode | null;
|
|
51
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun plugin for compiling Vue Single File Components (.vue) for @dex-ai/vue-tui.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { tuiVuePlugin } from "@dex-ai/vue-tui/plugin";
|
|
7
|
+
* Bun.plugin(tuiVuePlugin);
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { BunPlugin } from "bun";
|
|
12
|
+
import { parse, compileScript, compileTemplate } from "@vue/compiler-sfc";
|
|
13
|
+
|
|
14
|
+
export const tuiVuePlugin: BunPlugin = {
|
|
15
|
+
name: "@dex-ai/vue-tui",
|
|
16
|
+
setup(build) {
|
|
17
|
+
// Match .vue files with optional query params (e.g. App.vue?v=1 for cache-busting)
|
|
18
|
+
build.onLoad({ filter: /\.vue(\?.*)?$/ }, async (args) => {
|
|
19
|
+
const fs = await import("fs");
|
|
20
|
+
// Strip query params from the path to get the actual file on disk
|
|
21
|
+
const filePath = args.path.replace(/\?.*$/, "");
|
|
22
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
23
|
+
|
|
24
|
+
const { descriptor, errors } = parse(source, {
|
|
25
|
+
filename: filePath,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (errors.length > 0) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Vue SFC parse error in ${filePath}:\n${errors.map((e: any) => e.message).join("\n")}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let scriptCode = "";
|
|
35
|
+
let bindings: Record<string, unknown> | undefined;
|
|
36
|
+
|
|
37
|
+
if (descriptor.scriptSetup || descriptor.script) {
|
|
38
|
+
const compiled = compileScript(descriptor, {
|
|
39
|
+
id: filePath,
|
|
40
|
+
inlineTemplate: false,
|
|
41
|
+
});
|
|
42
|
+
scriptCode = compiled.content;
|
|
43
|
+
bindings = compiled.bindings;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Our TUI element types — treated as native elements, not components
|
|
47
|
+
const TUI_ELEMENTS = new Set([
|
|
48
|
+
"root", "stack", "line", "row", "text", "spacer", "indent", "list",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
let templateCode = "";
|
|
52
|
+
if (descriptor.template) {
|
|
53
|
+
const templateResult = compileTemplate({
|
|
54
|
+
source: descriptor.template.content,
|
|
55
|
+
filename: filePath,
|
|
56
|
+
id: filePath,
|
|
57
|
+
compilerOptions: {
|
|
58
|
+
bindingMetadata: bindings as any,
|
|
59
|
+
mode: "function",
|
|
60
|
+
isCustomElement: (tag: string) => TUI_ELEMENTS.has(tag),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (templateResult.errors.length > 0) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Vue template compile error in ${filePath}:\n` +
|
|
67
|
+
templateResult.errors.map((e: any) => typeof e === "string" ? e : e.message).join("\n")
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
templateCode = templateResult.code;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const output = assembleModule(scriptCode, templateCode, args.path);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
contents: output,
|
|
78
|
+
loader: "ts",
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function assembleModule(scriptCode: string, templateCode: string, importPath: string): string {
|
|
85
|
+
const lines: string[] = [];
|
|
86
|
+
|
|
87
|
+
// Extract query string from the import path (e.g. ?v=1) to propagate to child .vue imports
|
|
88
|
+
const queryMatch = importPath.match(/(\?.*$)/);
|
|
89
|
+
const query = queryMatch ? queryMatch[1] : "";
|
|
90
|
+
|
|
91
|
+
// Import all Vue runtime helpers — the template compiler may use any of them.
|
|
92
|
+
// Using wildcard import + destructure avoids breakage when new helpers are used.
|
|
93
|
+
lines.push(`import * as _Vue from "@vue/runtime-core";`);
|
|
94
|
+
lines.push(
|
|
95
|
+
`const { h: _h, toDisplayString: _toDisplayString, openBlock: _openBlock, ` +
|
|
96
|
+
`createElementBlock: _createElementBlock, createElementVNode: _createElementVNode, ` +
|
|
97
|
+
`createVNode: _createVNode, createBlock: _createBlock, createCommentVNode: _createCommentVNode, ` +
|
|
98
|
+
`Fragment: _Fragment, renderList: _renderList, withCtx: _withCtx, ` +
|
|
99
|
+
`createTextVNode: _createTextVNode, resolveComponent: _resolveComponent, ` +
|
|
100
|
+
`normalizeStyle: _normalizeStyle, normalizeClass: _normalizeClass, ` +
|
|
101
|
+
`normalizeProps: _normalizeProps, guardReactiveProps: _guardReactiveProps, ` +
|
|
102
|
+
`mergeProps: _mergeProps, withDirectives: _withDirectives, ` +
|
|
103
|
+
`resolveDynamicComponent: _resolveDynamicComponent } = _Vue;`
|
|
104
|
+
);
|
|
105
|
+
lines.push("");
|
|
106
|
+
|
|
107
|
+
if (scriptCode) {
|
|
108
|
+
let modified = scriptCode;
|
|
109
|
+
// Replace 'vue' imports with '@vue/runtime-core'
|
|
110
|
+
modified = modified.replace(/from ['"]vue['"]/g, `from "@vue/runtime-core"`);
|
|
111
|
+
// Propagate query params to child .vue imports for cache-busting
|
|
112
|
+
if (query) {
|
|
113
|
+
modified = modified.replace(/from (['"])(.+?\.vue)\1/g, `from $1$2${query}$1`);
|
|
114
|
+
}
|
|
115
|
+
// Replace the default export with a variable
|
|
116
|
+
modified = modified
|
|
117
|
+
.replace(/export default\s*/, "const __sfc_main = ")
|
|
118
|
+
.replace(/export default/, "const __sfc_main =");
|
|
119
|
+
lines.push(modified);
|
|
120
|
+
} else {
|
|
121
|
+
lines.push("const __sfc_main = {};");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
lines.push("");
|
|
125
|
+
|
|
126
|
+
if (templateCode) {
|
|
127
|
+
// The compiled template in "function" mode outputs:
|
|
128
|
+
// const { ... } = Vue
|
|
129
|
+
// <hoisted vars>
|
|
130
|
+
// return function render(...) { ... }
|
|
131
|
+
//
|
|
132
|
+
// We strip the Vue destructure (already imported above) and
|
|
133
|
+
// wrap the return in an IIFE to extract the render function.
|
|
134
|
+
let code = templateCode;
|
|
135
|
+
// Remove the "const { ... } = Vue" line
|
|
136
|
+
code = code.replace(/^const \{[^}]+\} = Vue\s*\n?/m, "");
|
|
137
|
+
// Replace "return function render" with "const render = function"
|
|
138
|
+
code = code.replace(/^return function render/m, "const render = function");
|
|
139
|
+
lines.push(code);
|
|
140
|
+
lines.push("");
|
|
141
|
+
lines.push("__sfc_main.render = render;");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push("export default __sfc_main;");
|
|
146
|
+
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
package/src/register.ts
ADDED