@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.
@@ -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
+ }