@neuralumina/lumina-ui 0.1.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/LICENSE +21 -0
- package/README.md +1694 -0
- package/lumina-ui/core/element.js +118 -0
- package/lumina-ui/core/renderer.js +376 -0
- package/lumina-ui/core/state.js +99 -0
- package/lumina-ui/widgets/accessibility.js +45 -0
- package/lumina-ui/widgets/animation.js +112 -0
- package/lumina-ui/widgets/controls.js +312 -0
- package/lumina-ui/widgets/display.js +443 -0
- package/lumina-ui/widgets/feedback.js +316 -0
- package/lumina-ui/widgets/forms.js +342 -0
- package/lumina-ui/widgets/interaction.js +254 -0
- package/lumina-ui/widgets/layout.js +624 -0
- package/lumina-ui/widgets/navigation.js +313 -0
- package/lumina-ui/widgets/scrolling.js +330 -0
- package/lumina-ui/widgets/text.js +121 -0
- package/lumina-ui/widgets/utils.js +221 -0
- package/lumina-ui.js +154 -0
- package/package.json +38 -0
- package/scripts/smoke-test.mjs +256 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
class BaseNode {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.childNodes = [];
|
|
4
|
+
this.parentNode = null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
appendChild(node) {
|
|
8
|
+
if (node.nodeType === 11) {
|
|
9
|
+
[...node.childNodes].forEach((child) => this.appendChild(child));
|
|
10
|
+
node.childNodes = [];
|
|
11
|
+
return node;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
15
|
+
this.childNodes.push(node);
|
|
16
|
+
node.parentNode = this;
|
|
17
|
+
return node;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
insertBefore(node, reference) {
|
|
21
|
+
if (node.nodeType === 11) {
|
|
22
|
+
[...node.childNodes].forEach((child) =>
|
|
23
|
+
this.insertBefore(child, reference),
|
|
24
|
+
);
|
|
25
|
+
node.childNodes = [];
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
30
|
+
const index = reference ? this.childNodes.indexOf(reference) : -1;
|
|
31
|
+
if (index < 0) this.childNodes.push(node);
|
|
32
|
+
else this.childNodes.splice(index, 0, node);
|
|
33
|
+
node.parentNode = this;
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
replaceChild(next, old) {
|
|
38
|
+
const index = this.childNodes.indexOf(old);
|
|
39
|
+
if (index < 0) throw new Error("Cannot replace missing child");
|
|
40
|
+
|
|
41
|
+
if (next.nodeType === 11) {
|
|
42
|
+
this.childNodes.splice(index, 1);
|
|
43
|
+
old.parentNode = null;
|
|
44
|
+
[...next.childNodes]
|
|
45
|
+
.reverse()
|
|
46
|
+
.forEach((child) =>
|
|
47
|
+
this.insertBefore(child, this.childNodes[index] || null),
|
|
48
|
+
);
|
|
49
|
+
next.childNodes = [];
|
|
50
|
+
return old;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (next.parentNode) next.parentNode.removeChild(next);
|
|
54
|
+
this.childNodes[index] = next;
|
|
55
|
+
old.parentNode = null;
|
|
56
|
+
next.parentNode = this;
|
|
57
|
+
return old;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
removeChild(node) {
|
|
61
|
+
const index = this.childNodes.indexOf(node);
|
|
62
|
+
if (index >= 0) this.childNodes.splice(index, 1);
|
|
63
|
+
node.parentNode = null;
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get lastChild() {
|
|
68
|
+
return this.childNodes[this.childNodes.length - 1] || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get textContent() {
|
|
72
|
+
return this.childNodes.map((child) => child.textContent).join("");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
set textContent(value) {
|
|
76
|
+
this.childNodes = [new TextNode(value)];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class ElementNode extends BaseNode {
|
|
81
|
+
constructor(tag) {
|
|
82
|
+
super();
|
|
83
|
+
this.tagName = tag;
|
|
84
|
+
this.nodeType = 1;
|
|
85
|
+
this.style = { cssText: "" };
|
|
86
|
+
this.dataset = {};
|
|
87
|
+
this.attributes = {};
|
|
88
|
+
this.listeners = {};
|
|
89
|
+
this.className = "";
|
|
90
|
+
this.classList = {
|
|
91
|
+
add: (...classes) => {
|
|
92
|
+
this.className = classes.filter(Boolean).join(" ");
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
set innerHTML(value) {
|
|
98
|
+
this.childNodes = [];
|
|
99
|
+
this._innerHTML = String(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get innerHTML() {
|
|
103
|
+
return this._innerHTML || "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setAttribute(key, value) {
|
|
107
|
+
this.attributes[key] = String(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
removeAttribute(key) {
|
|
111
|
+
delete this.attributes[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
addEventListener(key, fn) {
|
|
115
|
+
this.listeners[key] = fn;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
removeEventListener(key, fn) {
|
|
119
|
+
if (this.listeners[key] === fn) delete this.listeners[key];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
click() {
|
|
123
|
+
this.listeners.click?.({
|
|
124
|
+
type: "click",
|
|
125
|
+
target: this,
|
|
126
|
+
defaultPrevented: false,
|
|
127
|
+
stopPropagation() {},
|
|
128
|
+
preventDefault() {
|
|
129
|
+
this.defaultPrevented = true;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class TextNode extends BaseNode {
|
|
136
|
+
constructor(text) {
|
|
137
|
+
super();
|
|
138
|
+
this.nodeType = 3;
|
|
139
|
+
this._text = String(text);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get textContent() {
|
|
143
|
+
return this._text;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
set textContent(value) {
|
|
147
|
+
this._text = String(value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
class FragmentNode extends BaseNode {
|
|
152
|
+
constructor() {
|
|
153
|
+
super();
|
|
154
|
+
this.nodeType = 11;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
globalThis.Node = BaseNode;
|
|
159
|
+
globalThis.document = {
|
|
160
|
+
createElement: (tag) => new ElementNode(tag),
|
|
161
|
+
createTextNode: (text) => new TextNode(text),
|
|
162
|
+
createDocumentFragment: () => new FragmentNode(),
|
|
163
|
+
head: { appendChild() {} },
|
|
164
|
+
getElementById: () => null,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function assert(condition, message) {
|
|
168
|
+
if (!condition) throw new Error(message);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const {
|
|
172
|
+
AbsorbPointer,
|
|
173
|
+
Button,
|
|
174
|
+
Column,
|
|
175
|
+
Input,
|
|
176
|
+
ListView,
|
|
177
|
+
SnackBar,
|
|
178
|
+
Switch,
|
|
179
|
+
Text,
|
|
180
|
+
mount,
|
|
181
|
+
} = await import("../lumina-ui.js");
|
|
182
|
+
|
|
183
|
+
let items = [{ id: "a" }, { id: "b" }];
|
|
184
|
+
let clicks = 0;
|
|
185
|
+
function stableClick() {
|
|
186
|
+
clicks += 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function App() {
|
|
190
|
+
return Column([
|
|
191
|
+
Button({ text: "Stable", onClick: stableClick }),
|
|
192
|
+
[[Text("Nested")]],
|
|
193
|
+
ListView({
|
|
194
|
+
items,
|
|
195
|
+
itemBuilder: (item) => ({ tag: "article", children: [item.id] }),
|
|
196
|
+
}),
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const root = new ElementNode("root");
|
|
201
|
+
const update = mount(App, root);
|
|
202
|
+
root.childNodes[0].childNodes[0].click();
|
|
203
|
+
items = [{ id: "b" }, { id: "c" }];
|
|
204
|
+
update();
|
|
205
|
+
root.childNodes[0].childNodes[0].click();
|
|
206
|
+
assert(clicks === 2, "stable event handler was lost after patch");
|
|
207
|
+
assert(root.textContent === "StableNestedbc", "keyed list patch failed");
|
|
208
|
+
|
|
209
|
+
let checked = false;
|
|
210
|
+
const inputRoot = new ElementNode("root");
|
|
211
|
+
const inputUpdate = mount(
|
|
212
|
+
() => Input({ type: "checkbox", value: checked }),
|
|
213
|
+
inputRoot,
|
|
214
|
+
);
|
|
215
|
+
inputRoot.childNodes[0].checked = true;
|
|
216
|
+
inputUpdate();
|
|
217
|
+
assert(inputRoot.childNodes[0].checked === false, "controlled checkbox failed");
|
|
218
|
+
|
|
219
|
+
let switched = false;
|
|
220
|
+
const disabledSwitch = Switch({
|
|
221
|
+
value: true,
|
|
222
|
+
disabled: true,
|
|
223
|
+
onChange: () => {
|
|
224
|
+
switched = true;
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
disabledSwitch.props.onClick({});
|
|
228
|
+
assert(disabledSwitch.props.type === "button", "switch type should be button");
|
|
229
|
+
assert(switched === false, "disabled switch should not change");
|
|
230
|
+
|
|
231
|
+
const blocked = AbsorbPointer({ style: { position: "static" } }, [Text("x")]);
|
|
232
|
+
assert(blocked.props.style.position === "relative", "AbsorbPointer position failed");
|
|
233
|
+
assert(blocked.children.length === 2, "AbsorbPointer overlay missing");
|
|
234
|
+
|
|
235
|
+
let snackOpen = true;
|
|
236
|
+
const stackRoot = new ElementNode("root");
|
|
237
|
+
const stackUpdate = mount(
|
|
238
|
+
() =>
|
|
239
|
+
snackOpen
|
|
240
|
+
? SnackBar({
|
|
241
|
+
message: "Saved",
|
|
242
|
+
action: Button({
|
|
243
|
+
text: "Dismiss",
|
|
244
|
+
onClick: () => {
|
|
245
|
+
snackOpen = false;
|
|
246
|
+
stackUpdate();
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
})
|
|
250
|
+
: null,
|
|
251
|
+
stackRoot,
|
|
252
|
+
);
|
|
253
|
+
stackRoot.childNodes[0].childNodes[1].click();
|
|
254
|
+
assert(stackRoot.textContent === "", "snackbar dismiss failed");
|
|
255
|
+
|
|
256
|
+
console.log("smoke ok");
|