jsx-framework-test-pb 0.3.2 → 0.4.0

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,15 @@
1
+ import type { FC, ElementChild } from './types.js';
2
+ export interface Context<T> {
3
+ Provider: FC<{
4
+ value: T;
5
+ children?: ElementChild | ElementChild[];
6
+ }>;
7
+ /** @internal */ _id: symbol;
8
+ /** @internal */ _defaultValue: T;
9
+ }
10
+ export declare function createContext<T>(defaultValue: T): Context<T>;
11
+ export declare function useContext<T>(context: Context<T>): T;
12
+ /** @internal — called by createElement when processing CONTEXT_PROVIDER_TYPE */
13
+ export declare function pushContextValue(id: symbol, value: unknown): void;
14
+ /** @internal — called by createElement after children are rendered */
15
+ export declare function popContextValue(id: symbol): void;
@@ -0,0 +1,29 @@
1
+ import { ELEMENT_TYPE, CONTEXT_PROVIDER_TYPE } from './utils/constants.js';
2
+ // Stack per context id — supports nested providers
3
+ const contextStacks = new Map();
4
+ export function createContext(defaultValue) {
5
+ const id = Symbol('context');
6
+ const Provider = ({ value, children }) => ({
7
+ $$typeof: ELEMENT_TYPE,
8
+ type: CONTEXT_PROVIDER_TYPE,
9
+ props: { _contextId: id, _value: value, children },
10
+ key: null,
11
+ });
12
+ return { Provider, _id: id, _defaultValue: defaultValue };
13
+ }
14
+ export function useContext(context) {
15
+ const stack = contextStacks.get(context._id);
16
+ return stack && stack.length > 0 ? stack[stack.length - 1] : context._defaultValue;
17
+ }
18
+ /** @internal — called by createElement when processing CONTEXT_PROVIDER_TYPE */
19
+ export function pushContextValue(id, value) {
20
+ const stack = contextStacks.get(id) ?? [];
21
+ contextStacks.set(id, [...stack, value]);
22
+ }
23
+ /** @internal — called by createElement after children are rendered */
24
+ export function popContextValue(id) {
25
+ const stack = contextStacks.get(id);
26
+ if (stack && stack.length > 0) {
27
+ contextStacks.set(id, stack.slice(0, -1));
28
+ }
29
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,10 @@ export { render, unmount } from './runtime/index.js';
2
2
  export { renderToString, hydrate } from './ssr/index.js';
3
3
  export { Fragment } from './jsx-runtime.js';
4
4
  export { isElement } from './utils/index.js';
5
+ export { isState } from './reactivity/index.js';
5
6
  export { state, computed, effect } from './reactivity/index.js';
6
7
  export type { StateObject } from './reactivity/index.js';
7
- export type { Element, ElementProps, ElementChild, DevElement, FC, HTMLAttributes, CSSProperties, ValidElement } from './types';
8
+ export { createContext, useContext } from './context.js';
9
+ export type { Context } from './context.js';
10
+ export { createPortal } from './portal.js';
11
+ export type { Element, ElementProps, ElementChild, DevElement, FC, HTMLAttributes, CSSProperties, ValidElement, } from './types.js';
package/dist/index.js CHANGED
@@ -2,4 +2,7 @@ export { render, unmount } from './runtime/index.js';
2
2
  export { renderToString, hydrate } from './ssr/index.js';
3
3
  export { Fragment } from './jsx-runtime.js';
4
4
  export { isElement } from './utils/index.js';
5
+ export { isState } from './reactivity/index.js';
5
6
  export { state, computed, effect } from './reactivity/index.js';
7
+ export { createContext, useContext } from './context.js';
8
+ export { createPortal } from './portal.js';
@@ -0,0 +1,9 @@
1
+ import type { Element, ElementChild } from './types.js';
2
+ /**
3
+ * Renders `children` into a different DOM node (`container`) while keeping
4
+ * them logically part of the component tree for cleanup purposes.
5
+ *
6
+ * @example
7
+ * return createPortal(<PopoverContent />, document.body);
8
+ */
9
+ export declare function createPortal(children: ElementChild | ElementChild[], container: HTMLElement): Element;
package/dist/portal.js ADDED
@@ -0,0 +1,16 @@
1
+ import { ELEMENT_TYPE, PORTAL_TYPE } from './utils/constants.js';
2
+ /**
3
+ * Renders `children` into a different DOM node (`container`) while keeping
4
+ * them logically part of the component tree for cleanup purposes.
5
+ *
6
+ * @example
7
+ * return createPortal(<PopoverContent />, document.body);
8
+ */
9
+ export function createPortal(children, container) {
10
+ return {
11
+ $$typeof: ELEMENT_TYPE,
12
+ type: PORTAL_TYPE,
13
+ props: { children, _portalContainer: container },
14
+ key: null,
15
+ };
16
+ }
@@ -1,2 +1,3 @@
1
1
  export declare function cleanupNode(node: Node): void;
2
+ export declare function cleanupTree(node: Node): void;
2
3
  export declare function createElement(element: any): Node | null;
@@ -1,5 +1,6 @@
1
- import { isElement, isValidChild, FRAGMENT_TYPE } from '../utils';
2
- import { effect } from '../reactivity';
1
+ import { isElement, isValidChild, FRAGMENT_TYPE, CONTEXT_PROVIDER_TYPE, PORTAL_TYPE } from '../utils/index.js';
2
+ import { effect } from '../reactivity/index.js';
3
+ import { pushContextValue, popContextValue } from '../context.js';
3
4
  // Track cleanups per node for proper disposal
4
5
  const nodeCleanups = new WeakMap();
5
6
  function trackCleanup(node, cleanup) {
@@ -17,26 +18,59 @@ export function cleanupNode(node) {
17
18
  cleanups.clear();
18
19
  }
19
20
  }
21
+ export function cleanupTree(node) {
22
+ cleanupNode(node);
23
+ node.childNodes.forEach(child => cleanupTree(child));
24
+ }
20
25
  export function createElement(element) {
21
- if (!isValidChild(element)) {
26
+ if (!isValidChild(element))
22
27
  return null;
23
- }
24
- if (typeof element === 'function') {
28
+ if (typeof element === 'function')
25
29
  return createReactiveChild(element);
26
- }
27
30
  if (typeof element === 'string' || typeof element === 'number') {
28
31
  return document.createTextNode(String(element));
29
32
  }
30
- if (Array.isArray(element)) {
33
+ if (Array.isArray(element))
31
34
  return createFragment(element);
32
- }
33
- if (!isElement(element)) {
35
+ if (!isElement(element))
34
36
  return null;
35
- }
36
37
  const { type, props } = element;
37
38
  if (type === FRAGMENT_TYPE) {
38
39
  return createFragment(getChildren(props));
39
40
  }
41
+ if (type === CONTEXT_PROVIDER_TYPE) {
42
+ const { _contextId, _value } = props;
43
+ pushContextValue(_contextId, _value);
44
+ const fragment = createFragment(getChildren(props));
45
+ popContextValue(_contextId);
46
+ return fragment;
47
+ }
48
+ if (type === PORTAL_TYPE) {
49
+ const { _portalContainer } = props;
50
+ const children = getChildren(props);
51
+ const portalNodes = [];
52
+ children.forEach((child) => {
53
+ const node = createElement(child);
54
+ if (!node)
55
+ return;
56
+ if (node instanceof DocumentFragment) {
57
+ portalNodes.push(...Array.from(node.childNodes));
58
+ }
59
+ else {
60
+ portalNodes.push(node);
61
+ }
62
+ _portalContainer.appendChild(node);
63
+ });
64
+ // Return an anchor in the current tree position — cleanup removes portal nodes
65
+ const anchor = document.createTextNode('');
66
+ trackCleanup(anchor, () => {
67
+ portalNodes.forEach(node => {
68
+ cleanupTree(node);
69
+ node.parentNode?.removeChild(node);
70
+ });
71
+ });
72
+ return anchor;
73
+ }
40
74
  if (typeof type === 'function') {
41
75
  const result = type(props);
42
76
  return createElement(result);
@@ -47,14 +81,11 @@ export function createElement(element) {
47
81
  return null;
48
82
  }
49
83
  function createReactiveChild(fn) {
50
- // Use an empty text node as a stable anchor point (invisible in DOM)
51
84
  const anchor = document.createTextNode('');
52
- // Track actual inserted nodes (not the DocumentFragment which empties on insert)
53
85
  let currentNodes = [];
54
86
  let isFirstRun = true;
55
87
  const cleanup = effect(() => {
56
88
  const value = fn();
57
- // Clean up previous nodes (skip on first run)
58
89
  if (!isFirstRun) {
59
90
  currentNodes.forEach(node => {
60
91
  cleanupTree(node);
@@ -62,7 +93,6 @@ function createReactiveChild(fn) {
62
93
  });
63
94
  currentNodes = [];
64
95
  }
65
- // Handle different return types
66
96
  let newNode = null;
67
97
  if (value == null || value === false) {
68
98
  newNode = null;
@@ -76,7 +106,6 @@ function createReactiveChild(fn) {
76
106
  else {
77
107
  newNode = document.createTextNode(String(value));
78
108
  }
79
- // Track the actual nodes before a DocumentFragment empties on insertion
80
109
  if (newNode) {
81
110
  if (newNode instanceof DocumentFragment) {
82
111
  currentNodes = Array.from(newNode.childNodes);
@@ -85,7 +114,6 @@ function createReactiveChild(fn) {
85
114
  currentNodes = [newNode];
86
115
  }
87
116
  }
88
- // Insert new node after anchor (skip on first run - handled by fragment)
89
117
  if (!isFirstRun && newNode && anchor.parentNode) {
90
118
  anchor.parentNode.insertBefore(newNode, anchor.nextSibling);
91
119
  }
@@ -98,20 +126,14 @@ function createReactiveChild(fn) {
98
126
  node.parentNode?.removeChild(node);
99
127
  });
100
128
  });
101
- // Return fragment with anchor and initial nodes
102
129
  const fragment = document.createDocumentFragment();
103
130
  fragment.appendChild(anchor);
104
131
  currentNodes.forEach(node => fragment.appendChild(node));
105
132
  return fragment;
106
133
  }
107
- function cleanupTree(node) {
108
- cleanupNode(node);
109
- node.childNodes.forEach(child => cleanupTree(child));
110
- }
111
134
  function createFragment(children) {
112
135
  const fragment = document.createDocumentFragment();
113
- const childArray = Array.isArray(children) ? children : [children];
114
- childArray.forEach(child => {
136
+ children.forEach(child => {
115
137
  const node = createElement(child);
116
138
  if (node)
117
139
  fragment.appendChild(node);
@@ -120,65 +142,104 @@ function createFragment(children) {
120
142
  }
121
143
  function getChildren(props) {
122
144
  const children = props.children;
123
- return Array.isArray(children) ? children : children ? [children] : [];
145
+ return Array.isArray(children) ? children : children != null ? [children] : [];
124
146
  }
125
147
  function createHTMLElement(type, props) {
126
148
  const element = document.createElement(type);
127
149
  Object.keys(props).forEach(key => {
150
+ const value = props[key];
128
151
  if (key === 'children') {
129
- appendChildren(element, props.children);
152
+ appendChildren(element, value);
130
153
  }
131
- else if (key.startsWith('on') && typeof props[key] === 'function') {
132
- attachEvent(element, key, props[key]);
154
+ else if (key.startsWith('on') && typeof value === 'function') {
155
+ attachEvent(element, key, value);
133
156
  }
134
- else if (key === 'className') {
135
- handleReactiveProp(element, 'class', props[key]);
157
+ else if (key === 'style' && typeof value === 'object' && value !== null) {
158
+ applyStyles(element, value);
136
159
  }
137
- else if (key === 'style' && typeof props[key] === 'object') {
138
- applyStyles(element, props[key]);
160
+ else if (key === 'key') {
161
+ // internal — skip
139
162
  }
140
- else if (key !== 'key' && props[key] != null) {
141
- handleReactiveProp(element, key, props[key]);
163
+ else if (value != null) {
164
+ applyProp(element, key, value);
142
165
  }
143
166
  });
144
167
  return element;
145
168
  }
146
- function handleReactiveProp(element, attrName, value) {
147
- if (typeof value === 'function' && !attrName.startsWith('on')) {
148
- const cleanup = effect(() => {
149
- element.setAttribute(attrName, String(value()));
150
- });
169
+ function applyProp(element, key, value) {
170
+ const apply = (v) => {
171
+ if (v === false || v === null || v === undefined) {
172
+ // Remove boolean attributes; skip non-attribute falsy values
173
+ const attrName = propToAttr(key);
174
+ element.removeAttribute(attrName);
175
+ return;
176
+ }
177
+ switch (key) {
178
+ case 'className':
179
+ element.setAttribute('class', String(v));
180
+ break;
181
+ case 'htmlFor':
182
+ element.htmlFor = String(v);
183
+ break;
184
+ case 'tabIndex':
185
+ element.tabIndex = Number(v);
186
+ break;
187
+ case 'disabled':
188
+ case 'checked':
189
+ case 'selected':
190
+ case 'readOnly':
191
+ case 'multiple':
192
+ case 'required':
193
+ case 'autoFocus':
194
+ case 'hidden':
195
+ case 'controls':
196
+ case 'autoPlay':
197
+ case 'loop':
198
+ case 'muted':
199
+ // Set as DOM property (boolean-like)
200
+ element[key] = Boolean(v);
201
+ break;
202
+ default: {
203
+ const attrName = propToAttr(key);
204
+ element.setAttribute(attrName, v === true ? '' : String(v));
205
+ }
206
+ }
207
+ };
208
+ if (typeof value === 'function') {
209
+ const cleanup = effect(() => apply(value()));
151
210
  trackCleanup(element, cleanup);
152
211
  }
153
212
  else {
154
- element.setAttribute(attrName, String(value));
213
+ apply(value);
155
214
  }
156
215
  }
216
+ /** Convert camelCase prop names to lowercase attribute names where needed */
217
+ function propToAttr(key) {
218
+ // Already lowercase or data-/aria- attributes
219
+ if (key === key.toLowerCase() || key.startsWith('data-') || key.startsWith('aria-'))
220
+ return key;
221
+ // camelCase → kebab-case for HTML attributes
222
+ return key.replace(/([A-Z])/g, c => `-${c.toLowerCase()}`);
223
+ }
157
224
  function appendChildren(parent, children) {
158
225
  const childArray = Array.isArray(children) ? children : [children];
159
226
  childArray.forEach(child => {
160
- const childNode = createElement(child);
161
- if (childNode) {
162
- parent.appendChild(childNode);
163
- }
227
+ const node = createElement(child);
228
+ if (node)
229
+ parent.appendChild(node);
164
230
  });
165
231
  }
166
232
  function attachEvent(element, key, handler) {
167
233
  const eventName = key.substring(2).toLowerCase();
168
234
  const listener = handler;
169
235
  element.addEventListener(eventName, listener);
170
- // Track for cleanup on unmount
171
- trackCleanup(element, () => {
172
- element.removeEventListener(eventName, listener);
173
- });
236
+ trackCleanup(element, () => element.removeEventListener(eventName, listener));
174
237
  }
175
238
  function applyStyles(element, styles) {
176
239
  Object.keys(styles).forEach(key => {
177
240
  const value = styles[key];
178
241
  if (typeof value === 'function') {
179
- const cleanup = effect(() => {
180
- element.style[key] = value();
181
- });
242
+ const cleanup = effect(() => { element.style[key] = value(); });
182
243
  trackCleanup(element, cleanup);
183
244
  }
184
245
  else {
@@ -1,4 +1,4 @@
1
- export type { FiberNode } from './types';
2
- export { reconcile } from './reconciler';
3
- export { render, unmount } from './render';
4
- export { createElement, cleanupNode } from './createElement';
1
+ export type { FiberNode } from './types.js';
2
+ export { reconcile } from './reconciler.js';
3
+ export { render, unmount } from './render.js';
4
+ export { createElement, cleanupNode, cleanupTree } from './createElement.js';
@@ -1,3 +1,3 @@
1
- export { reconcile } from './reconciler';
2
- export { render, unmount } from './render';
3
- export { createElement, cleanupNode } from './createElement';
1
+ export { reconcile } from './reconciler.js';
2
+ export { render, unmount } from './render.js';
3
+ export { createElement, cleanupNode, cleanupTree } from './createElement.js';
@@ -1,2 +1,2 @@
1
- import type { FiberNode } from "./types";
1
+ import type { FiberNode } from "./types.js";
2
2
  export declare function reconcile(parent: HTMLElement, newElements: any[] | any, oldFibers?: FiberNode[]): FiberNode[];
@@ -1,6 +1,6 @@
1
- import { createElement, cleanupNode } from "./createElement";
2
- import { isElement } from "../utils";
3
- import { effect } from "../reactivity";
1
+ import { createElement, cleanupTree } from "./createElement.js";
2
+ import { isElement } from "../utils/index.js";
3
+ import { effect } from "../reactivity/index.js";
4
4
  function canReuse(oldElement, newElement) {
5
5
  if (typeof oldElement !== typeof newElement)
6
6
  return false;
@@ -40,10 +40,6 @@ function setAttr(el, name, value) {
40
40
  function getInsertionPoint(parent, targetIndex) {
41
41
  return parent.childNodes.item(targetIndex) ?? null;
42
42
  }
43
- function cleanupTree(node) {
44
- cleanupNode(node);
45
- node.childNodes.forEach(child => cleanupTree(child));
46
- }
47
43
  export function reconcile(parent, newElements, oldFibers = []) {
48
44
  const newFibers = [];
49
45
  const newElementsArray = Array.isArray(newElements) ? newElements : [newElements];
@@ -1,6 +1,6 @@
1
- import { reconcile } from './reconciler';
2
- import { isElement } from '../utils';
3
- import { cleanupNode } from './createElement';
1
+ import { reconcile } from './reconciler.js';
2
+ import { isElement } from '../utils/index.js';
3
+ import { cleanupTree } from './createElement.js';
4
4
  const instances = new WeakMap();
5
5
  export function render(element, container) {
6
6
  if (!container)
@@ -17,14 +17,9 @@ export function render(element, container) {
17
17
  const newFibers = reconcile(container, children, oldFibers);
18
18
  instances.set(container, newFibers);
19
19
  }
20
- function cleanupTree(node) {
21
- cleanupNode(node);
22
- node.childNodes.forEach(child => cleanupTree(child));
23
- }
24
20
  export function unmount(container) {
25
21
  if (!container)
26
22
  return;
27
- // Clean up all nodes recursively before removing
28
23
  cleanupTree(container);
29
24
  container.replaceChildren();
30
25
  instances.delete(container);
@@ -1,2 +1,4 @@
1
1
  export declare const ELEMENT_TYPE: unique symbol;
2
2
  export declare const FRAGMENT_TYPE: unique symbol;
3
+ export declare const CONTEXT_PROVIDER_TYPE: unique symbol;
4
+ export declare const PORTAL_TYPE: unique symbol;
@@ -1,2 +1,4 @@
1
1
  export const ELEMENT_TYPE = Symbol.for('uraniyum.element');
2
2
  export const FRAGMENT_TYPE = Symbol.for('uraniyum.fragment');
3
+ export const CONTEXT_PROVIDER_TYPE = Symbol.for('uraniyum.contextProvider');
4
+ export const PORTAL_TYPE = Symbol.for('uraniyum.portal');
@@ -1,2 +1,2 @@
1
- export { ELEMENT_TYPE, FRAGMENT_TYPE } from './constants';
2
- export { isElement, isValidChild } from './guards';
1
+ export { ELEMENT_TYPE, FRAGMENT_TYPE, CONTEXT_PROVIDER_TYPE, PORTAL_TYPE, } from './constants.js';
2
+ export { isElement, isValidChild } from './guards.js';
@@ -1,2 +1,2 @@
1
- export { ELEMENT_TYPE, FRAGMENT_TYPE } from './constants';
2
- export { isElement, isValidChild } from './guards';
1
+ export { ELEMENT_TYPE, FRAGMENT_TYPE, CONTEXT_PROVIDER_TYPE, PORTAL_TYPE, } from './constants.js';
2
+ export { isElement, isValidChild } from './guards.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsx-framework-test-pb",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",