@opentui/solid 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -11,6 +11,7 @@ bun install solid-js @opentui/solid
11
11
  ## Usage
12
12
 
13
13
  1. Add jsx config to tsconfig.json:
14
+
14
15
  ```json
15
16
  {
16
17
  "compilerOptions": {
@@ -21,16 +22,17 @@ bun install solid-js @opentui/solid
21
22
  ```
22
23
 
23
24
  2. Add preload script to bunfig.toml:
25
+
24
26
  ```toml
25
27
  preload = ["@opentui/solid/preload"]
26
28
  ```
27
29
 
28
30
  3. Add render function to index.tsx:
31
+
29
32
  ```tsx
30
- import { render } from "@opentui/solid";
33
+ import { render } from "@opentui/solid"
31
34
 
32
- render(() => <text>Hello, World!</text>);
35
+ render(() => <text>Hello, World!</text>)
33
36
  ```
34
37
 
35
-
36
38
  4. Run with `bun --conditions=browser index.tsx`.
package/index.js CHANGED
@@ -106,7 +106,6 @@ var elements = {
106
106
 
107
107
  // src/reconciler.ts
108
108
  import {
109
- GroupRenderable as GroupRenderable2,
110
109
  InputRenderable as InputRenderable2,
111
110
  InputRenderableEvents,
112
111
  Renderable,
@@ -131,10 +130,13 @@ function getNextId(elementType) {
131
130
  }
132
131
 
133
132
  // src/reconciler.ts
133
+ var GHOST_NODE_TAG = "text-ghost";
134
+
134
135
  class TextNode {
135
136
  id;
136
137
  chunk;
137
138
  parent;
139
+ textParent;
138
140
  constructor(chunk) {
139
141
  this.id = getNextId("text-node");
140
142
  this.chunk = chunk;
@@ -144,43 +146,94 @@ var ChunkToTextNodeMap = new WeakMap;
144
146
  var log = (...args) => {
145
147
  console.log("[Reconciler]", ...args);
146
148
  };
147
- function _insertNode(parent, node, anchor) {
148
- log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
149
- if (node instanceof TextNode) {
150
- if (!(parent instanceof TextRenderable2)) {
151
- console.warn(`Cannot insert text:"${node.chunk.plainText}" unless wrapped with a <text> element.`);
152
- const ghostNode = new GroupRenderable2(getNextId("ghost-group"), {});
153
- ghostNode.getLayoutNode().yogaNode.setDisplay(2);
154
- if (parent instanceof Renderable) {
155
- parent.add(ghostNode);
156
- }
149
+ function getOrCreateTextGhostNode(parent, anchor) {
150
+ if (anchor instanceof TextNode && anchor.textParent) {
151
+ return anchor.textParent;
152
+ }
153
+ const children = parent.getChildren();
154
+ if (anchor instanceof Renderable) {
155
+ const anchorIndex = children.findIndex((el) => el.id === anchor.id);
156
+ const beforeAnchor = children[anchorIndex - 1];
157
+ if (beforeAnchor instanceof TextRenderable2 && beforeAnchor.id.startsWith(GHOST_NODE_TAG)) {
158
+ return beforeAnchor;
159
+ }
160
+ }
161
+ const lastChild = children.at(-1);
162
+ if (lastChild instanceof TextRenderable2 && lastChild.id.startsWith(GHOST_NODE_TAG)) {
163
+ return lastChild;
164
+ }
165
+ const ghostNode = new TextRenderable2(getNextId(GHOST_NODE_TAG), {});
166
+ _insertNode(parent, ghostNode, anchor);
167
+ return ghostNode;
168
+ }
169
+ function insertTextNode(parent, node, anchor) {
170
+ if (!(parent instanceof Renderable)) {
171
+ console.warn("Attaching text node to parent text node, impossible");
172
+ return;
173
+ }
174
+ log("Inserting text node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
175
+ let textParent;
176
+ if (!(parent instanceof TextRenderable2)) {
177
+ textParent = getOrCreateTextGhostNode(parent, anchor);
178
+ } else {
179
+ textParent = parent;
180
+ }
181
+ node.textParent = textParent;
182
+ let styledText = textParent.content;
183
+ if (anchor && anchor instanceof TextNode) {
184
+ const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
185
+ if (anchorIndex == -1) {
186
+ console.log("anchor not found");
157
187
  return;
158
188
  }
159
- const styledText = parent.content;
160
- if (anchor && anchor instanceof TextNode) {
161
- const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
162
- if (anchorIndex == -1) {
163
- console.log("anchor not found");
164
- return;
165
- }
166
- styledText.insert(node.chunk, anchorIndex);
189
+ styledText = styledText.insert(node.chunk, anchorIndex);
190
+ } else {
191
+ const firstChunk = textParent.content.chunks[0];
192
+ if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
193
+ styledText = styledText.replace(node.chunk, firstChunk);
167
194
  } else {
168
- const firstChunk = parent.content.chunks[0];
169
- if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
170
- styledText.replace(node.chunk, firstChunk);
171
- } else {
172
- styledText.insert(node.chunk);
173
- }
195
+ styledText = styledText.insert(node.chunk);
174
196
  }
175
- parent.content = styledText;
176
- node.parent = parent;
197
+ }
198
+ textParent.content = styledText;
199
+ node.parent = parent;
200
+ return;
201
+ }
202
+ function removeTextNode(parent, node) {
203
+ if (!(parent instanceof Renderable)) {
204
+ ChunkToTextNodeMap.delete(node.chunk);
177
205
  return;
178
206
  }
207
+ if (parent === node.textParent && parent instanceof TextRenderable2) {
208
+ ChunkToTextNodeMap.delete(node.chunk);
209
+ parent.content = parent.content.remove(node.chunk);
210
+ } else if (node.textParent) {
211
+ ChunkToTextNodeMap.delete(node.chunk);
212
+ let styledText = node.textParent.content;
213
+ styledText = styledText.remove(node.chunk);
214
+ if (styledText.chunks.length > 0) {
215
+ node.textParent.content = styledText;
216
+ } else {
217
+ node.parent?.remove(node.textParent.id);
218
+ node.textParent.destroyRecursively();
219
+ }
220
+ }
221
+ }
222
+ function _insertNode(parent, node, anchor) {
223
+ log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
224
+ if (node instanceof TextNode) {
225
+ return insertTextNode(parent, node, anchor);
226
+ }
179
227
  if (!(parent instanceof Renderable)) {
180
228
  return;
181
229
  }
182
230
  if (anchor) {
183
- const anchorIndex = parent.getChildren().findIndex((el) => el.id === anchor.id);
231
+ const anchorIndex = parent.getChildren().findIndex((el) => {
232
+ if (anchor instanceof TextNode) {
233
+ return el.id === anchor.textParent?.id;
234
+ }
235
+ return el.id === anchor.id;
236
+ });
184
237
  parent.add(node, anchorIndex);
185
238
  } else {
186
239
  parent.add(node);
@@ -188,14 +241,12 @@ function _insertNode(parent, node, anchor) {
188
241
  }
189
242
  function _removeNode(parent, node) {
190
243
  log("Removing node:", node.id, "from parent:", parent.id);
191
- if (parent instanceof TextRenderable2 && node instanceof TextNode) {
192
- ChunkToTextNodeMap.delete(node.chunk);
193
- const styledText = parent.content;
194
- styledText.remove(node.chunk);
195
- parent.content = styledText;
196
- } else if (parent instanceof Renderable && node instanceof Renderable) {
197
- node.destroyRecursively();
244
+ if (node instanceof TextNode) {
245
+ return removeTextNode(parent, node);
246
+ }
247
+ if (parent instanceof Renderable && node instanceof Renderable) {
198
248
  parent.remove(node.id);
249
+ node.destroyRecursively();
199
250
  }
200
251
  }
201
252
  var {
@@ -239,15 +290,15 @@ var {
239
290
  text: new TextEncoder().encode(value),
240
291
  plainText: value
241
292
  };
242
- const parent = textNode.parent;
243
- if (!parent) {
293
+ const textParent = textNode.textParent;
294
+ if (!textParent) {
244
295
  log("No parent found for text node:", textNode.id);
245
296
  return;
246
297
  }
247
- if (parent instanceof TextRenderable2) {
248
- const styledText = parent.content;
298
+ if (textParent instanceof TextRenderable2) {
299
+ const styledText = textParent.content;
249
300
  styledText.replace(newChunk, textNode.chunk);
250
- parent.content = styledText;
301
+ textParent.content = styledText;
251
302
  textNode.chunk = newChunk;
252
303
  ChunkToTextNodeMap.set(newChunk, textNode);
253
304
  }
package/jsx-runtime.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Renderable } from "@opentui/core";
1
+ import { Renderable } from "@opentui/core"
2
2
  import {
3
3
  ASCIIFontElementProps,
4
4
  BoxElementProps,
@@ -7,25 +7,25 @@ import {
7
7
  SelectElementProps,
8
8
  TabSelectElementProps,
9
9
  TextElementProps,
10
- } from "./src/elements/index";
10
+ } from "./src/elements/index"
11
11
 
12
12
  declare namespace JSX {
13
13
  // Replace Node with Renderable
14
- type Element = Renderable | ArrayElement | (string & {}) | number | boolean | null | undefined;
14
+ type Element = Renderable | ArrayElement | (string & {}) | number | boolean | null | undefined
15
15
 
16
16
  interface ArrayElement extends Array<Element> {}
17
17
 
18
18
  interface IntrinsicElements {
19
- ascii_font: ASCIIFontElementProps;
20
- box: BoxElementProps;
21
- group: GroupElementProps;
22
- input: InputElementProps;
23
- select: SelectElementProps;
24
- tab_select: TabSelectElementProps;
25
- text: TextElementProps;
19
+ ascii_font: ASCIIFontElementProps
20
+ box: BoxElementProps
21
+ group: GroupElementProps
22
+ input: InputElementProps
23
+ select: SelectElementProps
24
+ tab_select: TabSelectElementProps
25
+ text: TextElementProps
26
26
  }
27
27
 
28
28
  interface ElementChildrenAttribute {
29
- children: {};
29
+ children: {}
30
30
  }
31
31
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "type": "module",
7
- "version": "0.1.6",
7
+ "version": "0.1.8",
8
8
  "description": "SolidJS renderer for OpenTUI",
9
9
  "license": "MIT",
10
10
  "repository": {
@@ -30,18 +30,18 @@
30
30
  "./jsx-dev-runtime": "./jsx-runtime.d.ts"
31
31
  },
32
32
  "dependencies": {
33
- "@opentui/core": "0.1.6"
33
+ "@opentui/core": "0.1.8"
34
34
  },
35
35
  "devDependencies": {
36
- "@types/babel__core": "^7.20.5",
37
- "babel-plugin-module-resolver": "^5.0.2",
38
- "@babel/core": "^7.28.0",
39
- "@babel/preset-typescript": "^7.27.1",
40
- "babel-preset-solid": "^1.9.9",
36
+ "@types/babel__core": "7.20.5",
37
+ "babel-plugin-module-resolver": "5.0.2",
38
+ "@babel/core": "7.28.0",
39
+ "@babel/preset-typescript": "7.27.1",
40
+ "babel-preset-solid": "1.9.9",
41
41
  "@types/bun": "latest"
42
42
  },
43
43
  "peerDependencies": {
44
- "solid-js": "^1.9.9",
44
+ "solid-js": "1.9.9",
45
45
  "typescript": "^5"
46
46
  }
47
47
  }
@@ -1,4 +1,4 @@
1
- import solidTransformPlugin from "./solid-plugin";
2
- import { plugin, type BunPlugin } from "bun";
1
+ import solidTransformPlugin from "./solid-plugin"
2
+ import { plugin, type BunPlugin } from "bun"
3
3
 
4
- plugin(solidTransformPlugin);
4
+ plugin(solidTransformPlugin)
@@ -1,9 +1,9 @@
1
- import { transformAsync } from "@babel/core";
1
+ import { transformAsync } from "@babel/core"
2
2
  // @ts-expect-error - Types not important.
3
- import ts from "@babel/preset-typescript";
3
+ import ts from "@babel/preset-typescript"
4
4
  // @ts-expect-error - Types not important.
5
- import solid from "babel-preset-solid";
6
- import { type BunPlugin } from "bun";
5
+ import solid from "babel-preset-solid"
6
+ import { type BunPlugin } from "bun"
7
7
 
8
8
  const solidTransformPlugin: BunPlugin = {
9
9
  name: "bun-plugin-solid",
@@ -26,8 +26,8 @@ const solidTransformPlugin: BunPlugin = {
26
26
  // };
27
27
  // });
28
28
  build.onLoad({ filter: /\.(js|ts)x$/ }, async (args) => {
29
- const { readFile } = await import("node:fs/promises");
30
- const code = await readFile(args.path, "utf8");
29
+ const { readFile } = await import("node:fs/promises")
30
+ const code = await readFile(args.path, "utf8")
31
31
  const transforms = await transformAsync(code, {
32
32
  filename: args.path,
33
33
  // env: {
@@ -46,13 +46,13 @@ const solidTransformPlugin: BunPlugin = {
46
46
  ],
47
47
  [ts],
48
48
  ],
49
- });
49
+ })
50
50
  return {
51
51
  contents: transforms?.code ?? "",
52
52
  loader: "js",
53
- };
54
- });
53
+ }
54
+ })
55
55
  },
56
- };
56
+ }
57
57
 
58
- export default solidTransformPlugin;
58
+ export default solidTransformPlugin
@@ -1,8 +1,9 @@
1
- import { Renderable, type TextChunk } from "@opentui/core";
1
+ import { Renderable, TextRenderable, type TextChunk } from "@opentui/core";
2
2
  declare class TextNode {
3
3
  id: string;
4
4
  chunk: TextChunk;
5
5
  parent?: Renderable;
6
+ textParent?: TextRenderable;
6
7
  constructor(chunk: TextChunk);
7
8
  }
8
9
  type DomNode = Renderable | TextNode;
package/src/reconciler.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // @bun
2
2
  // src/reconciler.ts
3
3
  import {
4
- GroupRenderable as GroupRenderable2,
5
4
  InputRenderable as InputRenderable2,
6
5
  InputRenderableEvents,
7
6
  Renderable,
@@ -56,10 +55,13 @@ function getNextId(elementType) {
56
55
  }
57
56
 
58
57
  // src/reconciler.ts
58
+ var GHOST_NODE_TAG = "text-ghost";
59
+
59
60
  class TextNode {
60
61
  id;
61
62
  chunk;
62
63
  parent;
64
+ textParent;
63
65
  constructor(chunk) {
64
66
  this.id = getNextId("text-node");
65
67
  this.chunk = chunk;
@@ -69,43 +71,94 @@ var ChunkToTextNodeMap = new WeakMap;
69
71
  var log = (...args) => {
70
72
  console.log("[Reconciler]", ...args);
71
73
  };
72
- function _insertNode(parent, node, anchor) {
73
- log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
74
- if (node instanceof TextNode) {
75
- if (!(parent instanceof TextRenderable2)) {
76
- console.warn(`Cannot insert text:"${node.chunk.plainText}" unless wrapped with a <text> element.`);
77
- const ghostNode = new GroupRenderable2(getNextId("ghost-group"), {});
78
- ghostNode.getLayoutNode().yogaNode.setDisplay(2);
79
- if (parent instanceof Renderable) {
80
- parent.add(ghostNode);
81
- }
74
+ function getOrCreateTextGhostNode(parent, anchor) {
75
+ if (anchor instanceof TextNode && anchor.textParent) {
76
+ return anchor.textParent;
77
+ }
78
+ const children = parent.getChildren();
79
+ if (anchor instanceof Renderable) {
80
+ const anchorIndex = children.findIndex((el) => el.id === anchor.id);
81
+ const beforeAnchor = children[anchorIndex - 1];
82
+ if (beforeAnchor instanceof TextRenderable2 && beforeAnchor.id.startsWith(GHOST_NODE_TAG)) {
83
+ return beforeAnchor;
84
+ }
85
+ }
86
+ const lastChild = children.at(-1);
87
+ if (lastChild instanceof TextRenderable2 && lastChild.id.startsWith(GHOST_NODE_TAG)) {
88
+ return lastChild;
89
+ }
90
+ const ghostNode = new TextRenderable2(getNextId(GHOST_NODE_TAG), {});
91
+ _insertNode(parent, ghostNode, anchor);
92
+ return ghostNode;
93
+ }
94
+ function insertTextNode(parent, node, anchor) {
95
+ if (!(parent instanceof Renderable)) {
96
+ console.warn("Attaching text node to parent text node, impossible");
97
+ return;
98
+ }
99
+ log("Inserting text node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
100
+ let textParent;
101
+ if (!(parent instanceof TextRenderable2)) {
102
+ textParent = getOrCreateTextGhostNode(parent, anchor);
103
+ } else {
104
+ textParent = parent;
105
+ }
106
+ node.textParent = textParent;
107
+ let styledText = textParent.content;
108
+ if (anchor && anchor instanceof TextNode) {
109
+ const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
110
+ if (anchorIndex == -1) {
111
+ console.log("anchor not found");
82
112
  return;
83
113
  }
84
- const styledText = parent.content;
85
- if (anchor && anchor instanceof TextNode) {
86
- const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
87
- if (anchorIndex == -1) {
88
- console.log("anchor not found");
89
- return;
90
- }
91
- styledText.insert(node.chunk, anchorIndex);
114
+ styledText = styledText.insert(node.chunk, anchorIndex);
115
+ } else {
116
+ const firstChunk = textParent.content.chunks[0];
117
+ if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
118
+ styledText = styledText.replace(node.chunk, firstChunk);
92
119
  } else {
93
- const firstChunk = parent.content.chunks[0];
94
- if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
95
- styledText.replace(node.chunk, firstChunk);
96
- } else {
97
- styledText.insert(node.chunk);
98
- }
120
+ styledText = styledText.insert(node.chunk);
99
121
  }
100
- parent.content = styledText;
101
- node.parent = parent;
122
+ }
123
+ textParent.content = styledText;
124
+ node.parent = parent;
125
+ return;
126
+ }
127
+ function removeTextNode(parent, node) {
128
+ if (!(parent instanceof Renderable)) {
129
+ ChunkToTextNodeMap.delete(node.chunk);
102
130
  return;
103
131
  }
132
+ if (parent === node.textParent && parent instanceof TextRenderable2) {
133
+ ChunkToTextNodeMap.delete(node.chunk);
134
+ parent.content = parent.content.remove(node.chunk);
135
+ } else if (node.textParent) {
136
+ ChunkToTextNodeMap.delete(node.chunk);
137
+ let styledText = node.textParent.content;
138
+ styledText = styledText.remove(node.chunk);
139
+ if (styledText.chunks.length > 0) {
140
+ node.textParent.content = styledText;
141
+ } else {
142
+ node.parent?.remove(node.textParent.id);
143
+ node.textParent.destroyRecursively();
144
+ }
145
+ }
146
+ }
147
+ function _insertNode(parent, node, anchor) {
148
+ log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
149
+ if (node instanceof TextNode) {
150
+ return insertTextNode(parent, node, anchor);
151
+ }
104
152
  if (!(parent instanceof Renderable)) {
105
153
  return;
106
154
  }
107
155
  if (anchor) {
108
- const anchorIndex = parent.getChildren().findIndex((el) => el.id === anchor.id);
156
+ const anchorIndex = parent.getChildren().findIndex((el) => {
157
+ if (anchor instanceof TextNode) {
158
+ return el.id === anchor.textParent?.id;
159
+ }
160
+ return el.id === anchor.id;
161
+ });
109
162
  parent.add(node, anchorIndex);
110
163
  } else {
111
164
  parent.add(node);
@@ -113,14 +166,12 @@ function _insertNode(parent, node, anchor) {
113
166
  }
114
167
  function _removeNode(parent, node) {
115
168
  log("Removing node:", node.id, "from parent:", parent.id);
116
- if (parent instanceof TextRenderable2 && node instanceof TextNode) {
117
- ChunkToTextNodeMap.delete(node.chunk);
118
- const styledText = parent.content;
119
- styledText.remove(node.chunk);
120
- parent.content = styledText;
121
- } else if (parent instanceof Renderable && node instanceof Renderable) {
122
- node.destroyRecursively();
169
+ if (node instanceof TextNode) {
170
+ return removeTextNode(parent, node);
171
+ }
172
+ if (parent instanceof Renderable && node instanceof Renderable) {
123
173
  parent.remove(node.id);
174
+ node.destroyRecursively();
124
175
  }
125
176
  }
126
177
  var {
@@ -164,15 +215,15 @@ var {
164
215
  text: new TextEncoder().encode(value),
165
216
  plainText: value
166
217
  };
167
- const parent = textNode.parent;
168
- if (!parent) {
218
+ const textParent = textNode.textParent;
219
+ if (!textParent) {
169
220
  log("No parent found for text node:", textNode.id);
170
221
  return;
171
222
  }
172
- if (parent instanceof TextRenderable2) {
173
- const styledText = parent.content;
223
+ if (textParent instanceof TextRenderable2) {
224
+ const styledText = textParent.content;
174
225
  styledText.replace(newChunk, textNode.chunk);
175
- parent.content = styledText;
226
+ textParent.content = styledText;
176
227
  textNode.chunk = newChunk;
177
228
  ChunkToTextNodeMap.set(newChunk, textNode);
178
229
  }