@pyreon/react-compat 0.13.1 → 0.15.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.
package/README.md CHANGED
@@ -147,3 +147,23 @@ function Dashboard() {
147
147
  - **`useErrorBoundary`** -- alias for `onErrorCaptured`.
148
148
  - **`createSelector`** -- O(1) equality selector from `@pyreon/reactivity`.
149
149
  - **`createElement` / `h`**, **`Fragment`** -- JSX runtime.
150
+
151
+ ## Composing Pyreon framework components inside react-compat
152
+
153
+ Pyreon's framework components (`RouterView`, `PyreonUI`, `FormProvider`, `QueryClientProvider`, …) ship marked with `nativeCompat()` from `@pyreon/core` — react-compat's JSX runtime detects the marker and routes them through Pyreon's setup frame instead of the compat wrapper. **You don't need to do anything** for the 24 components shipped marked.
154
+
155
+ If you write your **own** Pyreon-flavored helper that uses `provide()` / `onMount()` / `onUnmount()` / `effect()` at component-body scope and use it in a react-compat app, mark it explicitly:
156
+
157
+ ```tsx
158
+ import { nativeCompat, provide, createContext } from '@pyreon/core'
159
+
160
+ const MyCtx = createContext<string>('default')
161
+
162
+ function MyProvider(props: { value: string; children?: unknown }) {
163
+ provide(MyCtx, props.value)
164
+ return props.children as never
165
+ }
166
+ nativeCompat(MyProvider) // ← required for compat-mode apps
167
+ ```
168
+
169
+ Without the marker, the wrapper relocates the body's render context and `provide()` lands in a torn-down context stack — descendants read the default. See [`packages/core/core/src/compat-marker.ts`](../../core/core/src/compat-marker.ts) for details.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"32dc1380-1","name":"jsx-runtime.ts"},{"uid":"32dc1380-3","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"32dc1380-1":{"renderedLength":186,"gzipLength":138,"brotliLength":0,"metaUid":"32dc1380-0"},"32dc1380-3":{"renderedLength":7034,"gzipLength":1959,"brotliLength":0,"metaUid":"32dc1380-2"}},"nodeMetas":{"32dc1380-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"index.js":"32dc1380-1"},"imported":[{"uid":"32dc1380-4"},{"uid":"32dc1380-5"}],"importedBy":[{"uid":"32dc1380-2"}]},"32dc1380-2":{"id":"/src/index.ts","moduleParts":{"index.js":"32dc1380-3"},"imported":[{"uid":"32dc1380-4"},{"uid":"32dc1380-5"},{"uid":"32dc1380-0"}],"importedBy":[],"isEntry":true},"32dc1380-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"32dc1380-2"},{"uid":"32dc1380-0"}]},"32dc1380-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"32dc1380-2"},{"uid":"32dc1380-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"4d7452bf-1","name":"jsx-runtime.ts"},{"uid":"4d7452bf-3","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"4d7452bf-1":{"renderedLength":186,"gzipLength":138,"brotliLength":0,"metaUid":"4d7452bf-0"},"4d7452bf-3":{"renderedLength":16657,"gzipLength":4670,"brotliLength":0,"metaUid":"4d7452bf-2"}},"nodeMetas":{"4d7452bf-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"index.js":"4d7452bf-1"},"imported":[{"uid":"4d7452bf-4"},{"uid":"4d7452bf-5"}],"importedBy":[{"uid":"4d7452bf-2"}]},"4d7452bf-2":{"id":"/src/index.ts","moduleParts":{"index.js":"4d7452bf-3"},"imported":[{"uid":"4d7452bf-4"},{"uid":"4d7452bf-5"},{"uid":"4d7452bf-0"}],"importedBy":[],"isEntry":true},"4d7452bf-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"4d7452bf-2"},{"uid":"4d7452bf-0"}]},"4d7452bf-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"4d7452bf-2"},{"uid":"4d7452bf-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"jsx-runtime.js","children":[{"name":"src","children":[{"uid":"243e8c0a-1","name":"jsx-runtime.ts"},{"uid":"243e8c0a-3","name":"jsx-dev-runtime.ts"}]}]}],"isRoot":true},"nodeParts":{"243e8c0a-1":{"renderedLength":2522,"gzipLength":870,"brotliLength":0,"metaUid":"243e8c0a-0"},"243e8c0a-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"243e8c0a-2"}},"nodeMetas":{"243e8c0a-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"jsx-runtime.js":"243e8c0a-1"},"imported":[{"uid":"243e8c0a-4"},{"uid":"243e8c0a-5"}],"importedBy":[{"uid":"243e8c0a-2"}]},"243e8c0a-2":{"id":"/src/jsx-dev-runtime.ts","moduleParts":{"jsx-runtime.js":"243e8c0a-3"},"imported":[{"uid":"243e8c0a-0"}],"importedBy":[],"isEntry":true},"243e8c0a-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"243e8c0a-0"}]},"243e8c0a-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"243e8c0a-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"jsx-runtime.js","children":[{"name":"src","children":[{"uid":"8146f520-1","name":"jsx-runtime.ts"},{"uid":"8146f520-3","name":"jsx-dev-runtime.ts"}]}]}],"isRoot":true},"nodeParts":{"8146f520-1":{"renderedLength":4794,"gzipLength":1456,"brotliLength":0,"metaUid":"8146f520-0"},"8146f520-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"8146f520-2"}},"nodeMetas":{"8146f520-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"jsx-runtime.js":"8146f520-1"},"imported":[{"uid":"8146f520-4"},{"uid":"8146f520-5"}],"importedBy":[{"uid":"8146f520-2"}]},"8146f520-2":{"id":"/src/jsx-dev-runtime.ts","moduleParts":{"jsx-runtime.js":"8146f520-3"},"imported":[{"uid":"8146f520-0"}],"importedBy":[],"isEntry":true},"8146f520-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"8146f520-0"}]},"8146f520-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"8146f520-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ErrorBoundary, Fragment, Portal, Suspense, createContext, h, h as createElement, h as h$1, lazy, useContext } from "@pyreon/core";
1
+ import { ErrorBoundary, Fragment, Portal, Suspense, createContext as createContext$1, createRef, h, h as createElement, h as h$1, lazy, nativeCompat, provide, useContext as useContext$1 } from "@pyreon/core";
2
2
  import { batch } from "@pyreon/reactivity";
3
3
 
4
4
  //#region src/jsx-runtime.ts
@@ -31,33 +31,50 @@ function depsChanged(a, b) {
31
31
  function useState(initial) {
32
32
  const ctx = requireCtx();
33
33
  const idx = getHookIndex();
34
- if (ctx.hooks.length <= idx) ctx.hooks.push(typeof initial === "function" ? initial() : initial);
35
- const value = ctx.hooks[idx];
36
- const setter = (v) => {
37
- const current = ctx.hooks[idx];
38
- const next = typeof v === "function" ? v(current) : v;
39
- if (Object.is(current, next)) return;
40
- ctx.hooks[idx] = next;
41
- ctx.scheduleRerender();
42
- };
43
- return [value, setter];
34
+ if (ctx.hooks.length <= idx) {
35
+ const entry = {
36
+ value: typeof initial === "function" ? initial() : initial,
37
+ setter: null
38
+ };
39
+ entry.setter = (v) => {
40
+ const current = entry.value;
41
+ const next = typeof v === "function" ? v(current) : v;
42
+ if (Object.is(current, next)) return;
43
+ entry.value = next;
44
+ ctx.scheduleRerender();
45
+ };
46
+ ctx.hooks.push(entry);
47
+ }
48
+ const entry = ctx.hooks[idx];
49
+ return [entry.value, entry.setter];
44
50
  }
45
51
  /**
46
52
  * React-compatible `useReducer` — returns `[state, dispatch]`.
53
+ * Supports the 3-argument form: `useReducer(reducer, initialArg, init)`.
47
54
  */
48
- function useReducer(reducer, initial) {
55
+ function useReducer(reducer, initialArg, init) {
49
56
  const ctx = requireCtx();
50
57
  const idx = getHookIndex();
51
- if (ctx.hooks.length <= idx) ctx.hooks.push(typeof initial === "function" ? initial() : initial);
52
- const state = ctx.hooks[idx];
53
- const dispatch = (action) => {
54
- const current = ctx.hooks[idx];
55
- const next = reducer(current, action);
56
- if (Object.is(current, next)) return;
57
- ctx.hooks[idx] = next;
58
- ctx.scheduleRerender();
59
- };
60
- return [state, dispatch];
58
+ if (ctx.hooks.length <= idx) {
59
+ let initial;
60
+ if (init) initial = init(initialArg);
61
+ else if (typeof initialArg === "function") initial = initialArg();
62
+ else initial = initialArg;
63
+ const entry = {
64
+ value: initial,
65
+ dispatch: null
66
+ };
67
+ entry.dispatch = (action) => {
68
+ const current = entry.value;
69
+ const next = reducer(current, action);
70
+ if (Object.is(current, next)) return;
71
+ entry.value = next;
72
+ ctx.scheduleRerender();
73
+ };
74
+ ctx.hooks.push(entry);
75
+ }
76
+ const entry = ctx.hooks[idx];
77
+ return [entry.value, entry.dispatch];
61
78
  }
62
79
  /**
63
80
  * React-compatible `useEffect` — runs after render when deps change.
@@ -107,6 +124,30 @@ function useLayoutEffect(fn, deps) {
107
124
  }
108
125
  }
109
126
  /**
127
+ * React-compatible `useInsertionEffect` — runs synchronously before layout effects.
128
+ * Intended for CSS-in-JS libraries to inject styles before DOM reads.
129
+ */
130
+ function useInsertionEffect(fn, deps) {
131
+ const ctx = requireCtx();
132
+ const idx = getHookIndex();
133
+ if (ctx.hooks.length <= idx) {
134
+ const entry = {
135
+ fn,
136
+ deps,
137
+ cleanup: void 0
138
+ };
139
+ ctx.hooks.push(entry);
140
+ ctx.pendingInsertionEffects.push(entry);
141
+ } else {
142
+ const entry = ctx.hooks[idx];
143
+ if (depsChanged(entry.deps, deps)) {
144
+ entry.fn = fn;
145
+ entry.deps = deps;
146
+ ctx.pendingInsertionEffects.push(entry);
147
+ }
148
+ }
149
+ }
150
+ /**
110
151
  * React-compatible `useMemo` — returns the cached value, recomputed when deps change.
111
152
  */
112
153
  function useMemo(fn, deps) {
@@ -145,6 +186,69 @@ function useRef(initial) {
145
186
  }
146
187
  return ctx.hooks[idx];
147
188
  }
189
+ const COMPAT_CTX = Symbol.for("pyreon:compat-ctx");
190
+ const COMPAT_CTX_BRAND = COMPAT_CTX;
191
+ /**
192
+ * React-compatible `createContext` — creates a context with a Provider that
193
+ * supports nested Providers (inner overrides outer for its subtree) and
194
+ * notifies all `useContext` consumers when its value changes.
195
+ */
196
+ function createContext(defaultValue) {
197
+ const pyreonCtx = createContext$1({
198
+ value: defaultValue,
199
+ subscribers: /* @__PURE__ */ new Set()
200
+ });
201
+ const defaultSubscribers = /* @__PURE__ */ new Set();
202
+ const Provider = (props) => {
203
+ const frame = {
204
+ value: props.value,
205
+ subscribers: /* @__PURE__ */ new Set()
206
+ };
207
+ provide(pyreonCtx, frame);
208
+ return () => {
209
+ const { value, children } = props;
210
+ if (!Object.is(frame.value, value)) {
211
+ frame.value = value;
212
+ for (const sub of frame.subscribers) sub();
213
+ }
214
+ return children ?? null;
215
+ };
216
+ };
217
+ nativeCompat(Provider);
218
+ return {
219
+ [COMPAT_CTX_BRAND]: true,
220
+ _defaultValue: defaultValue,
221
+ _pyreonCtx: pyreonCtx,
222
+ _subscribers: defaultSubscribers,
223
+ Provider
224
+ };
225
+ }
226
+ /**
227
+ * React-compatible `useContext` — reads the current context value and
228
+ * subscribes the calling component to future value changes.
229
+ *
230
+ * Reads from Pyreon's tree-scoped context stack (correct nesting) and
231
+ * subscribes to the nearest Provider's subscriber set for re-rendering.
232
+ *
233
+ * Works with both compat contexts (from this module's `createContext`) and
234
+ * Pyreon native contexts (from `@pyreon/core`).
235
+ */
236
+ function useContext(context) {
237
+ if (COMPAT_CTX in context) {
238
+ const frame = useContext$1(context._pyreonCtx);
239
+ const renderCtx = getCurrentCtx();
240
+ if (renderCtx) {
241
+ const idx = getHookIndex();
242
+ if (renderCtx.hooks.length <= idx) {
243
+ const sub = () => renderCtx.scheduleRerender();
244
+ frame.subscribers.add(sub);
245
+ renderCtx.hooks.push({ _contextUnsub: () => frame.subscribers.delete(sub) });
246
+ }
247
+ }
248
+ return frame.value;
249
+ }
250
+ return useContext$1(context);
251
+ }
148
252
  let _idCounter = 0;
149
253
  /**
150
254
  * React-compatible `useId` — returns a stable unique string per hook call.
@@ -155,26 +259,47 @@ function useId() {
155
259
  if (ctx.hooks.length <= idx) ctx.hooks.push(`:r${(_idCounter++).toString(36)}:`);
156
260
  return ctx.hooks[idx];
157
261
  }
262
+ function shallowEqual(a, b) {
263
+ const keysA = Object.keys(a);
264
+ const keysB = Object.keys(b);
265
+ if (keysA.length !== keysB.length) return false;
266
+ for (const k of keysA) if (!Object.is(a[k], b[k])) return false;
267
+ return true;
268
+ }
158
269
  /**
159
270
  * React-compatible `memo` — wraps a component to skip re-render when props
160
271
  * are shallowly equal.
272
+ *
273
+ * Each component INSTANCE gets its own props/result cache via a hook slot,
274
+ * so two `<MemoComp />` usages don't share memoization state.
161
275
  */
162
276
  function memo(component, areEqual) {
163
- const compare = areEqual ?? ((a, b) => {
164
- const keysA = Object.keys(a);
165
- const keysB = Object.keys(b);
166
- if (keysA.length !== keysB.length) return false;
167
- for (const k of keysA) if (!Object.is(a[k], b[k])) return false;
168
- return true;
169
- });
170
- let prevProps = null;
171
- let prevResult = null;
172
- return (props) => {
173
- if (prevProps !== null && compare(prevProps, props)) return prevResult;
174
- prevProps = props;
175
- prevResult = component(props);
176
- return prevResult;
277
+ const compare = areEqual ?? shallowEqual;
278
+ const MEMO_MARKER = Symbol.for("pyreon:memo");
279
+ let _fallbackPrevProps = null;
280
+ let _fallbackPrevResult = null;
281
+ const memoized = (props) => {
282
+ const ctx = getCurrentCtx();
283
+ if (ctx) {
284
+ const idx = getHookIndex();
285
+ if (ctx.hooks.length <= idx) ctx.hooks.push({
286
+ prevProps: null,
287
+ prevResult: null
288
+ });
289
+ const cache = ctx.hooks[idx];
290
+ if (cache.prevProps !== null && compare(cache.prevProps, props)) return cache.prevResult;
291
+ cache.prevProps = props;
292
+ cache.prevResult = component(props);
293
+ return cache.prevResult;
294
+ }
295
+ if (_fallbackPrevProps !== null && compare(_fallbackPrevProps, props)) return _fallbackPrevResult;
296
+ _fallbackPrevProps = props;
297
+ _fallbackPrevResult = component(props);
298
+ return _fallbackPrevResult;
177
299
  };
300
+ memoized[MEMO_MARKER] = true;
301
+ memoized.displayName = component.displayName || component.name || "Memo";
302
+ return memoized;
178
303
  }
179
304
  /**
180
305
  * React-compatible `useTransition` — no concurrent mode in Pyreon.
@@ -214,10 +339,12 @@ function createPortal(children, target) {
214
339
  * The render function receives (props, ref) — we merge ref into props.
215
340
  */
216
341
  function forwardRef(render) {
217
- return (props) => {
342
+ const forwarded = (props) => {
218
343
  const { ref, ...rest } = props;
219
344
  return render(rest, ref ?? null);
220
345
  };
346
+ forwarded.displayName = render.displayName || render.name || "ForwardRef";
347
+ return forwarded;
221
348
  }
222
349
  /**
223
350
  * React-compatible `cloneElement` — creates a new VNode with merged props.
@@ -242,40 +369,243 @@ function flattenChildren(children) {
242
369
  * React-compatible `Children` utilities for working with VNode children.
243
370
  */
244
371
  const Children = {
372
+ /**
373
+ * Iterate over children, calling `fn` for each non-null child.
374
+ */
245
375
  map(children, fn) {
246
376
  const flat = flattenChildren(children);
247
377
  const result = [];
378
+ let validIndex = 0;
248
379
  for (let i = 0; i < flat.length; i++) {
249
380
  const child = flat[i];
250
381
  if (child == null || child === true || child === false) continue;
251
- result.push(fn(child, i));
382
+ const mapped = fn(child, validIndex);
383
+ if (mapped && typeof mapped === "object" && "type" in mapped && "props" in mapped) {
384
+ const vnode = mapped;
385
+ if (vnode.key == null) vnode.key = `.${validIndex}`;
386
+ }
387
+ result.push(mapped);
388
+ validIndex++;
252
389
  }
253
390
  return result;
254
391
  },
392
+ /**
393
+ * Call `fn` for each non-null child (no return value).
394
+ */
255
395
  forEach(children, fn) {
256
396
  const flat = flattenChildren(children);
397
+ let validIndex = 0;
257
398
  for (let i = 0; i < flat.length; i++) {
258
399
  const child = flat[i];
259
400
  if (child == null || child === true || child === false) continue;
260
- fn(child, i);
401
+ fn(child, validIndex++);
261
402
  }
262
403
  },
404
+ /**
405
+ * Count non-null children.
406
+ */
263
407
  count(children) {
264
408
  const flat = flattenChildren(children);
265
409
  let count = 0;
266
410
  for (const child of flat) if (child != null && child !== true && child !== false) count++;
267
411
  return count;
268
412
  },
413
+ /**
414
+ * Convert children to a flat array.
415
+ */
269
416
  toArray(children) {
270
417
  return flattenChildren(children).filter((child) => child != null && child !== true && child !== false);
271
418
  },
419
+ /**
420
+ * Assert and return the only child. Throws if not exactly one child.
421
+ */
272
422
  only(children) {
273
423
  const arr = Children.toArray(children);
274
424
  if (arr.length !== 1) throw new Error("[Pyreon] Children.only expected exactly one child");
275
425
  return arr[0];
276
426
  }
277
427
  };
428
+ /**
429
+ * React-compatible `useSyncExternalStore` — subscribes to an external store.
430
+ * Re-subscribes automatically when the `subscribe` function identity changes.
431
+ */
432
+ function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
433
+ const ctx = requireCtx();
434
+ const idx = getHookIndex();
435
+ if (typeof window === "undefined" && getServerSnapshot) {
436
+ if (ctx.hooks.length <= idx) ctx.hooks.push({
437
+ subscribe,
438
+ unsubscribe: void 0,
439
+ snapshot: getServerSnapshot()
440
+ });
441
+ return ctx.hooks[idx].snapshot;
442
+ }
443
+ if (ctx.hooks.length <= idx) {
444
+ const snapshot = getSnapshot();
445
+ const entry = {
446
+ subscribe,
447
+ unsubscribe: void 0,
448
+ snapshot
449
+ };
450
+ const onChange = () => {
451
+ const next = getSnapshot();
452
+ if (!Object.is(entry.snapshot, next)) {
453
+ entry.snapshot = next;
454
+ ctx.scheduleRerender();
455
+ }
456
+ };
457
+ entry.unsubscribe = subscribe(onChange);
458
+ ctx.hooks.push(entry);
459
+ return snapshot;
460
+ }
461
+ const entry = ctx.hooks[idx];
462
+ if (entry.subscribe !== subscribe) {
463
+ if (entry.unsubscribe) entry.unsubscribe();
464
+ const onChange = () => {
465
+ const next = getSnapshot();
466
+ if (!Object.is(entry.snapshot, next)) {
467
+ entry.snapshot = next;
468
+ ctx.scheduleRerender();
469
+ }
470
+ };
471
+ entry.unsubscribe = subscribe(onChange);
472
+ entry.subscribe = subscribe;
473
+ }
474
+ entry.snapshot = getSnapshot();
475
+ return entry.snapshot;
476
+ }
477
+ const _promiseCache = /* @__PURE__ */ new WeakMap();
478
+ /**
479
+ * React-compatible `use` — reads a Context or suspends on a Promise.
480
+ * Can be called conditionally (unlike other hooks).
481
+ *
482
+ * IMPORTANT: Promises must have a stable identity across renders.
483
+ * Create promises outside the component or memoize them. Calling
484
+ * `use(fetch('/api'))` creates a new promise each render and will
485
+ * cause infinite suspension.
486
+ */
487
+ function use(resource) {
488
+ if (resource && typeof resource === "object" && COMPAT_CTX in resource) return useContext(resource);
489
+ if (resource && typeof resource === "object" && "id" in resource && "defaultValue" in resource) return useContext$1(resource);
490
+ const promise = resource;
491
+ let entry = _promiseCache.get(promise);
492
+ if (!entry) {
493
+ entry = { status: "pending" };
494
+ _promiseCache.set(promise, entry);
495
+ promise.then((value) => {
496
+ entry.status = "resolved";
497
+ entry.value = value;
498
+ }, (error) => {
499
+ entry.status = "rejected";
500
+ entry.error = error;
501
+ });
502
+ }
503
+ if (entry.status === "resolved") return entry.value;
504
+ if (entry.status === "rejected") throw entry.error;
505
+ throw promise;
506
+ }
507
+ /**
508
+ * React-compatible `useActionState` — manages async action state with pending indicator.
509
+ */
510
+ function useActionState(action, initialState) {
511
+ const [state, setState] = useState(initialState);
512
+ const [isPending, setIsPending] = useState(false);
513
+ const dispatch = (payload) => {
514
+ setIsPending(true);
515
+ const result = action(state, payload);
516
+ if (result instanceof Promise) result.then((next) => {
517
+ setState(next);
518
+ setIsPending(false);
519
+ });
520
+ else {
521
+ setState(result);
522
+ setIsPending(false);
523
+ }
524
+ };
525
+ return [
526
+ state,
527
+ dispatch,
528
+ isPending
529
+ ];
530
+ }
531
+ /**
532
+ * React-compatible `startTransition` — runs the callback synchronously.
533
+ * No concurrent mode in Pyreon, so transitions are immediate.
534
+ */
535
+ function startTransition(fn) {
536
+ fn();
537
+ }
538
+ /**
539
+ * React-compatible `isValidElement` — checks if a value is a VNode.
540
+ */
541
+ function isValidElement(value) {
542
+ return value != null && typeof value === "object" && "type" in value && "props" in value;
543
+ }
544
+ /**
545
+ * React-compatible `useDebugValue` — no-op in Pyreon (no React DevTools integration).
546
+ */
547
+ function useDebugValue(_value, _format) {}
548
+ /**
549
+ * React-compatible `flushSync` — runs the callback synchronously.
550
+ *
551
+ * BEHAVIORAL DIFFERENCE: In Pyreon's compat model, state updates are
552
+ * batched via microtask. flushSync runs the callback and returns its
553
+ * result, but the DOM updates triggered by state changes inside the
554
+ * callback still fire asynchronously. For DOM measurement after state
555
+ * updates, use `await act(() => setState(...))` in tests, or
556
+ * `requestAnimationFrame` in production code.
557
+ */
558
+ function flushSync(fn) {
559
+ return fn();
560
+ }
561
+ /**
562
+ * React-compatible `act` — flushes pending microtasks for testing.
563
+ */
564
+ async function act(fn) {
565
+ const result = fn();
566
+ if (result instanceof Promise) await result;
567
+ await new Promise((r) => queueMicrotask(r));
568
+ await new Promise((r) => queueMicrotask(r));
569
+ }
570
+ const version = "19.0.0-pyreon";
571
+ /**
572
+ * React-compatible `StrictMode` — pass-through in Pyreon (no double-invoke behavior).
573
+ */
574
+ function StrictMode(props) {
575
+ return props.children ?? null;
576
+ }
577
+ /**
578
+ * React-compatible `Profiler` — pass-through in Pyreon (no profiling integration).
579
+ */
580
+ function Profiler(props) {
581
+ return props.children ?? null;
582
+ }
583
+ /**
584
+ * React-compatible `Component` class stub.
585
+ * Class components are not fully supported — use function components with hooks.
586
+ */
587
+ var Component = class {
588
+ props;
589
+ state;
590
+ constructor(props) {
591
+ this.props = props;
592
+ this.state = {};
593
+ }
594
+ setState(_partial) {
595
+ console.warn("[Pyreon] Class component setState is not supported. Use function components with hooks.");
596
+ }
597
+ forceUpdate() {
598
+ console.warn("[Pyreon] Class component forceUpdate is not supported. Use function components with hooks.");
599
+ }
600
+ render() {
601
+ return null;
602
+ }
603
+ };
604
+ /**
605
+ * React-compatible `PureComponent` class stub.
606
+ */
607
+ var PureComponent = class extends Component {};
278
608
 
279
609
  //#endregion
280
- export { Children, ErrorBoundary, Fragment, Suspense, batch, cloneElement, createContext, createElement, createPortal, forwardRef, h, lazy, memo, useCallback, useContext, useDeferredValue, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useTransition };
610
+ export { Children, Component, ErrorBoundary, Fragment, Profiler, PureComponent, StrictMode, Suspense, act, batch, cloneElement, createContext, createElement, createPortal, createRef, flushSync, forwardRef, h, isValidElement, lazy, memo, startTransition, use, useActionState, useCallback, useContext, useDebugValue, useDeferredValue, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore, useTransition, version };
281
611
  //# sourceMappingURL=index.js.map
@@ -1,16 +1,24 @@
1
- import { Fragment, h } from "@pyreon/core";
1
+ import { Fragment, h, isNativeCompat, onUnmount } from "@pyreon/core";
2
2
  import { signal } from "@pyreon/reactivity";
3
3
 
4
4
  //#region src/jsx-runtime.ts
5
5
  let _currentCtx = null;
6
6
  let _hookIndex = 0;
7
+ let _expectedHookCount = -1;
7
8
  function beginRender(ctx) {
8
9
  _currentCtx = ctx;
9
10
  _hookIndex = 0;
11
+ ctx.pendingInsertionEffects = [];
10
12
  ctx.pendingEffects = [];
11
13
  ctx.pendingLayoutEffects = [];
14
+ if (ctx._hookCount !== void 0) _expectedHookCount = ctx._hookCount;
15
+ else _expectedHookCount = -1;
12
16
  }
13
17
  function endRender() {
18
+ if (_currentCtx) {
19
+ if (process.env.NODE_ENV !== "production" && _expectedHookCount !== -1 && _hookIndex !== _expectedHookCount) console.error(`[Pyreon] Hook count changed between renders (expected ${_expectedHookCount}, got ${_hookIndex}). This usually means a hook is called conditionally. Hooks must be called in the same order every render.`);
20
+ _currentCtx._hookCount = _hookIndex;
21
+ }
14
22
  _currentCtx = null;
15
23
  _hookIndex = 0;
16
24
  }
@@ -40,6 +48,7 @@ function wrapCompatComponent(reactComponent) {
40
48
  const ctx = {
41
49
  hooks: [],
42
50
  scheduleRerender: () => {},
51
+ pendingInsertionEffects: [],
43
52
  pendingEffects: [],
44
53
  pendingLayoutEffects: [],
45
54
  unmounted: false
@@ -54,13 +63,32 @@ function wrapCompatComponent(reactComponent) {
54
63
  if (!ctx.unmounted) version.set(version.peek() + 1);
55
64
  });
56
65
  };
66
+ onUnmount(() => {
67
+ ctx.unmounted = true;
68
+ for (const hook of ctx.hooks) {
69
+ if (hook && typeof hook === "object" && "cleanup" in hook) {
70
+ const entry = hook;
71
+ if (typeof entry.cleanup === "function") entry.cleanup();
72
+ }
73
+ if (hook && typeof hook === "object" && "unsubscribe" in hook) {
74
+ const sub = hook;
75
+ if (typeof sub.unsubscribe === "function") sub.unsubscribe();
76
+ }
77
+ if (hook && typeof hook === "object" && "_contextUnsub" in hook) {
78
+ const ctxHook = hook;
79
+ if (typeof ctxHook._contextUnsub === "function") ctxHook._contextUnsub();
80
+ }
81
+ }
82
+ });
57
83
  return () => {
58
84
  version();
59
85
  beginRender(ctx);
60
86
  const result = reactComponent(props);
87
+ const insertionEffects = ctx.pendingInsertionEffects;
61
88
  const layoutEffects = ctx.pendingLayoutEffects;
62
89
  const effects = ctx.pendingEffects;
63
90
  endRender();
91
+ runLayoutEffects(insertionEffects);
64
92
  runLayoutEffects(layoutEffects);
65
93
  scheduleEffects(ctx, effects);
66
94
  return result;
@@ -75,10 +103,14 @@ function jsx(type, props, key) {
75
103
  ...rest,
76
104
  key
77
105
  } : rest;
78
- if (typeof type === "function") return h(wrapCompatComponent(type), children !== void 0 ? {
79
- ...propsWithKey,
80
- children
81
- } : propsWithKey);
106
+ if (typeof type === "function") {
107
+ const componentProps = children !== void 0 ? {
108
+ ...propsWithKey,
109
+ children
110
+ } : propsWithKey;
111
+ if (isNativeCompat(type)) return h(type, componentProps);
112
+ return h(wrapCompatComponent(type), componentProps);
113
+ }
82
114
  const childArray = children === void 0 ? [] : Array.isArray(children) ? children : [children];
83
115
  if (typeof type === "string") {
84
116
  if (propsWithKey.className !== void 0) {
@@ -89,6 +121,26 @@ function jsx(type, props, key) {
89
121
  propsWithKey.for = propsWithKey.htmlFor;
90
122
  delete propsWithKey.htmlFor;
91
123
  }
124
+ if ((type === "input" || type === "textarea" || type === "select") && propsWithKey.onChange !== void 0) {
125
+ if (propsWithKey.onInput === void 0) propsWithKey.onInput = propsWithKey.onChange;
126
+ delete propsWithKey.onChange;
127
+ }
128
+ if (propsWithKey.autoFocus !== void 0) {
129
+ propsWithKey.autofocus = propsWithKey.autoFocus;
130
+ delete propsWithKey.autoFocus;
131
+ }
132
+ if (type === "input" || type === "textarea") {
133
+ if (propsWithKey.defaultValue !== void 0 && propsWithKey.value === void 0) {
134
+ propsWithKey.value = propsWithKey.defaultValue;
135
+ delete propsWithKey.defaultValue;
136
+ }
137
+ if (propsWithKey.defaultChecked !== void 0 && propsWithKey.checked === void 0) {
138
+ propsWithKey.checked = propsWithKey.defaultChecked;
139
+ delete propsWithKey.defaultChecked;
140
+ }
141
+ }
142
+ delete propsWithKey.suppressHydrationWarning;
143
+ delete propsWithKey.suppressContentEditableWarning;
92
144
  }
93
145
  return h(type, propsWithKey, ...childArray);
94
146
  }