@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,118 @@
|
|
|
1
|
+
export function createElement(tag, props = {}) {
|
|
2
|
+
const { children, style, className, dataset, key, ...rest } = props;
|
|
3
|
+
const element = document.createElement(tag);
|
|
4
|
+
|
|
5
|
+
// Apply classes
|
|
6
|
+
if (className) {
|
|
7
|
+
if (Array.isArray(className)) element.classList.add(...className);
|
|
8
|
+
else element.className = className;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Apply dataset
|
|
12
|
+
if (dataset && typeof dataset === "object") {
|
|
13
|
+
Object.keys(dataset).forEach((k) => {
|
|
14
|
+
element.dataset[k] = dataset[k];
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Apply styles
|
|
19
|
+
if (style && typeof style === "object") {
|
|
20
|
+
Object.assign(element.style, style);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Apply remaining props (reflecting properties for common DOM props)
|
|
24
|
+
Object.entries(rest).forEach(([key, value]) => {
|
|
25
|
+
if (value === undefined || value === null) return;
|
|
26
|
+
|
|
27
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
28
|
+
const event = normalizeEventName(key);
|
|
29
|
+
element.addEventListener(event, value);
|
|
30
|
+
} else if (
|
|
31
|
+
key === "checked" ||
|
|
32
|
+
key === "value" ||
|
|
33
|
+
key === "disabled" ||
|
|
34
|
+
key === "selected"
|
|
35
|
+
) {
|
|
36
|
+
// Reflect common boolean/value props
|
|
37
|
+
try {
|
|
38
|
+
element[key] = value;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
element.setAttribute(key, String(value));
|
|
41
|
+
}
|
|
42
|
+
} else if (key === "htmlFor") {
|
|
43
|
+
element.setAttribute("for", String(value));
|
|
44
|
+
} else if (
|
|
45
|
+
key === "id" ||
|
|
46
|
+
key === "type" ||
|
|
47
|
+
key === "name" ||
|
|
48
|
+
key === "placeholder" ||
|
|
49
|
+
key === "title" ||
|
|
50
|
+
key === "role" ||
|
|
51
|
+
key === "aria-label" ||
|
|
52
|
+
key === "aria-checked" ||
|
|
53
|
+
key.startsWith("aria-")
|
|
54
|
+
) {
|
|
55
|
+
element.setAttribute(key, String(value));
|
|
56
|
+
} else if (key === "tabIndex") {
|
|
57
|
+
element.tabIndex = value;
|
|
58
|
+
} else {
|
|
59
|
+
// fallback to attribute - only for primitive types
|
|
60
|
+
if (typeof value !== "object" || value === null) {
|
|
61
|
+
element.setAttribute(key, String(value));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Append children if provided
|
|
67
|
+
if (children !== undefined && children !== null) {
|
|
68
|
+
const childrenArray = Array.isArray(children) ? children : [children];
|
|
69
|
+
childrenArray.forEach((child) => {
|
|
70
|
+
if (child === null || child === undefined) return;
|
|
71
|
+
if (child instanceof Node) {
|
|
72
|
+
element.appendChild(child);
|
|
73
|
+
} else if (typeof child === "object" && child._dom) {
|
|
74
|
+
// internal fast path if widget has a cached DOM
|
|
75
|
+
element.appendChild(child._dom);
|
|
76
|
+
} else {
|
|
77
|
+
element.appendChild(document.createTextNode(String(child)));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return element;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function Fragment({ children }) {
|
|
86
|
+
const fragment = document.createDocumentFragment();
|
|
87
|
+
if (children) {
|
|
88
|
+
const childrenArray = Array.isArray(children) ? children : [children];
|
|
89
|
+
childrenArray.forEach((child) => {
|
|
90
|
+
if (child instanceof Node) fragment.appendChild(child);
|
|
91
|
+
else if (typeof child === "object" && child._dom) {
|
|
92
|
+
fragment.appendChild(child._dom);
|
|
93
|
+
} else if (child !== null && child !== undefined)
|
|
94
|
+
fragment.appendChild(document.createTextNode(String(child)));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return fragment;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function applyStyles(element, styles) {
|
|
101
|
+
if (!element || !styles) return element;
|
|
102
|
+
Object.assign(element.style, styles);
|
|
103
|
+
return element;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function addClasses(element, ...classes) {
|
|
107
|
+
if (!element) return element;
|
|
108
|
+
element.classList.add(...classes.filter(Boolean));
|
|
109
|
+
return element;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeEventName(onName) {
|
|
113
|
+
// onClick -> click, onDoubleClick -> dblclick (common mapping)
|
|
114
|
+
const name = onName.slice(2);
|
|
115
|
+
const lower = name.toLowerCase();
|
|
116
|
+
if (lower === "doubleclick" || lower === "dblclick") return "dblclick";
|
|
117
|
+
return lower;
|
|
118
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { createElement } from "./element.js";
|
|
2
|
+
|
|
3
|
+
// vnode shape: { tag, props, children, key }
|
|
4
|
+
// function widgets: (forceUpdate) => vnode
|
|
5
|
+
|
|
6
|
+
export function mount(componentFn, container) {
|
|
7
|
+
let currentTree = null;
|
|
8
|
+
let mounted = true;
|
|
9
|
+
|
|
10
|
+
const forceUpdate = () => {
|
|
11
|
+
if (!mounted) return;
|
|
12
|
+
render();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function render() {
|
|
16
|
+
const newTree =
|
|
17
|
+
typeof componentFn === "function" ? componentFn(forceUpdate) : componentFn;
|
|
18
|
+
if (!currentTree) {
|
|
19
|
+
const dom = renderWidget(newTree, forceUpdate);
|
|
20
|
+
container.innerHTML = "";
|
|
21
|
+
container.appendChild(dom);
|
|
22
|
+
} else {
|
|
23
|
+
patchWidget(container, currentTree, newTree, 0, null, forceUpdate);
|
|
24
|
+
}
|
|
25
|
+
currentTree = normalizeVNode(newTree);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render();
|
|
29
|
+
|
|
30
|
+
const wrappedForce = () => {
|
|
31
|
+
if (mounted) forceUpdate();
|
|
32
|
+
};
|
|
33
|
+
wrappedForce.unmount = () => {
|
|
34
|
+
mounted = false;
|
|
35
|
+
container.innerHTML = "";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return wrappedForce;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderWidget(widget, forceUpdate) {
|
|
42
|
+
// primitives
|
|
43
|
+
if (isEmptyWidget(widget))
|
|
44
|
+
return document.createTextNode("");
|
|
45
|
+
if (typeof widget === "string" || typeof widget === "number")
|
|
46
|
+
return document.createTextNode(String(widget));
|
|
47
|
+
if (typeof Node !== "undefined" && widget instanceof Node) return widget;
|
|
48
|
+
|
|
49
|
+
// arrays -> fragment
|
|
50
|
+
if (Array.isArray(widget)) {
|
|
51
|
+
const fragment = document.createDocumentFragment();
|
|
52
|
+
flattenChildren(widget).forEach((child) => {
|
|
53
|
+
const childDom = renderWidget(child, forceUpdate);
|
|
54
|
+
if (childDom) fragment.appendChild(childDom);
|
|
55
|
+
});
|
|
56
|
+
return fragment;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// functional widget (component)
|
|
60
|
+
if (typeof widget === "function") {
|
|
61
|
+
return renderWidget(widget(forceUpdate), forceUpdate);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// vnode object
|
|
65
|
+
if (widget.tag) {
|
|
66
|
+
const props = widget.props || {};
|
|
67
|
+
const children = flattenChildren(widget.children || []);
|
|
68
|
+
|
|
69
|
+
const element = createElement(widget.tag, {
|
|
70
|
+
...(props || {}),
|
|
71
|
+
children: [],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// attach a backref for fast updates (optional)
|
|
75
|
+
element._vnodeKey = widget.key ?? props.key ?? null;
|
|
76
|
+
|
|
77
|
+
(children || []).forEach((child) => {
|
|
78
|
+
const childDom = renderWidget(child, forceUpdate);
|
|
79
|
+
if (childDom) element.appendChild(childDom);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return element;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return document.createTextNode("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeVNode(v) {
|
|
89
|
+
// If given a function, call it (no forceUpdate)
|
|
90
|
+
if (typeof v === "function") v = v();
|
|
91
|
+
if (isEmptyWidget(v)) return { tag: "empty", children: [] };
|
|
92
|
+
if (Array.isArray(v))
|
|
93
|
+
return { tag: "fragment", children: flattenChildren(v).map(normalizeVNode) };
|
|
94
|
+
if (v && v.tag) {
|
|
95
|
+
return {
|
|
96
|
+
...v,
|
|
97
|
+
children: flattenChildren(v.children || []),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { tag: "text", children: [String(v)] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function patchWidget(parent, oldWidget, newWidget, index = 0, currentDom = null, forceUpdate = null) {
|
|
104
|
+
// Normalize to vnodes
|
|
105
|
+
const oldV = normalizeVNode(oldWidget);
|
|
106
|
+
const newV = normalizeVNode(newWidget);
|
|
107
|
+
|
|
108
|
+
const dom = currentDom || parent.childNodes[index];
|
|
109
|
+
if (!dom) return;
|
|
110
|
+
|
|
111
|
+
// Replace if different tag or key
|
|
112
|
+
const oldTag = oldV.tag;
|
|
113
|
+
const newTag = newV.tag;
|
|
114
|
+
const oldKey = oldV.key ?? (oldV.props && oldV.props.key);
|
|
115
|
+
const newKey = newV.key ?? (newV.props && newV.props.key);
|
|
116
|
+
|
|
117
|
+
if (oldTag === "empty" && newTag === "empty") return;
|
|
118
|
+
|
|
119
|
+
if (oldKey != null || newKey != null) {
|
|
120
|
+
// keyed children are handled at parent's loop level — here we fall back to replace if mismatched
|
|
121
|
+
if (oldKey !== newKey || oldTag !== newTag) {
|
|
122
|
+
const newDom = renderWidget(newWidget, forceUpdate);
|
|
123
|
+
parent.replaceChild(newDom, dom);
|
|
124
|
+
return newDom;
|
|
125
|
+
}
|
|
126
|
+
} else if (oldTag !== newTag) {
|
|
127
|
+
const newDom = renderWidget(newWidget, forceUpdate);
|
|
128
|
+
parent.replaceChild(newDom, dom);
|
|
129
|
+
return newDom;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Text node
|
|
133
|
+
if (
|
|
134
|
+
newTag === "empty" ||
|
|
135
|
+
newTag === "text" ||
|
|
136
|
+
typeof newWidget === "string" ||
|
|
137
|
+
typeof newWidget === "number"
|
|
138
|
+
) {
|
|
139
|
+
const nextText =
|
|
140
|
+
newTag === "empty" ? "" : String(newV.children?.[0] ?? newWidget);
|
|
141
|
+
if (
|
|
142
|
+
dom &&
|
|
143
|
+
dom.nodeType === Node.TEXT_NODE &&
|
|
144
|
+
dom.textContent !== nextText
|
|
145
|
+
) {
|
|
146
|
+
dom.textContent = nextText;
|
|
147
|
+
} else if (dom && dom.nodeType !== Node.TEXT_NODE) {
|
|
148
|
+
const newDom = renderWidget(newWidget, forceUpdate);
|
|
149
|
+
parent.replaceChild(newDom, dom);
|
|
150
|
+
return newDom;
|
|
151
|
+
}
|
|
152
|
+
return dom;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Update props
|
|
156
|
+
const newProps = newV.props || {};
|
|
157
|
+
const oldProps = oldV.props || {};
|
|
158
|
+
updateProps(dom, oldProps, newProps);
|
|
159
|
+
|
|
160
|
+
// Reconcile children (simple keyed-first pass)
|
|
161
|
+
const oldChildren = flattenChildren(oldV.children || []);
|
|
162
|
+
const newChildren = flattenChildren(newV.children || []);
|
|
163
|
+
|
|
164
|
+
// Build key -> index map for old children
|
|
165
|
+
const keyed = new Map();
|
|
166
|
+
oldChildren.forEach((c, i) => {
|
|
167
|
+
const k = widgetKey(c);
|
|
168
|
+
const node = dom.childNodes[i];
|
|
169
|
+
if (k != null && node) keyed.set(k, { vnode: c, node });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// If any new child has a key, do keyed reconciliation
|
|
173
|
+
const anyKeyed = newChildren.some((c) => widgetKey(c) != null);
|
|
174
|
+
if (anyKeyed) {
|
|
175
|
+
const usedKeys = new Set();
|
|
176
|
+
|
|
177
|
+
newChildren.forEach((nc, i) => {
|
|
178
|
+
const nk = widgetKey(nc);
|
|
179
|
+
let node;
|
|
180
|
+
|
|
181
|
+
if (nk != null && keyed.has(nk)) {
|
|
182
|
+
const oldEntry = keyed.get(nk);
|
|
183
|
+
usedKeys.add(nk);
|
|
184
|
+
node = patchWidget(dom, oldEntry.vnode, nc, 0, oldEntry.node, forceUpdate);
|
|
185
|
+
} else {
|
|
186
|
+
node = renderWidget(nc, forceUpdate);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const targetNode = dom.childNodes[i] || null;
|
|
190
|
+
if (node && node !== targetNode) {
|
|
191
|
+
dom.insertBefore(node, targetNode);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
keyed.forEach((entry, key) => {
|
|
196
|
+
if (!usedKeys.has(key) && entry.node.parentNode === dom) {
|
|
197
|
+
dom.removeChild(entry.node);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
while (dom.childNodes.length > newChildren.length) {
|
|
202
|
+
dom.removeChild(dom.lastChild);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return dom;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fallback index-based reconciliation
|
|
209
|
+
const maxLen = Math.max(oldChildren.length, newChildren.length);
|
|
210
|
+
for (let i = 0; i < maxLen; i++) {
|
|
211
|
+
const oldC = oldChildren[i];
|
|
212
|
+
const newC = newChildren[i];
|
|
213
|
+
const oldEmpty = isEmptyWidget(oldC);
|
|
214
|
+
const newEmpty = isEmptyWidget(newC);
|
|
215
|
+
|
|
216
|
+
if (oldEmpty && newEmpty) {
|
|
217
|
+
continue;
|
|
218
|
+
} else if (oldEmpty && !newEmpty) {
|
|
219
|
+
const newDom = renderWidget(newC, forceUpdate);
|
|
220
|
+
if (dom.childNodes[i]) dom.replaceChild(newDom, dom.childNodes[i]);
|
|
221
|
+
else dom.insertBefore(newDom, dom.childNodes[i] || null);
|
|
222
|
+
} else if (!oldEmpty && newEmpty) {
|
|
223
|
+
const emptyDom = renderWidget(newC, forceUpdate);
|
|
224
|
+
if (dom.childNodes[i]) dom.replaceChild(emptyDom, dom.childNodes[i]);
|
|
225
|
+
else dom.insertBefore(emptyDom, dom.childNodes[i] || null);
|
|
226
|
+
} else {
|
|
227
|
+
patchWidget(dom, oldC, newC, i, null, forceUpdate);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return dom;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function updateProps(dom, oldProps = {}, newProps = {}) {
|
|
235
|
+
if (!dom) return;
|
|
236
|
+
|
|
237
|
+
// Remove old event listeners and attributes not in newProps
|
|
238
|
+
Object.keys(oldProps).forEach((key) => {
|
|
239
|
+
const oldVal = oldProps[key];
|
|
240
|
+
const nextVal = newProps[key];
|
|
241
|
+
if (
|
|
242
|
+
key.startsWith("on") &&
|
|
243
|
+
typeof oldVal === "function" &&
|
|
244
|
+
oldVal !== nextVal
|
|
245
|
+
) {
|
|
246
|
+
const event = normalizeEventName(key);
|
|
247
|
+
dom.removeEventListener(event, oldVal);
|
|
248
|
+
}
|
|
249
|
+
if (!(key in newProps) || newProps[key] === undefined || newProps[key] === null) {
|
|
250
|
+
// cleanup reflecting props and attributes
|
|
251
|
+
if (key === "style") {
|
|
252
|
+
dom.removeAttribute("style");
|
|
253
|
+
if (dom.style) dom.style.cssText = "";
|
|
254
|
+
} else if (key === "className") {
|
|
255
|
+
dom.className = "";
|
|
256
|
+
dom.removeAttribute("class");
|
|
257
|
+
} else if (key === "dataset" && typeof oldVal === "object") {
|
|
258
|
+
Object.keys(oldVal).forEach((k) => delete dom.dataset[k]);
|
|
259
|
+
} else if (key === "value") {
|
|
260
|
+
try {
|
|
261
|
+
dom.value = "";
|
|
262
|
+
} catch (e) {
|
|
263
|
+
dom.removeAttribute(key);
|
|
264
|
+
}
|
|
265
|
+
} else if (
|
|
266
|
+
key === "checked" ||
|
|
267
|
+
key === "disabled" ||
|
|
268
|
+
key === "selected"
|
|
269
|
+
) {
|
|
270
|
+
try {
|
|
271
|
+
dom[key] = false;
|
|
272
|
+
} catch (e) {
|
|
273
|
+
dom.removeAttribute(key);
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
dom.removeAttribute(key);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Set new props
|
|
282
|
+
Object.keys(newProps).forEach((key) => {
|
|
283
|
+
const value = newProps[key];
|
|
284
|
+
const oldValue = oldProps[key];
|
|
285
|
+
|
|
286
|
+
if (value === undefined || value === null) return;
|
|
287
|
+
if (value === oldValue && !isReflectedDomProp(key)) return;
|
|
288
|
+
|
|
289
|
+
if (key === "style" && typeof value === "object") {
|
|
290
|
+
const previous = oldProps.style || {};
|
|
291
|
+
Object.keys(previous).forEach((styleKey) => {
|
|
292
|
+
if (!(styleKey in value)) dom.style[styleKey] = "";
|
|
293
|
+
});
|
|
294
|
+
Object.assign(dom.style, value);
|
|
295
|
+
} else if (key.startsWith("on") && typeof value === "function") {
|
|
296
|
+
const event = normalizeEventName(key);
|
|
297
|
+
dom.addEventListener(event, value);
|
|
298
|
+
} else if (key === "className") {
|
|
299
|
+
if (Array.isArray(value)) {
|
|
300
|
+
dom.className = value.filter((v) => v && typeof v === "string").join(" ");
|
|
301
|
+
} else {
|
|
302
|
+
dom.className = value ? String(value) : "";
|
|
303
|
+
}
|
|
304
|
+
} else if (key === "dataset" && typeof value === "object") {
|
|
305
|
+
const previous = oldProps.dataset || {};
|
|
306
|
+
Object.keys(previous).forEach((k) => {
|
|
307
|
+
if (!(k in value)) delete dom.dataset[k];
|
|
308
|
+
});
|
|
309
|
+
Object.keys(value).forEach((k) => (dom.dataset[k] = value[k]));
|
|
310
|
+
} else if (
|
|
311
|
+
key === "checked" ||
|
|
312
|
+
key === "value" ||
|
|
313
|
+
key === "disabled" ||
|
|
314
|
+
key === "selected"
|
|
315
|
+
) {
|
|
316
|
+
try {
|
|
317
|
+
dom[key] = value;
|
|
318
|
+
} catch (e) {
|
|
319
|
+
dom.setAttribute(key, String(value));
|
|
320
|
+
}
|
|
321
|
+
} else if (key === "tabIndex") {
|
|
322
|
+
dom.tabIndex = value;
|
|
323
|
+
} else if (key === "htmlFor") {
|
|
324
|
+
dom.setAttribute("for", String(value));
|
|
325
|
+
} else if (
|
|
326
|
+
key !== "children" &&
|
|
327
|
+
key !== "key" &&
|
|
328
|
+
value !== undefined &&
|
|
329
|
+
value !== null
|
|
330
|
+
) {
|
|
331
|
+
dom.setAttribute(key, String(value));
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function flattenChildren(children = []) {
|
|
337
|
+
const output = [];
|
|
338
|
+
const list = Array.isArray(children) ? children : [children];
|
|
339
|
+
|
|
340
|
+
list.forEach((child) => {
|
|
341
|
+
if (Array.isArray(child)) output.push(...flattenChildren(child));
|
|
342
|
+
else output.push(child);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return output;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function widgetKey(widget) {
|
|
349
|
+
if (!widget || Array.isArray(widget) || typeof widget !== "object") return null;
|
|
350
|
+
return widget.key ?? (widget.props && widget.props.key) ?? null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isReflectedDomProp(key) {
|
|
354
|
+
return (
|
|
355
|
+
key === "checked" ||
|
|
356
|
+
key === "value" ||
|
|
357
|
+
key === "disabled" ||
|
|
358
|
+
key === "selected"
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function normalizeEventName(onName) {
|
|
363
|
+
const name = onName.slice(2);
|
|
364
|
+
const lower = name.toLowerCase();
|
|
365
|
+
if (lower === "doubleclick" || lower === "dblclick") return "dblclick";
|
|
366
|
+
return lower;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function isEmptyWidget(widget) {
|
|
370
|
+
return (
|
|
371
|
+
widget === null ||
|
|
372
|
+
widget === undefined ||
|
|
373
|
+
widget === false ||
|
|
374
|
+
widget === true
|
|
375
|
+
);
|
|
376
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight state utilities that work with renderer's forceUpdate pattern.
|
|
3
|
+
*
|
|
4
|
+
* API:
|
|
5
|
+
* createState(initial, forceUpdate) -> [get, set]
|
|
6
|
+
* useState(initial) -> same shape but subscriptions supported
|
|
7
|
+
* useEffect(effect, deps, subscribeFn) -> returns a runner to be called by renderer or subscribers
|
|
8
|
+
* createStore(reducer, initial)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function createState(initialValue, forceUpdate = null) {
|
|
12
|
+
let value =
|
|
13
|
+
typeof initialValue === "function" ? initialValue() : initialValue;
|
|
14
|
+
const subs = new Set();
|
|
15
|
+
|
|
16
|
+
if (typeof forceUpdate === "function") subs.add(forceUpdate);
|
|
17
|
+
|
|
18
|
+
const get = () => value;
|
|
19
|
+
|
|
20
|
+
const set = (next) => {
|
|
21
|
+
const nextValue = typeof next === "function" ? next(value) : next;
|
|
22
|
+
if (nextValue !== value) {
|
|
23
|
+
value = nextValue;
|
|
24
|
+
subs.forEach((fn) => {
|
|
25
|
+
try {
|
|
26
|
+
fn(value);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
/* swallow */
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const subscribe = (fn) => {
|
|
35
|
+
subs.add(fn);
|
|
36
|
+
return () => subs.delete(fn);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return [get, set, subscribe];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// convenience alias (like React) - returns [get, set, subscribe]
|
|
43
|
+
export const useState = createState;
|
|
44
|
+
|
|
45
|
+
export function useEffect(effect, deps = undefined) {
|
|
46
|
+
let cleanup = null;
|
|
47
|
+
let prevDeps = undefined;
|
|
48
|
+
|
|
49
|
+
const run = () => {
|
|
50
|
+
if (cleanup) {
|
|
51
|
+
try {
|
|
52
|
+
cleanup();
|
|
53
|
+
} catch (e) {}
|
|
54
|
+
}
|
|
55
|
+
const ret = effect();
|
|
56
|
+
cleanup = typeof ret === "function" ? ret : null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const checkAndRun = (currentDeps = deps) => {
|
|
60
|
+
const changed =
|
|
61
|
+
!prevDeps ||
|
|
62
|
+
!currentDeps ||
|
|
63
|
+
currentDeps.some((d, i) => d !== prevDeps[i]);
|
|
64
|
+
if (changed) {
|
|
65
|
+
run();
|
|
66
|
+
prevDeps = currentDeps ? [...currentDeps] : null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// return a function that can be called by renderer or subscribers
|
|
71
|
+
return checkAndRun;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createStore(reducer, initialState) {
|
|
75
|
+
let state = initialState;
|
|
76
|
+
const listeners = new Set();
|
|
77
|
+
|
|
78
|
+
const getState = () => state;
|
|
79
|
+
|
|
80
|
+
const dispatch = (action) => {
|
|
81
|
+
const nextState =
|
|
82
|
+
typeof action === "function" ? action(state) : reducer(state, action);
|
|
83
|
+
if (nextState !== state) {
|
|
84
|
+
state = nextState;
|
|
85
|
+
listeners.forEach((fn) => {
|
|
86
|
+
try {
|
|
87
|
+
fn(state);
|
|
88
|
+
} catch (e) {}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const subscribe = (fn) => {
|
|
94
|
+
listeners.add(fn);
|
|
95
|
+
return () => listeners.delete(fn);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return { getState, dispatch, subscribe };
|
|
99
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { cleanStyle, normalizeWidgetArgs, omitProps } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
export function Semantics(propsOrChildren = {}, maybeChildren = undefined) {
|
|
4
|
+
const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
tag: props.as || "div",
|
|
8
|
+
props: {
|
|
9
|
+
...omitProps(props, [
|
|
10
|
+
"as",
|
|
11
|
+
"label",
|
|
12
|
+
"hint",
|
|
13
|
+
"role",
|
|
14
|
+
"hidden",
|
|
15
|
+
"liveRegion",
|
|
16
|
+
]),
|
|
17
|
+
role: props.role,
|
|
18
|
+
"aria-label": props.label,
|
|
19
|
+
"aria-description": props.hint,
|
|
20
|
+
"aria-hidden": props.hidden ? "true" : undefined,
|
|
21
|
+
"aria-live": props.liveRegion ? "polite" : undefined,
|
|
22
|
+
style: cleanStyle(props.style),
|
|
23
|
+
},
|
|
24
|
+
children,
|
|
25
|
+
key: props.key,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ExcludeSemantics(
|
|
30
|
+
propsOrChildren = {},
|
|
31
|
+
maybeChildren = undefined,
|
|
32
|
+
) {
|
|
33
|
+
const [props, children] = normalizeWidgetArgs(propsOrChildren, maybeChildren);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
tag: "div",
|
|
37
|
+
props: {
|
|
38
|
+
...omitProps(props),
|
|
39
|
+
"aria-hidden": "true",
|
|
40
|
+
style: cleanStyle(props.style),
|
|
41
|
+
},
|
|
42
|
+
children,
|
|
43
|
+
key: props.key,
|
|
44
|
+
};
|
|
45
|
+
}
|