@unsetsoft/ryunixjs 1.2.5-canary.11 → 1.2.5-canary.13

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.
@@ -83,10 +83,6 @@ function nextValidSibling$1(node) {
83
83
  return next;
84
84
  }
85
85
 
86
- /**
87
- * @param {string | number | boolean} text
88
- * @returns {RyunixTextElement}
89
- */
90
86
  const createTextElement = (text) => {
91
87
  return {
92
88
  type: RYUNIX_TYPES.TEXT_ELEMENT,
@@ -96,13 +92,6 @@ const createTextElement = (text) => {
96
92
  },
97
93
  };
98
94
  };
99
- /**
100
- * Create a virtual DOM element.
101
- * @param {string | symbol | RyunixComponent} type
102
- * @param {RyunixProps | null} [props]
103
- * @param {...RyunixNode} children
104
- * @returns {RyunixElement}
105
- */
106
95
  const createElement = (type, props, ...children) => {
107
96
  const safeProps = props || {};
108
97
  let rawChildren = children;
@@ -114,7 +103,6 @@ const createElement = (type, props, ...children) => {
114
103
  rawChildren = rawChildren
115
104
  .flat()
116
105
  .filter((child) => child != null && child !== false && child !== true);
117
- /** @type {RyunixNode[]} */
118
106
  const normalizedChildren = [];
119
107
  let currentText = '';
120
108
  for (const child of rawChildren) {
@@ -140,22 +128,12 @@ const createElement = (type, props, ...children) => {
140
128
  },
141
129
  };
142
130
  };
143
- /**
144
- * @param {{ children?: RyunixNode | RyunixNode[] }} props
145
- * @returns {RyunixElement}
146
- */
147
131
  const Fragment = (props) => {
148
132
  const children = Array.isArray(props.children)
149
133
  ? props.children
150
134
  : [props.children];
151
135
  return createElement(RYUNIX_TYPES.RYUNIX_FRAGMENT, {}, ...children);
152
136
  };
153
- /**
154
- * @param {RyunixElement} element
155
- * @param {RyunixProps} [props]
156
- * @param {...RyunixNode} children
157
- * @returns {RyunixElement}
158
- */
159
137
  const cloneElement = (element, props = {}, ...children) => {
160
138
  if (!element || !is.object(element)) {
161
139
  throw new Error('cloneElement requires a valid element');
@@ -169,32 +147,13 @@ const isValidElement = (object) => {
169
147
  object.props !== undefined);
170
148
  };
171
149
 
172
- /** @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber */
173
- /** @typedef {import('../types/internal.js').RyunixHook} RyunixHook */
174
- /**
175
- * @param {string} key
176
- * @returns {boolean}
177
- */
178
150
  const isEvent = (key) => key.startsWith('on');
179
- /**
180
- * @param {string} key
181
- * @returns {boolean}
182
- */
183
- const isProperty = (key) => key !== STRINGS.CHILDREN && !isEvent(key);
184
- /**
185
- * @param {Record<string, unknown>} prev
186
- * @param {Record<string, unknown>} next
187
- */
188
- const isNew = (prev, next) => /** @param {string} key */ (key) => {
151
+ const RESERVED_DOM_PROPS = new Set(['key', 'ref', STRINGS.CHILDREN]);
152
+ const isProperty = (key) => !RESERVED_DOM_PROPS.has(key) && !isEvent(key);
153
+ const isNew = (prev, next) => (key) => {
189
154
  return !Object.is(prev[key], next[key]);
190
155
  };
191
- /**
192
- * @param {Record<string, unknown>} next
193
- */
194
- const isGone = (next) => /** @param {string} key */ (key) => !(key in next);
195
- /**
196
- * @param {RyunixFiber} fiber
197
- */
156
+ const isGone = (next) => (key) => !(key in next);
198
157
  const cancelEffects = (fiber) => {
199
158
  if (!fiber?.hooks?.length)
200
159
  return;
@@ -213,9 +172,6 @@ const cancelEffects = (fiber) => {
213
172
  }
214
173
  });
215
174
  };
216
- /**
217
- * @param {RyunixFiber} fiber
218
- */
219
175
  const cancelEffectsDeep = (fiber) => {
220
176
  if (!fiber)
221
177
  return;
@@ -327,23 +283,9 @@ function getCurrentPriority() {
327
283
  return currentPriority;
328
284
  }
329
285
 
330
- /**
331
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
332
- * @typedef {import('../types/internal.js').RyunixDomElement} RyunixDomElement
333
- */
334
- /**
335
- * Convert camelCase to kebab-case for CSS properties
336
- * @param {string} camelCase - CamelCase string
337
- * @returns {string} Kebab-case string
338
- */
339
286
  const camelToKebab = (camelCase) => {
340
287
  return camelCase.replace(CAMEL_TO_KEBAB_REGEX, (match) => `-${match.toLowerCase()}`);
341
288
  };
342
- /**
343
- * Apply styles to DOM element
344
- * @param {HTMLElement} dom - DOM element
345
- * @param {Object} styleObj - Style object
346
- */
347
289
  const applyStyles = (dom, styleObj) => {
348
290
  if (!is.object(styleObj) || is.null(styleObj)) {
349
291
  dom.style.cssText = '';
@@ -351,7 +293,7 @@ const applyStyles = (dom, styleObj) => {
351
293
  }
352
294
  try {
353
295
  const cssText = Object.entries(styleObj)
354
- .filter(([_, value]) => value != null) // Filter out null/undefined
296
+ .filter(([_, value]) => value != null)
355
297
  .map(([key, value]) => {
356
298
  const kebabKey = camelToKebab(key);
357
299
  return `${kebabKey}: ${value}`;
@@ -365,14 +307,7 @@ const applyStyles = (dom, styleObj) => {
365
307
  }
366
308
  }
367
309
  };
368
- /**
369
- * Apply CSS classes to DOM element
370
- * @param {HTMLElement} dom - DOM element
371
- * @param {string} prevClasses - Previous class string
372
- * @param {string} nextClasses - Next class string
373
- */
374
310
  const applyClasses = (dom, prevClasses, nextClasses) => {
375
- // Allow empty/undefined - just remove classes
376
311
  if (!nextClasses || nextClasses.trim() === '') {
377
312
  if (prevClasses) {
378
313
  const oldClasses = prevClasses.split(/\s+/).filter(Boolean);
@@ -380,24 +315,16 @@ const applyClasses = (dom, prevClasses, nextClasses) => {
380
315
  }
381
316
  return;
382
317
  }
383
- // Remove old classes
384
318
  if (prevClasses) {
385
319
  const oldClasses = prevClasses.split(/\s+/).filter(Boolean);
386
320
  dom.classList.remove(...oldClasses);
387
321
  }
388
- // Add new classes
389
322
  const newClasses = nextClasses.split(/\s+/).filter(Boolean);
390
323
  if (newClasses.length > 0) {
391
324
  dom.classList.add(...newClasses);
392
325
  }
393
326
  };
394
- /**
395
- * Create a DOM element from fiber
396
- * @param {RyunixFiber} fiber - Fiber node
397
- * @returns {HTMLElement | SVGElement | Text | null}
398
- */
399
327
  const createDom = (fiber) => {
400
- // Fragments and Context Providers don't create real DOM nodes
401
328
  if (fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT ||
402
329
  fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT ||
403
330
  fiber.type === Symbol.for('ryunix.portal')) {
@@ -409,7 +336,7 @@ const createDom = (fiber) => {
409
336
  dom = document.createTextNode('');
410
337
  }
411
338
  else if (is.string(fiber.type)) {
412
- const hostType = /** @type {string} */ fiber.type;
339
+ const hostType = fiber.type;
413
340
  const isSvg = [
414
341
  'svg',
415
342
  'path',
@@ -457,8 +384,7 @@ const createDom = (fiber) => {
457
384
  }
458
385
  return null;
459
386
  }
460
- updateDom(
461
- /** @type {HTMLElement | Text} */ /** @type {HTMLElement | SVGElement | Text} */ dom, {}, fiber.props);
387
+ updateDom(dom, {}, fiber.props);
462
388
  return dom;
463
389
  }
464
390
  catch (error) {
@@ -468,12 +394,6 @@ const createDom = (fiber) => {
468
394
  return null;
469
395
  }
470
396
  };
471
- /**
472
- * @param {string} attrName
473
- * @param {unknown} value
474
- * @returns {unknown}
475
- */
476
- /** @type {(attrName: string, value: unknown) => unknown} */
477
397
  const checkAttributeUri = (attrName, value) => {
478
398
  if (typeof value !== 'string')
479
399
  return value;
@@ -496,12 +416,6 @@ const checkAttributeUri = (attrName, value) => {
496
416
  return value;
497
417
  };
498
418
  const validateUri = checkAttributeUri;
499
- /**
500
- * Update DOM element with new props
501
- * @param {HTMLElement|Text} dom - DOM element
502
- * @param {Record<string, unknown>} [prevProps] - Previous props
503
- * @param {Record<string, unknown>} [nextProps] - Next props
504
- */
505
419
  const updateDom = (dom, prevProps = {}, nextProps = {}) => {
506
420
  if (dom.nodeType === 3) {
507
421
  if (prevProps.nodeValue !== nextProps.nodeValue) {
@@ -512,7 +426,7 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
512
426
  const el = dom;
513
427
  const domEl = el;
514
428
  const handlerMap = domEl._ryunixHandlers;
515
- // Remove old event listeners
429
+ const elRecord = el;
516
430
  Object.keys(prevProps)
517
431
  .filter(isEvent)
518
432
  .filter((key) => isGone(nextProps)(key) || isNew(prevProps, nextProps)(key))
@@ -532,12 +446,10 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
532
446
  }
533
447
  }
534
448
  });
535
- // Remove old properties
536
449
  Object.keys(prevProps)
537
450
  .filter(isProperty)
538
451
  .filter(isGone(nextProps))
539
452
  .forEach((propKey) => {
540
- // Skip special properties
541
453
  if (propKey === STRINGS.STYLE ||
542
454
  propKey === OLD_STRINGS.STYLE ||
543
455
  propKey === STRINGS.CLASS_NAME ||
@@ -549,59 +461,45 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
549
461
  el.removeAttribute(attrName);
550
462
  }
551
463
  else {
552
- /** @type {Record<string, unknown>} */ /** @type {unknown} */ el[propKey] = '';
464
+ elRecord[propKey] = '';
553
465
  el.removeAttribute(propKey);
554
466
  }
555
467
  });
556
- // Set new properties
557
468
  Object.keys(nextProps)
558
469
  .filter(isProperty)
559
470
  .filter(isNew(prevProps, nextProps))
560
471
  .forEach((propKey) => {
561
472
  try {
562
- // Handle style properties
563
473
  if (propKey === STRINGS.STYLE || propKey === OLD_STRINGS.STYLE) {
564
474
  const styleValue = nextProps[propKey];
565
- applyStyles(el, /** @type {Record<string, unknown>} */ styleValue);
475
+ applyStyles(el, styleValue);
566
476
  }
567
- // Handle className properties
568
477
  else if (propKey === STRINGS.CLASS_NAME) {
569
- applyClasses(el,
570
- /** @type {string} */ prevProps[STRINGS.CLASS_NAME],
571
- /** @type {string} */ nextProps[STRINGS.CLASS_NAME]);
478
+ applyClasses(el, prevProps[STRINGS.CLASS_NAME], nextProps[STRINGS.CLASS_NAME]);
572
479
  }
573
480
  else if (propKey === OLD_STRINGS.CLASS_NAME) {
574
- applyClasses(el,
575
- /** @type {string} */ prevProps[OLD_STRINGS.CLASS_NAME],
576
- /** @type {string} */ nextProps[OLD_STRINGS.CLASS_NAME]);
481
+ applyClasses(el, prevProps[OLD_STRINGS.CLASS_NAME], nextProps[OLD_STRINGS.CLASS_NAME]);
577
482
  }
578
- // Handle other properties
579
483
  else {
580
- // Special handling for value and checked (controlled components)
581
484
  if (propKey === 'value' || propKey === 'checked') {
582
- if (
583
- /** @type {Record<string, unknown>} */ /** @type {unknown} */ el[propKey] !== nextProps[propKey]) {
584
- /** @type {Record<string, unknown>} */ /** @type {unknown} */ el[propKey] = nextProps[propKey];
485
+ if (elRecord[propKey] !== nextProps[propKey]) {
486
+ elRecord[propKey] = nextProps[propKey];
585
487
  }
586
488
  }
587
489
  else {
588
490
  const isSvgNode = el instanceof SVGElement;
589
491
  if (isSvgNode) {
590
492
  const attrName = toSvgAttrName(propKey);
591
- /** @type {unknown} */
592
493
  const svgValidated = checkAttributeUri(attrName, nextProps[propKey]);
593
- // viewBox is case sensitive, we respect the camelCase for it.
594
- el.setAttribute(attrName, /** @type {string} */ svgValidated);
494
+ el.setAttribute(attrName, String(svgValidated));
595
495
  }
596
496
  else {
597
497
  const attrVal = nextProps[propKey];
598
- /** @type {unknown} */
599
498
  const safeValue = checkAttributeUri(propKey, attrVal);
600
- /** @type {Record<string, unknown>} */ /** @type {unknown} */ el[propKey] = safeValue;
601
- // Best effort: set html attributes if it's not a primitive component property
499
+ elRecord[propKey] = safeValue;
602
500
  if (typeof attrVal !== 'object' &&
603
501
  typeof attrVal !== 'function') {
604
- el.setAttribute(propKey, /** @type {string} */ safeValue);
502
+ el.setAttribute(propKey, String(safeValue));
605
503
  }
606
504
  }
607
505
  }
@@ -613,7 +511,6 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
613
511
  }
614
512
  }
615
513
  });
616
- // Add new event listeners
617
514
  Object.keys(nextProps)
618
515
  .filter(isEvent)
619
516
  .filter(isNew(prevProps, nextProps))
@@ -623,15 +520,6 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
623
520
  const handler = (e) => {
624
521
  runWithPriority(Priority.IMMEDIATE, () => nextProps[propKey](e));
625
522
  };
626
- // Store the wrapped handler so it can be removed later
627
- // Note: For simplicity, we could also just wrap it on the fly,
628
- // but we need the exact reference for removeEventListener.
629
- // Actually, the current removeDom logic uses prevProps[name],
630
- // which won't work if we wrap it here and don't store it.
631
- // Wait, the current removeEventListener call in dom.js:177 is:
632
- // dom.removeEventListener(eventType, prevProps[name])
633
- // If we wrap it, we MUST store the wrapper.
634
- // Let's use a weakMap or a property on the DOM node to store the wrappers.
635
523
  if (!domEl._ryunixHandlers) {
636
524
  domEl._ryunixHandlers = new Map();
637
525
  }
@@ -645,10 +533,6 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
645
533
  }
646
534
  });
647
535
  };
648
- /**
649
- * Clear all children from a DOM element
650
- * @param {HTMLElement} container - DOM element to clear
651
- */
652
536
  const clearContainer = (container) => {
653
537
  if (!container)
654
538
  return;
@@ -676,10 +560,6 @@ function createPortal(children, container) {
676
560
  }
677
561
 
678
562
  const PREFIX = '[Ryunix Hydration]';
679
- /**
680
- * @param {'warn' | 'error'} level
681
- * @param {string} message
682
- */
683
563
  const emit = (level, message) => {
684
564
  const line = `${PREFIX} ${message}`;
685
565
  if (level === 'error') {
@@ -691,16 +571,11 @@ const emit = (level, message) => {
691
571
  };
692
572
  const shouldReportStrict = () => process.env.NODE_ENV !== 'production' &&
693
573
  process.env.RYUNIX_HYDRATION_STRICT === 'true';
694
- /** @param {string} message */
695
574
  const logHydrationInfo = (message) => {
696
575
  if (!shouldReportStrict())
697
576
  return;
698
577
  emit('warn', message);
699
578
  };
700
- /**
701
- * Log the first hydration DOM mismatch (tag/text vs client tree).
702
- * @param {string} detail
703
- */
704
579
  const logHydrationMismatch = (detail) => {
705
580
  const state = getState();
706
581
  if (state.hydrationMismatchReported)
@@ -709,9 +584,6 @@ const logHydrationMismatch = (detail) => {
709
584
  const level = process.env.NODE_ENV === 'production' ? 'error' : 'warn';
710
585
  emit(level, `${detail} Server HTML did not match the client render. Falling back to client-side render.`);
711
586
  };
712
- /**
713
- * @param {string} detail
714
- */
715
587
  const logHydrationBoundaryMismatch = (detail) => {
716
588
  const state = getState();
717
589
  if (state.hydrationBoundaryMismatchReported)
@@ -720,18 +592,11 @@ const logHydrationBoundaryMismatch = (detail) => {
720
592
  const level = process.env.NODE_ENV === 'production' ? 'error' : 'warn';
721
593
  emit(level, `${detail} Recovering the nearest hydration boundary with a scoped client render.`);
722
594
  };
723
- /**
724
- * @param {string} detail
725
- */
726
595
  const logHydrationRecoverable = (detail) => {
727
596
  if (!shouldReportStrict())
728
597
  return;
729
598
  emit('warn', `Recovered a hydration mismatch (${detail}) without root fallback.`);
730
599
  };
731
- /**
732
- * Log when hydration failed and the SSR container is being cleared.
733
- * @param {string} [reason]
734
- */
735
600
  const logHydrationFailure = (reason = '') => {
736
601
  const state = getState();
737
602
  if (state.hydrationFailureReported)
@@ -745,10 +610,6 @@ const logHydrationFailure = (reason = '') => {
745
610
  const level = process.env.NODE_ENV === 'production' ? 'error' : 'warn';
746
611
  emit(level, `${detail}Clearing #__ryunix and remounting on the client.`);
747
612
  };
748
- /**
749
- * Log when leftover SSR nodes are removed after hydration (soft mismatch).
750
- * @param {number} count
751
- */
752
613
  const logHydrationUnmatchedNodes = (count) => {
753
614
  if (!count)
754
615
  return;
@@ -758,7 +619,6 @@ const logHydrationUnmatchedNodes = (count) => {
758
619
  state.hydrationUnmatchedReported = true;
759
620
  emit('warn', `Removed ${count} server-rendered DOM node(s) that were not used by the client tree. This can indicate an SSR/client markup mismatch.`);
760
621
  };
761
- /** Log CSR recovery after a failed hydration pass. */
762
622
  const logHydrationRecovery = () => {
763
623
  const state = getState();
764
624
  if (state.hydrationRecoveryReported)
@@ -766,7 +626,6 @@ const logHydrationRecovery = () => {
766
626
  state.hydrationRecoveryReported = true;
767
627
  emit('warn', 'Remounting the application on the client after hydration failure.');
768
628
  };
769
- /** Log scoped boundary recovery after a local mismatch. */
770
629
  const logHydrationBoundaryRecovery = () => {
771
630
  const state = getState();
772
631
  if (state.hydrationBoundaryRecoveryReported)
@@ -774,13 +633,9 @@ const logHydrationBoundaryRecovery = () => {
774
633
  state.hydrationBoundaryRecoveryReported = true;
775
634
  emit('warn', 'Remounting a hydration boundary after local mismatch.');
776
635
  };
777
- /**
778
- * @param {string} reason
779
- */
780
636
  const logHydrationFatal = (reason) => {
781
637
  emit('error', reason);
782
638
  };
783
- /** Reset per-mount hydration log flags (call from init). */
784
639
  const resetHydrationLogFlags = () => {
785
640
  const state = getState();
786
641
  state.hydrationMismatchReported = false;
@@ -791,25 +646,14 @@ const resetHydrationLogFlags = () => {
791
646
  state.hydrationBoundaryRecoveryReported = false;
792
647
  };
793
648
 
794
- /**
795
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
796
- * @typedef {import('../types/internal.js').RyunixRootFiber} RyunixRootFiber
797
- */
798
- /**
799
- * Run layout effects (useLayoutEffect) synchronously during commit.
800
- * These run after DOM mutations but before the browser paints.
801
- * @param {RyunixFiber} fiber
802
- */
803
649
  const runLayoutEffects = (fiber) => {
804
650
  if (!fiber?.hooks?.length)
805
651
  return;
806
652
  for (let i = 0; i < fiber.hooks.length; i++) {
807
- /** @type {import('../types/internal.js').RyunixHook & { isLayout?: boolean }} */
808
653
  const hook = fiber.hooks[i];
809
654
  if (hook.type === RYUNIX_TYPES.RYUNIX_EFFECT &&
810
655
  hook.isLayout &&
811
656
  is.function(hook.effect)) {
812
- // Cancel previous layout cleanup if exists
813
657
  if (is.function(hook.cancel)) {
814
658
  try {
815
659
  hook.cancel();
@@ -820,12 +664,9 @@ const runLayoutEffects = (fiber) => {
820
664
  }
821
665
  }
822
666
  }
823
- // Run new layout effect synchronously
824
667
  try {
825
668
  const cleanup = hook.effect();
826
- hook.cancel = is.function(cleanup)
827
- ? /** @type {() => void} */ cleanup
828
- : null;
669
+ hook.cancel = is.function(cleanup) ? cleanup : null;
829
670
  }
830
671
  catch (error) {
831
672
  if (process.env.NODE_ENV !== 'production') {
@@ -837,20 +678,14 @@ const runLayoutEffects = (fiber) => {
837
678
  }
838
679
  }
839
680
  };
840
- /**
841
- * Run normal (non-layout) effects asynchronously after paint.
842
- * @param {RyunixFiber} fiber
843
- */
844
681
  const runNormalEffects = (fiber) => {
845
682
  if (!fiber?.hooks?.length)
846
683
  return;
847
684
  for (let i = 0; i < fiber.hooks.length; i++) {
848
- /** @type {import('../types/internal.js').RyunixHook & { isLayout?: boolean }} */
849
685
  const hook = fiber.hooks[i];
850
686
  if (hook.type === RYUNIX_TYPES.RYUNIX_EFFECT &&
851
687
  !hook.isLayout &&
852
688
  is.function(hook.effect)) {
853
- // Cancel previous cleanup if exists
854
689
  if (is.function(hook.cancel)) {
855
690
  try {
856
691
  hook.cancel();
@@ -861,12 +696,9 @@ const runNormalEffects = (fiber) => {
861
696
  }
862
697
  }
863
698
  }
864
- // Run new effect
865
699
  try {
866
700
  const cleanup = hook.effect();
867
- hook.cancel = is.function(cleanup)
868
- ? /** @type {() => void} */ cleanup
869
- : null;
701
+ hook.cancel = is.function(cleanup) ? cleanup : null;
870
702
  }
871
703
  catch (error) {
872
704
  if (process.env.NODE_ENV !== 'production') {
@@ -878,25 +710,19 @@ const runNormalEffects = (fiber) => {
878
710
  }
879
711
  }
880
712
  };
881
- /**
882
- * The `commitRoot` function commits the changes made to the virtual DOM by updating the actual DOM.
883
- */
884
713
  function commitRoot() {
885
714
  const state = getState();
886
715
  state.deletions.forEach(commitWork);
887
- const finishedWork = /** @type {RyunixRootFiber} */ state.wipRoot;
888
- // Swap the currentRoot pointer BEFORE running effects
889
- // This allows dispatches inside effects to base their new work on the just-finished tree
716
+ const finishedWork = state.wipRoot;
717
+ if (!finishedWork)
718
+ return;
890
719
  state.currentRoot = finishedWork;
891
- // After hydration is done, reset the flag and cleanup unconsumed nodes
892
720
  if (state.isHydrating || state.hydrationFailed) {
893
721
  if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
894
722
  console.log(`[Ryunix Debug] commitRoot - isHydrating: ${state.isHydrating}, hydrationFailed: ${state.hydrationFailed}`);
895
723
  }
896
724
  if (state.hydrationFailed) ;
897
725
  else {
898
- // If there is a cursor left, it means these are SSR nodes that weren't matched
899
- // by any client fiber. We must remove them to avoid duplication.
900
726
  let cursor = state.hydrateCursor;
901
727
  let removed = 0;
902
728
  if (cursor &&
@@ -919,23 +745,17 @@ function commitRoot() {
919
745
  state.hydrateCursor = null;
920
746
  }
921
747
  commitWork(finishedWork.child);
922
- // If wipRoot was not reassigned by a synchronous dispatch during effects, clear it
923
748
  if (state.wipRoot === finishedWork) {
924
749
  state.wipRoot = null;
925
750
  }
926
751
  }
927
- /**
928
- * @param {RyunixFiber | null | undefined} fiber
929
- */
930
752
  function commitWork(fiber) {
931
753
  if (!fiber) {
932
754
  return;
933
755
  }
934
- // Handle portal fibers — they render into a different container
935
756
  if (fiber.type === RYUNIX_PORTAL || fiber._isPortal) {
936
757
  const portalContainer = fiber.containerInfo;
937
758
  if (portalContainer) {
938
- // Process portal children into the portal container
939
759
  const portalFiber = fiber.child;
940
760
  if (portalFiber) {
941
761
  commitPortalWork(portalFiber, portalContainer);
@@ -952,6 +772,8 @@ function commitWork(fiber) {
952
772
  return;
953
773
  }
954
774
  const domParent = domParentFiber.dom;
775
+ if (!domParent)
776
+ return;
955
777
  if (fiber.effectTag === EFFECT_TAGS.PLACEMENT) {
956
778
  if (fiber.dom != null) {
957
779
  if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
@@ -959,15 +781,13 @@ function commitWork(fiber) {
959
781
  }
960
782
  domParent.appendChild(fiber.dom);
961
783
  }
962
- // Layout effects run synchronously during commit
963
784
  runLayoutEffects(fiber);
964
- // Normal effects run after paint
965
785
  runNormalEffects(fiber);
966
786
  }
967
787
  else if (fiber.effectTag === EFFECT_TAGS.UPDATE) {
968
788
  cancelEffects(fiber);
969
789
  if (fiber.dom != null) {
970
- updateDom(fiber.dom, fiber.alternate.props, fiber.props);
790
+ updateDom(fiber.dom, fiber.alternate?.props, fiber.props);
971
791
  }
972
792
  runLayoutEffects(fiber);
973
793
  runNormalEffects(fiber);
@@ -978,8 +798,6 @@ function commitWork(fiber) {
978
798
  if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
979
799
  console.log('[Ryunix Debug] Hydration fallback PLACEMENT:', fiber.type);
980
800
  }
981
- // Since container is cleared on fallback, treat as normal placement
982
- // No need to check fiber.dom.parentNode !== domParent because the container was cleared.
983
801
  if (fiber.dom != null) {
984
802
  domParent.appendChild(fiber.dom);
985
803
  }
@@ -998,7 +816,6 @@ function commitWork(fiber) {
998
816
  }
999
817
  }
1000
818
  else if (fiber.effectTag === EFFECT_TAGS.DELETION) {
1001
- // Run cleanups BEFORE removing DOM to allow cleanup functions to read DOM state
1002
819
  cancelEffectsDeep(fiber);
1003
820
  commitDeletion(fiber);
1004
821
  return;
@@ -1006,11 +823,6 @@ function commitWork(fiber) {
1006
823
  commitWork(fiber.child);
1007
824
  commitWork(fiber.sibling);
1008
825
  }
1009
- /**
1010
- * Commit work for portal children into a specific container
1011
- * @param {RyunixFiber | null | undefined} fiber
1012
- * @param {Element | DocumentFragment} portalContainer
1013
- */
1014
826
  const commitPortalWork = (fiber, portalContainer) => {
1015
827
  if (!fiber)
1016
828
  return;
@@ -1024,7 +836,7 @@ const commitPortalWork = (fiber, portalContainer) => {
1024
836
  else if (fiber.effectTag === EFFECT_TAGS.UPDATE) {
1025
837
  cancelEffects(fiber);
1026
838
  if (fiber.dom != null) {
1027
- updateDom(fiber.dom, fiber.alternate.props, fiber.props);
839
+ updateDom(fiber.dom, fiber.alternate?.props, fiber.props);
1028
840
  }
1029
841
  runLayoutEffects(fiber);
1030
842
  runNormalEffects(fiber);
@@ -1037,10 +849,6 @@ const commitPortalWork = (fiber, portalContainer) => {
1037
849
  commitPortalWork(fiber.child, portalContainer);
1038
850
  commitPortalWork(fiber.sibling, portalContainer);
1039
851
  };
1040
- /**
1041
- * @param {RyunixFiber} fiber
1042
- * @param {Node} domParent
1043
- */
1044
852
  const commitDeletion = (fiber, domParent) => {
1045
853
  if (fiber.dom) {
1046
854
  if (fiber.dom.parentNode) {
@@ -1081,6 +889,12 @@ const reconcileChildren = (wipFiber, elements) => {
1081
889
  let newFiber;
1082
890
  const sameType = matchedFiber && element.type === matchedFiber.type;
1083
891
  if (sameType && matchedFiber) {
892
+ const matchedType = matchedFiber.type;
893
+ const isErrorBoundary = typeof matchedFiber.type === 'function' &&
894
+ matchedType?.ryunix_type === 'RYUNIX_ERROR_BOUNDARY';
895
+ const preserveBoundaryError = isErrorBoundary &&
896
+ matchedFiber.stateError != null &&
897
+ matchedFiber.child == null;
1084
898
  newFiber = {
1085
899
  type: matchedFiber.type,
1086
900
  props: element.props,
@@ -1089,7 +903,7 @@ const reconcileChildren = (wipFiber, elements) => {
1089
903
  alternate: matchedFiber,
1090
904
  effectTag: EFFECT_TAGS.UPDATE,
1091
905
  hooks: matchedFiber.hooks,
1092
- stateError: matchedFiber.stateError,
906
+ stateError: preserveBoundaryError ? matchedFiber.stateError : undefined,
1093
907
  key: element.key,
1094
908
  index,
1095
909
  };
@@ -1130,852 +944,751 @@ const reconcileChildren = (wipFiber, elements) => {
1130
944
  });
1131
945
  };
1132
946
 
1133
- /**
1134
- * Bridge module to break circular dependencies between hooks and workers.
1135
- */
1136
- /** @type {import('../types/internal.js').ScheduleWorkFn | null} */
1137
- let scheduleWorkFn = null;
1138
- /**
1139
- * @param {import('../types/internal.js').ScheduleWorkFn} fn
1140
- */
1141
- const setScheduleWork = (fn) => {
1142
- scheduleWorkFn = fn;
947
+ const getHydrationPolicy = () => {
948
+ const env = (globalThis.process && globalThis.process.env) || {};
949
+ const recoverRaw = env.RYUNIX_HYDRATION_RECOVER || 'boundary';
950
+ const boundariesRaw = env.RYUNIX_HYDRATION_BOUNDARIES || 'route';
951
+ const strict = env.RYUNIX_HYDRATION_STRICT === 'true';
952
+ const recover = recoverRaw === 'none' || recoverRaw === 'root' ? recoverRaw : 'boundary';
953
+ const boundaries = boundariesRaw === 'server-only' || boundariesRaw === 'all-layouts'
954
+ ? boundariesRaw
955
+ : 'route';
956
+ return {
957
+ recover,
958
+ boundaries,
959
+ strict,
960
+ };
1143
961
  };
1144
- /**
1145
- * @param {import('../types/internal.js').RyunixRootFiber} root
1146
- * @param {number} [priority]
1147
- */
1148
- const scheduleWork$1 = (root, priority) => {
1149
- if (scheduleWorkFn) {
1150
- return scheduleWorkFn(root, priority);
1151
- }
1152
- if (process.env.NODE_ENV !== 'production') {
1153
- console.warn('[Ryunix] scheduleWork called before being initialized.');
962
+ const findNearestHydrationBoundary = (fiber) => {
963
+ let current = fiber || null;
964
+ while (current) {
965
+ const props = current.props;
966
+ if (props &&
967
+ Object.prototype.hasOwnProperty.call(props, 'data-ryunix-hydrate-boundary')) {
968
+ return current;
969
+ }
970
+ const type = current.type;
971
+ const maybeTyped = type;
972
+ if (type &&
973
+ typeof type === 'function' &&
974
+ maybeTyped?.ryunix_type === 'RYUNIX_HYDRATION_BOUNDARY') {
975
+ return current;
976
+ }
977
+ current = current.parent || null;
1154
978
  }
979
+ return null;
1155
980
  };
1156
-
1157
- let isBatching = false;
1158
- let pendingUpdates = [];
1159
- function batchUpdates(callback) {
1160
- const wasBatching = isBatching;
1161
- isBatching = true;
1162
- try {
1163
- callback();
1164
- }
1165
- finally {
1166
- isBatching = wasBatching;
1167
- if (!isBatching && pendingUpdates.length > 0) {
1168
- flushUpdates();
981
+ const findBoundaryDomFromNode = (node) => {
982
+ let current = node ?? null;
983
+ while (current) {
984
+ if (current.nodeType === 1 &&
985
+ current.hasAttribute('data-ryunix-hydrate-boundary')) {
986
+ return current;
1169
987
  }
988
+ current = current.parentNode;
1170
989
  }
1171
- }
1172
- function queueUpdate(update) {
1173
- pendingUpdates.push(update);
1174
- if (!isBatching) {
1175
- flushUpdates();
1176
- }
1177
- }
1178
- function flushUpdates() {
1179
- if (pendingUpdates.length === 0)
1180
- return;
1181
- const updates = pendingUpdates;
1182
- pendingUpdates = [];
1183
- updates.forEach((update) => update());
1184
- }
1185
-
1186
- /**
1187
- * @typedef {import('../types/internal.js').RyunixComponent} RyunixComponent
1188
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
1189
- */
1190
- /**
1191
- * Development warnings
1192
- */
1193
- process.env.NODE_ENV !== 'production';
1194
- /**
1195
- * Hook call validation
1196
- */
1197
- const validateHookContext = (hookName = 'A hook') => {
1198
- const state = getState();
1199
- if (!state.wipFiber) {
1200
- throw new Error(`${hookName} can only be called inside function components. ` +
1201
- 'Make sure you are calling hooks at the top level of your component.');
990
+ return null;
991
+ };
992
+ const getBoundaryDom = (fiber) => {
993
+ if (!fiber)
994
+ return null;
995
+ if (fiber.dom && fiber.dom.nodeType === 1) {
996
+ const el = fiber.dom;
997
+ if (el.hasAttribute('data-ryunix-hydrate-boundary'))
998
+ return el;
1202
999
  }
1203
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1204
- if (!Array.isArray(wipFiber.hooks)) {
1205
- wipFiber.hooks = [];
1000
+ let child = fiber.child || null;
1001
+ while (child) {
1002
+ if (child.dom && child.dom.nodeType === 1) {
1003
+ const el = child.dom;
1004
+ if (el.hasAttribute('data-ryunix-hydrate-boundary'))
1005
+ return el;
1006
+ }
1007
+ child = child.child || child.sibling || null;
1206
1008
  }
1009
+ return null;
1207
1010
  };
1208
-
1209
- /**
1210
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
1211
- * @typedef {import('../types/internal.js').RyunixHook} RyunixHook
1212
- * @typedef {import('../types/internal.js').RyunixRoute} RyunixRoute
1213
- * @typedef {import('../types/internal.js').RyunixRouterContextValue} RyunixRouterContextValue
1214
- * @typedef {import('../types/internal.js').RyunixMetadataTags} RyunixMetadataTags
1215
- * @typedef {import('../types/internal.js').RyunixMetadataOptions} RyunixMetadataOptions
1216
- * @typedef {import('../types/internal.js').RyunixRootFiber} RyunixRootFiber
1217
- * @typedef {import('../types/internal.js').RyunixComponent} RyunixComponent
1218
- */
1219
- /**
1220
- * @typedef {{ route: RyunixRoute | { component: RyunixComponent | null }, params: Record<string, string | string[]> }} RouteMatch
1221
- */
1222
- /**
1223
- * @param {unknown[] | undefined} oldDeps
1224
- * @param {unknown[] | undefined} newDeps
1225
- * @returns {boolean}
1226
- */
1227
- const haveDepsChanged = (oldDeps, newDeps) => {
1228
- if (!oldDeps || !newDeps)
1229
- return true;
1230
- if (oldDeps.length !== newDeps.length)
1231
- return true;
1232
- return oldDeps.some((dep, i) => !Object.is(dep, newDeps[i]));
1011
+ const skipHydrationSubtree = (cursor, boundaryRoot) => {
1012
+ if (!cursor || !boundaryRoot)
1013
+ return cursor;
1014
+ if (cursor === boundaryRoot)
1015
+ return boundaryRoot.nextSibling;
1016
+ if (boundaryRoot.contains(cursor))
1017
+ return boundaryRoot.nextSibling;
1018
+ return cursor;
1233
1019
  };
1234
- /**
1235
- * @param {unknown} initialState
1236
- * @param {number} [priority]
1237
- * @returns {[unknown, (action: unknown, priority?: number) => void]}
1238
- */
1239
- const useStore = (initialState, priority = getCurrentPriority()) => {
1240
- // SSR safety check - more reliable than state.isServerRendering
1241
- if (typeof window === 'undefined') {
1242
- return [
1243
- is.function(initialState)
1244
- ? /** @type {() => unknown} */ initialState()
1245
- : initialState,
1246
- () => { },
1247
- ];
1248
- }
1020
+ const enqueueScopedRecovery = (boundaryFiber, boundaryDom, resumeCursor) => {
1021
+ if (!boundaryFiber || !boundaryDom)
1022
+ return;
1249
1023
  const state = getState();
1250
- if (state.isServerRendering) {
1251
- return [
1252
- is.function(initialState)
1253
- ? /** @type {() => unknown} */ initialState()
1254
- : initialState,
1255
- () => { },
1256
- ];
1257
- }
1258
- /**
1259
- * @param {unknown} state
1260
- * @param {unknown} action
1261
- */
1262
- const reducer = (state, action) => is.function(action) ? action(state) : action;
1263
- return useReducer(reducer, initialState, undefined, priority);
1024
+ const queue = state.scopedRecoveryQueue || [];
1025
+ queue.push({
1026
+ boundaryFiber,
1027
+ boundaryDom,
1028
+ resumeCursor,
1029
+ element: (Array.isArray(boundaryFiber.props?.children)
1030
+ ? boundaryFiber.props.children[0]
1031
+ : boundaryFiber.props?.children),
1032
+ });
1033
+ state.scopedRecoveryQueue = queue;
1264
1034
  };
1265
- /**
1266
- * @param {(state: unknown, action: unknown) => unknown} reducer
1267
- * @param {unknown} initialState
1268
- * @param {((initial: unknown) => unknown)=} [init]
1269
- * @param {number} [defaultPriority]
1270
- * @returns {[unknown, (action: unknown, priority?: number) => void]}
1271
- */
1272
- const useReducer = (reducer, initialState, init, defaultPriority = getCurrentPriority()) => {
1273
- // SSR safety check - more reliable than state.isServerRendering
1274
- if (typeof window === 'undefined') {
1275
- return [init ? init(initialState) : initialState, () => { }];
1276
- }
1035
+
1036
+ const updateFunctionComponent = (fiber) => {
1277
1037
  const state = getState();
1278
- if (state.isServerRendering) {
1279
- return [init ? init(initialState) : initialState, () => { }];
1038
+ state.wipFiber = fiber;
1039
+ state.hookIndex = 0;
1040
+ fiber.hooks = [];
1041
+ const componentType = fiber.type;
1042
+ if (state.hydrationRecover &&
1043
+ componentType.ryunix_type === 'RYUNIX_ERROR_BOUNDARY') {
1044
+ fiber.stateError = undefined;
1280
1045
  }
1281
- validateHookContext();
1282
- const { hookIndex } = state;
1283
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1284
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1285
- const hook = {
1286
- hookID: hookIndex,
1287
- type: RYUNIX_TYPES.RYUNIX_STORE,
1288
- state: oldHook ? oldHook.state : init ? init(initialState) : initialState,
1289
- queue: /** @type {unknown[]} */ [],
1290
- };
1291
- if (oldHook?.queue) {
1292
- oldHook.queue.forEach(
1293
- /** @param {unknown} action */ (action) => {
1294
- try {
1295
- hook.state = reducer(hook.state, action);
1296
- }
1297
- catch (error) {
1298
- if (process.env.NODE_ENV !== 'production') {
1299
- console.error('Error in reducer:', error);
1300
- }
1301
- }
1302
- });
1046
+ if (state.isHydrating) {
1047
+ fiber.effectTag = EFFECT_TAGS.HYDRATE;
1303
1048
  }
1304
- /** @param {unknown} action @param {number} [priority] */
1305
- const dispatch = (action, priority = defaultPriority) => {
1306
- if (action === undefined) {
1307
- if (process.env.NODE_ENV !== 'production') {
1308
- console.warn('dispatch called with undefined action');
1049
+ if (componentType._isMemo && fiber.alternate) {
1050
+ const { children: _pc, ...prevRest } = fiber.alternate.props || {};
1051
+ const { children: _nc, ...nextRest } = fiber.props || {};
1052
+ if (componentType._arePropsEqual?.(prevRest, nextRest)) {
1053
+ fiber.hooks = fiber.alternate.hooks;
1054
+ const oldChild = fiber.alternate.child;
1055
+ if (oldChild) {
1056
+ oldChild.parent = fiber;
1057
+ fiber.child = oldChild;
1309
1058
  }
1310
1059
  return;
1311
1060
  }
1312
- hook.queue.push(action);
1313
- const currentState = getState();
1314
- const activeRoot =
1315
- /** @type {RyunixRootFiber | null | undefined} */ currentState.currentRoot ||
1316
- currentState.wipRoot;
1317
- if (!activeRoot)
1318
- return;
1319
- const newRoot = /** @type {RyunixRootFiber} */ {
1320
- dom: activeRoot.dom,
1321
- props: activeRoot.props,
1322
- alternate:
1323
- /** @type {RyunixRootFiber | null} */ currentState.currentRoot || null,
1324
- };
1325
- queueUpdate(() => scheduleWork$1(newRoot, priority));
1326
- };
1327
- wipFiber.hooks[hookIndex] = hook;
1328
- state.hookIndex++;
1329
- return [hook.state, dispatch];
1330
- };
1331
- /**
1332
- * The `useEffect` function in JavaScript is used to manage side effects in functional components by
1333
- * comparing dependencies and executing a callback function when dependencies change.
1334
- * @param callback - The `callback` parameter in the `useEffect` function is a function that will be
1335
- * executed as the effect. This function can perform side effects like data fetching, subscriptions, or
1336
- * DOM manipulations.
1337
- * @param deps - The `deps` parameter in the `useEffect` function stands for dependencies. It is an
1338
- * optional array that contains values that the effect depends on. The effect will only re-run if any
1339
- * of the values in the `deps` array have changed since the last render. If the `deps` array
1340
- * @param {() => void | (() => void)} callback
1341
- * @param {unknown[] | undefined} deps
1342
- * @returns {void}
1343
- */
1344
- const useEffect = (callback, deps) => {
1345
- // SSR safety check - more reliable than state.isServerRendering
1346
- if (typeof window === 'undefined') {
1347
- return;
1348
- }
1349
- const state = getState();
1350
- if (state.isServerRendering) {
1351
- return;
1352
1061
  }
1353
- validateHookContext();
1354
- if (!is.function(callback)) {
1355
- throw new Error('useEffect callback must be a function');
1062
+ const children = [
1063
+ componentType(fiber.props),
1064
+ ];
1065
+ if (componentType._contextId && fiber.props?.value !== undefined) {
1066
+ fiber._contextId = componentType._contextId;
1067
+ fiber._contextValue = fiber.props.value;
1356
1068
  }
1357
- if (deps !== undefined && !Array.isArray(deps)) {
1358
- throw new Error('useEffect dependencies must be an array or undefined');
1069
+ reconcileChildren(fiber, children);
1070
+ };
1071
+ const isUnderClientOnlyBoundary = (fiber) => {
1072
+ let current = fiber?.parent || null;
1073
+ while (current) {
1074
+ if (current._hydrateClientOnly)
1075
+ return true;
1076
+ current = current.parent || null;
1359
1077
  }
1360
- const { hookIndex } = state;
1361
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1362
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1363
- const hasChanged = haveDepsChanged(oldHook?.deps, deps);
1364
- const hook = {
1365
- hookID: hookIndex,
1366
- type: RYUNIX_TYPES.RYUNIX_EFFECT,
1367
- deps,
1368
- effect: hasChanged ? callback : null,
1369
- cancel: oldHook?.cancel,
1370
- };
1371
- wipFiber.hooks[hookIndex] = hook;
1372
- state.hookIndex++;
1078
+ return false;
1373
1079
  };
1374
- /**
1375
- * The useRef function in JavaScript creates a reference object with an initial value for use in functional components.
1376
- * @param initialValue - The `initialValue` parameter in the `useRef` function represents the initial
1377
- * value that will be assigned to the `current` property of the reference object. This initial value
1378
- * will be used if there is no previous value stored in the hook.
1379
- * @param {unknown} initialValue
1380
- * @returns {{ current: unknown }}
1381
- */
1382
- const useRef = (initialValue) => {
1383
- // SSR safety check - more reliable than state.isServerRendering
1384
- if (typeof window === 'undefined') {
1385
- return { current: initialValue };
1080
+ const isUnderServerPreserveBoundary = (fiber) => {
1081
+ let current = fiber?.parent || null;
1082
+ while (current) {
1083
+ if (current._hydratePreserveServer)
1084
+ return true;
1085
+ current = current.parent || null;
1386
1086
  }
1087
+ return false;
1088
+ };
1089
+ const normalizeChildNodes = (children) => {
1090
+ if (children == null)
1091
+ return [];
1092
+ return Array.isArray(children) ? children : [children];
1093
+ };
1094
+ const updateHostComponent = (fiber) => {
1387
1095
  const state = getState();
1388
- if (state.isServerRendering) {
1389
- return { current: initialValue };
1096
+ if (fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
1097
+ fiber._contextId = fiber.props?._contextId;
1098
+ fiber._contextValue = fiber.props?.value;
1390
1099
  }
1391
- validateHookContext();
1392
- const { hookIndex } = state;
1393
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1394
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1395
- const hook = {
1396
- hookID: hookIndex,
1397
- type: RYUNIX_TYPES.RYUNIX_REF,
1398
- value: oldHook
1399
- ? /** @type {{ value: { current: unknown } }} */ oldHook.value
1400
- : { current: initialValue },
1401
- };
1402
- wipFiber.hooks[hookIndex] = hook;
1403
- state.hookIndex++;
1404
- return /** @type {{ current: unknown }} */ hook.value;
1405
- };
1406
- /**
1407
- * The useMemo function in JavaScript is used to memoize the result of a computation based on
1408
- * dependencies.
1409
- * @param compute - The `compute` parameter in the `useMemo` function is a callback function that
1410
- * calculates the value that `useMemo` will memoize and return. This function will be called to compute
1411
- * the memoized value when necessary.
1412
- * @param deps - The `deps` parameter in the `useMemo` function refers to an array of dependencies.
1413
- * These dependencies are used to determine whether the memoized value needs to be recalculated or if
1414
- * the previously calculated value can be reused. The `useMemo` hook will recompute the memoized value
1415
- * only if
1416
- * @param {() => unknown} compute
1417
- * @param {unknown[]} deps
1418
- * @returns {unknown}
1419
- */
1420
- const useMemo = (compute, deps) => {
1421
- // SSR safety check - more reliable than state.isServerRendering
1422
- if (typeof window === 'undefined') {
1423
- return compute();
1424
- }
1425
- const state = getState();
1426
- if (state.isServerRendering) {
1427
- return compute();
1428
- }
1429
- validateHookContext();
1430
- if (!is.function(compute)) {
1431
- throw new Error('useMemo callback must be a function');
1432
- }
1433
- if (!Array.isArray(deps)) {
1434
- throw new Error('useMemo requires a dependencies array');
1100
+ const isPassthrough = fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT ||
1101
+ fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT ||
1102
+ fiber.type === Symbol.for('ryunix.portal');
1103
+ if (state.isHydrating && isPassthrough) {
1104
+ fiber.effectTag = EFFECT_TAGS.HYDRATE;
1435
1105
  }
1436
- const { hookIndex } = state;
1437
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1438
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1439
- let value;
1440
- if (oldHook && !haveDepsChanged(oldHook.deps, deps)) {
1441
- value = /** @type {{ value?: unknown }} */ oldHook.value;
1106
+ else if (state.isHydrating && isUnderServerPreserveBoundary(fiber)) {
1107
+ return;
1442
1108
  }
1443
- else {
1444
- try {
1445
- value = compute();
1109
+ else if (state.isHydrating && isUnderClientOnlyBoundary(fiber)) {
1110
+ if (!fiber.dom) {
1111
+ fiber.dom = createDom(fiber);
1112
+ fiber.effectTag = EFFECT_TAGS.PLACEMENT;
1446
1113
  }
1447
- catch (error) {
1448
- if (process.env.NODE_ENV !== 'production') {
1449
- console.error('Error in useMemo computation:', error);
1114
+ }
1115
+ else if (!fiber.dom) {
1116
+ if (state.isHydrating && state.hydrateCursor) {
1117
+ const domNode = state.hydrateCursor;
1118
+ const isText = fiber.type === RYUNIX_TYPES.TEXT_ELEMENT && domNode.nodeType === 3;
1119
+ const isElement = typeof fiber.type === 'string' &&
1120
+ domNode.nodeType === 1 &&
1121
+ domNode.tagName.toLowerCase() === fiber.type.toLowerCase();
1122
+ if (isText || isElement) {
1123
+ fiber.dom = domNode;
1124
+ fiber.effectTag = EFFECT_TAGS.HYDRATE;
1125
+ if (isText &&
1126
+ fiber.props?.nodeValue != null &&
1127
+ domNode.nodeValue !== String(fiber.props.nodeValue)) {
1128
+ domNode.nodeValue = String(fiber.props.nodeValue);
1129
+ logHydrationRecoverable('text');
1130
+ }
1131
+ if (isElement &&
1132
+ domNode.hasAttribute('data-ryunix-server')) {
1133
+ fiber._hydratePreserveServer = true;
1134
+ state.hydrateCursor = nextValidSibling$1(domNode);
1135
+ reconcileChildren(fiber, []);
1136
+ return;
1137
+ }
1138
+ if (isElement &&
1139
+ domNode.hasAttribute('data-ryunix-hydrate-boundary')) {
1140
+ fiber._hydrateClientOnly = true;
1141
+ }
1142
+ state.hydrateCursor = nextValidSibling$1(domNode.firstChild);
1450
1143
  }
1451
- throw error;
1144
+ else {
1145
+ const policy = getHydrationPolicy();
1146
+ const detail = `Mismatch at ${getTypeLabel(fiber.type ?? 'unknown')}. Expected ${domNode.nodeType === 1 ? domNode.tagName : 'text'} but got ${String(fiber.type)}.`;
1147
+ const boundaryFiber = findNearestHydrationBoundary(fiber);
1148
+ const boundaryDom = (boundaryFiber ? getBoundaryDom(boundaryFiber) : null) ??
1149
+ findBoundaryDomFromNode(state.hydrateCursor);
1150
+ if (policy.recover === 'boundary' && boundaryFiber && boundaryDom) {
1151
+ logHydrationBoundaryMismatch(detail);
1152
+ enqueueScopedRecovery(boundaryFiber, boundaryDom, state.hydrateCursor ?? null);
1153
+ state.hydrateCursor = skipHydrationSubtree(state.hydrateCursor ?? null, boundaryDom);
1154
+ fiber.dom = createDom(fiber);
1155
+ fiber.effectTag = EFFECT_TAGS.PLACEMENT;
1156
+ }
1157
+ else if (policy.recover === 'none') {
1158
+ logHydrationFatal(detail);
1159
+ state.isHydrating = false;
1160
+ state.hydrateCursor = null;
1161
+ fiber.dom = createDom(fiber);
1162
+ fiber.effectTag = EFFECT_TAGS.PLACEMENT;
1163
+ }
1164
+ else {
1165
+ logHydrationMismatch(detail);
1166
+ state.isHydrating = false;
1167
+ state.hydrationFailed = true;
1168
+ state.hydrateCursor = null;
1169
+ fiber.dom = createDom(fiber);
1170
+ fiber.effectTag = EFFECT_TAGS.PLACEMENT;
1171
+ }
1172
+ }
1173
+ }
1174
+ else {
1175
+ fiber.dom = createDom(fiber);
1452
1176
  }
1453
1177
  }
1454
- const hook = {
1455
- hookID: hookIndex,
1456
- type: RYUNIX_TYPES.RYUNIX_MEMO,
1457
- value,
1458
- deps,
1459
- };
1460
- wipFiber.hooks[hookIndex] = hook;
1461
- state.hookIndex++;
1462
- return value;
1178
+ if (fiber._hydratePreserveServer) {
1179
+ return;
1180
+ }
1181
+ const children = normalizeChildNodes(fiber.props?.children);
1182
+ reconcileChildren(fiber, children);
1463
1183
  };
1464
- /**
1465
- * The useCallback function in JavaScript ensures that a callback function is memoized based on its
1466
- * dependencies.
1467
- * @param callback - A function that you want to memoize and return for later use.
1468
- * @param deps - The `deps` parameter in the `useCallback` function refers to an array of dependencies.
1469
- * These dependencies are used to determine when the callback function should be re-evaluated and
1470
- * memoized. If any of the dependencies change, the callback function will be re-executed and the
1471
- * memoized value will
1472
- * @param {(...args: never[]) => unknown} callback
1473
- * @param {unknown[]} deps
1474
- * @returns {(...args: never[]) => unknown}
1475
- */
1476
- const useCallback = (callback, deps) => {
1477
- if (!is.function(callback)) {
1478
- throw new Error('useCallback requires a function as first argument');
1184
+ const getTypeLabel = (type) => {
1185
+ if (typeof type === 'symbol')
1186
+ return type.description || type.toString();
1187
+ if (typeof type === 'function')
1188
+ return type.name || 'anonymous';
1189
+ return String(type);
1190
+ };
1191
+
1192
+ let scheduleWorkFn = null;
1193
+ const setScheduleWork = (fn) => {
1194
+ scheduleWorkFn = fn;
1195
+ };
1196
+ const scheduleWork$1 = (root, priority) => {
1197
+ if (scheduleWorkFn) {
1198
+ return scheduleWorkFn(root, priority);
1479
1199
  }
1480
- return /** @type {(...args: never[]) => unknown} */ useMemo(() => callback, deps);
1481
- };
1482
- /**
1483
- * The createContext function creates a context provider and useContext hook in JavaScript.
1484
- * @param [contextId] - The `contextId` parameter in the `createContext` function is used to specify
1485
- * the unique identifier for the context being created. It defaults to `RYUNIX_TYPES.RYUNIX_CONTEXT` if
1486
- * not provided.
1487
- * @param [defaultValue] - The `defaultValue` parameter in the `createContext` function is used to
1488
- * specify the default value that will be returned by the `useContext` hook if no provider is found in
1489
- * the component tree. It is an optional parameter, and if not provided, an empty object `{}` will be
1490
- * used as
1491
- * @param {string | symbol} [contextId]
1492
- * @param {unknown} [defaultValue]
1493
- * @returns {{ Provider: RyunixComponent & { _contextId?: string | symbol }, useContext: (ctxID?: string | symbol) => unknown }}
1494
- */
1495
- const createContext = (contextId = RYUNIX_TYPES.RYUNIX_CONTEXT, defaultValue = {}) => {
1496
- /** @param {{ value?: unknown, children?: import('../types/internal.js').RyunixNode }} props */
1497
- const Provider = ({ value, children }) => {
1498
- return createElement(RYUNIX_TYPES.RYUNIX_CONTEXT, { value, children, _contextId: contextId }, ...flattenArray([children]));
1499
- };
1500
- Provider._contextId = contextId;
1501
- /** @param {string | symbol} [ctxID] */
1502
- const useContext = (ctxID = contextId) => {
1503
- const state = getState();
1504
- if (state.isServerRendering) {
1505
- const ssrContexts =
1506
- /** @type {Record<string | symbol, unknown> | undefined} */ state.ssrContexts;
1507
- return ssrContexts && ssrContexts[ctxID] !== undefined
1508
- ? ssrContexts[ctxID]
1509
- : defaultValue;
1510
- }
1511
- validateHookContext();
1512
- /** @type {RyunixFiber | null | undefined} */
1513
- let fiber = /** @type {RyunixFiber} */ state.wipFiber;
1514
- while (fiber) {
1515
- if (fiber._contextId === ctxID && fiber._contextValue !== undefined) {
1516
- return fiber._contextValue;
1517
- }
1518
- const fiberType = fiber.type;
1519
- if (fiberType?._contextId === ctxID && fiber.props?.value !== undefined) {
1520
- return fiber.props.value;
1521
- }
1522
- fiber = fiber.parent;
1523
- }
1524
- return defaultValue;
1525
- };
1526
- return {
1527
- Provider:
1528
- /** @type {RyunixComponent & { _contextId?: string | symbol }} */ Provider,
1529
- useContext,
1200
+ if (process.env.NODE_ENV !== 'production') {
1201
+ console.warn('[Ryunix] scheduleWork called before being initialized.');
1202
+ }
1203
+ };
1204
+
1205
+ const getRootChild = (children) => {
1206
+ if (children == null)
1207
+ return undefined;
1208
+ return Array.isArray(children) ? children[0] : children;
1209
+ };
1210
+ const renderSubtree = (element, container) => {
1211
+ clearContainer(container);
1212
+ const root = {
1213
+ dom: container,
1214
+ props: { children: [element] },
1215
+ isHydrating: false,
1216
+ hydrateCursor: null,
1530
1217
  };
1218
+ scheduleWork$1(root, undefined);
1531
1219
  };
1532
- /**
1533
- * The `useQuery` function extracts query parameters from the URL in a browser environment.
1534
- * @returns {Record<string, string>}
1535
- */
1536
- const useQuery = () => {
1537
- if (typeof window === 'undefined')
1538
- return {};
1539
- const searchParams = new URLSearchParams(window.location.search);
1540
- /** @type {Record<string, string>} */
1541
- const query = {};
1542
- for (const [key, value] of searchParams.entries()) {
1543
- query[key] = value;
1220
+ const recoverScopedHydrationFailures = () => {
1221
+ const state = getState();
1222
+ const queue = state.scopedRecoveryQueue;
1223
+ if (!queue?.length)
1224
+ return;
1225
+ state.scopedRecoveryQueue = [];
1226
+ for (const item of queue) {
1227
+ logHydrationBoundaryRecovery();
1228
+ renderSubtree(item.element, item.boundaryDom);
1544
1229
  }
1545
- return query;
1546
1230
  };
1547
- /**
1548
- * The function `useHash` in JavaScript is used to manage and update the hash portion of the URL in a
1549
- * web application.
1550
- * @returns {string}
1551
- */
1552
- const useHash = () => {
1553
- if (typeof window === 'undefined')
1554
- return '';
1555
- const [hash, setHash] = useStore(window.location.hash);
1556
- useEffect(() => {
1557
- const onHashChange = () => setHash(window.location.hash);
1558
- window.addEventListener('hashchange', onHashChange);
1559
- return () => window.removeEventListener('hashchange', onHashChange);
1560
- }, []);
1561
- return /** @type {string} */ hash;
1562
- };
1563
- /**
1564
- * The `useMetadata` function in JavaScript is used to dynamically update metadata tags in the document
1565
- * head based on provided tags and options.
1566
- * @param [tags] - The `tags` parameter in the `useMetadata` function is an object that contains
1567
- * metadata information for the webpage. It can include properties like `pageTitle`, `canonical`, and
1568
- * other custom metadata tags like `og:title`, `og:description`, `twitter:title`,
1569
- * `twitter:description`, etc. These tags
1570
- * @param [options] - The `options` parameter in the `useMetadata` function is an object that can
1571
- * contain the following properties:
1572
- * - `title`: An object that can have the following properties:
1573
- * - `template`: A string that defines the template for the page title. It can include a placeholder
1574
- * `%s` that will be replaced with the actual page title.
1575
- * - `prefix`: A string that will be used as the default title if no specific page title is provided.
1576
- * This hook can't be reached by google crawler.
1577
- * @param {RyunixMetadataTags} [tags]
1578
- * @param {RyunixMetadataOptions} [options]
1579
- * @returns {void}
1580
- */
1581
- const useMetadata = (tags = {}, options = {}) => {
1231
+ const recoverHydrationFailureIfNeeded = () => {
1582
1232
  const state = getState();
1583
- if (state.isServerRendering) {
1584
- state.ssrMetadata = { ...state.ssrMetadata, ...tags };
1233
+ if (!state.hydrationFailed || state.hydrationRecover)
1585
1234
  return;
1586
- }
1587
- useEffect(() => {
1588
- if (typeof document === 'undefined')
1589
- return;
1590
- // ...
1591
- let finalTitle = 'Ryunix App';
1592
- const template = options.title?.template;
1593
- const defaultTitle = options.title?.prefix || 'Ryunix App';
1594
- const pageTitle = tags.pageTitle || tags.title;
1595
- if (is.string(pageTitle) && pageTitle.trim()) {
1596
- finalTitle = template?.includes('%s')
1597
- ? template.replace('%s', pageTitle)
1598
- : pageTitle;
1235
+ const policy = getHydrationPolicy();
1236
+ if (policy.recover === 'none')
1237
+ return;
1238
+ const container = state.containerRoot || state.currentRoot?.dom;
1239
+ const element = getRootChild(state.currentRoot?.props?.children);
1240
+ if (!container || element == null)
1241
+ return;
1242
+ state.hydrationRecover = true;
1243
+ state.hydrationFailed = false;
1244
+ logHydrationFailure('');
1245
+ logHydrationRecovery();
1246
+ renderSubtree(element, container);
1247
+ };
1248
+ const runHydrationRecovery = () => {
1249
+ const state = getState();
1250
+ recoverScopedHydrationFailures();
1251
+ recoverHydrationFailureIfNeeded();
1252
+ state.hydrationRecover = false;
1253
+ };
1254
+
1255
+ let workQueue = [];
1256
+ let isWorkLoopScheduled = false;
1257
+ function performUnitOfWork(fiber) {
1258
+ const state = getState();
1259
+ const isFunctionComponent = fiber.type instanceof Function || typeof fiber.type === 'function';
1260
+ try {
1261
+ if (isFunctionComponent) {
1262
+ updateFunctionComponent(fiber);
1599
1263
  }
1600
1264
  else {
1601
- finalTitle = defaultTitle;
1265
+ updateHostComponent(fiber);
1602
1266
  }
1603
- document.title = finalTitle;
1604
- if (tags.canonical) {
1605
- let link = document.querySelector('link[rel="canonical"]');
1606
- if (!link) {
1607
- link = document.createElement('link');
1608
- link.setAttribute('rel', 'canonical');
1609
- document.head.appendChild(link);
1267
+ }
1268
+ catch (error) {
1269
+ if (process.env.NODE_ENV !== 'production') {
1270
+ console.error('[Ryunix ErrorBoundary] Caught error during render:', error);
1271
+ try {
1272
+ const fiberProps = fiber.props;
1273
+ const src = fiberProps?.__source;
1274
+ if (src && error && typeof error === 'object') {
1275
+ ;
1276
+ error.__ryunix_source = src;
1277
+ }
1278
+ let targetFiber = fiber;
1279
+ while (!error.__ryunix_source && targetFiber) {
1280
+ const targetProps = targetFiber.props;
1281
+ if (targetProps?.__source) {
1282
+ ;
1283
+ error.__ryunix_source = targetProps.__source;
1284
+ }
1285
+ targetFiber = targetFiber.parent;
1286
+ }
1610
1287
  }
1611
- link.setAttribute('href', tags.canonical);
1288
+ catch (_e) { }
1612
1289
  }
1613
- Object.entries(tags).forEach(([key, value]) => {
1614
- if (['title', 'pageTitle', 'canonical'].includes(key))
1615
- return;
1616
- const isProperty = key.startsWith('og:') || key.startsWith('twitter:');
1617
- const selector = `meta[${isProperty ? 'property' : 'name'}='${key}']`;
1618
- let meta = document.head.querySelector(selector);
1619
- if (!meta) {
1620
- meta = document.createElement('meta');
1621
- meta.setAttribute(isProperty ? 'property' : 'name', key);
1622
- document.head.appendChild(meta);
1290
+ let boundaryFiber = fiber.parent;
1291
+ let foundBoundary = false;
1292
+ while (boundaryFiber) {
1293
+ if (boundaryFiber.type &&
1294
+ boundaryFiber.type.ryunix_type ===
1295
+ 'RYUNIX_ERROR_BOUNDARY') {
1296
+ foundBoundary = true;
1297
+ break;
1623
1298
  }
1624
- meta.setAttribute('content', value);
1625
- });
1626
- }, [JSON.stringify(tags), JSON.stringify(options)]);
1627
- };
1628
- // Router Context
1629
- /** @type {ReturnType<typeof createContext>} */
1630
- const RouterContext = createContext('ryunix.navigation',
1631
- /** @type {RyunixRouterContextValue} */ {
1632
- location: '/',
1633
- params: {},
1634
- query: {},
1635
- /** @param {string} _path */
1636
- navigate: (_path) => { },
1637
- route: null,
1638
- });
1639
- /**
1640
- * @param {RyunixRoute[]} routes
1641
- * @param {string} path
1642
- * @returns {RouteMatch}
1643
- */
1644
- const findRoute = (routes, path) => {
1645
- const pathname = path.split('?')[0].split('#')[0];
1646
- const notFoundRoute = routes.find((route) => route.NotFound);
1647
- const notFound = notFoundRoute
1648
- ? { route: { component: notFoundRoute.NotFound }, params: {} }
1649
- : { route: { component: null }, params: {} };
1650
- for (const route of routes) {
1651
- if (route.subRoutes) {
1652
- const childRoute = findRoute(route.subRoutes, path);
1653
- if (childRoute)
1654
- return childRoute;
1299
+ boundaryFiber = boundaryFiber.parent;
1655
1300
  }
1656
- if (route.path === '*')
1657
- return notFound;
1658
- if (!route.path || typeof route.path !== 'string')
1659
- continue;
1660
- /** @type {{ key: string, isCatchAll: boolean }[]} */
1661
- const keys = [];
1662
- const pattern = new RegExp(`^${route.path.replace(/:(\.\.\.)?(\w+)/g, (
1663
- /** @type {string} */ match,
1664
- /** @type {string | undefined} */ isCatchAll,
1665
- /** @type {string} */ key) => {
1666
- keys.push({ key, isCatchAll: !!isCatchAll });
1667
- return isCatchAll ? '(.+)' : '([^/]+)';
1668
- })}$`);
1669
- const matchPath = pathname.match(pattern);
1670
- if (matchPath) {
1671
- const params = keys.reduce((acc, keyObj, index) => {
1672
- const val = matchPath[index + 1];
1673
- acc[keyObj.key] = keyObj.isCatchAll && val ? val.split('/') : val;
1674
- return acc;
1675
- },
1676
- /** @type {Record<string, string | string[]>} */ {});
1677
- return { route, params };
1301
+ if (foundBoundary && boundaryFiber) {
1302
+ if (process.env.NODE_ENV !== 'production') {
1303
+ console.warn('[Ryunix ErrorBoundary] Recovering tree at nearest boundary.');
1304
+ }
1305
+ boundaryFiber.stateError = error;
1306
+ fiber.child = null;
1307
+ return boundaryFiber;
1308
+ }
1309
+ else {
1310
+ console.error('[Ryunix] Fatal Uncaught Error. No ErrorBoundary was found in the tree to handle this exception:\n', error);
1311
+ state.nextUnitOfWork = null;
1312
+ return null;
1678
1313
  }
1679
1314
  }
1680
- return notFound;
1681
- };
1682
- /**
1683
- * @returns {string}
1684
- */
1685
- const getSsrPathname = () => {
1686
- const pathname = globalThis?.window?.location?.pathname;
1687
- if (typeof pathname === 'string' && pathname) {
1688
- return pathname.split('?')[0].split('#')[0];
1315
+ if (fiber.child) {
1316
+ return fiber.child;
1689
1317
  }
1690
- return '/';
1691
- };
1692
- /**
1693
- * The `RouterProvider` component manages routing in a Ryunix application by updating the location based
1694
- * on window events and providing context for the current route.
1695
- * @param {{ routes: RyunixRoute[], children?: import('../types/internal.js').RyunixNode }} props
1696
- * @returns {import('./createElement.js').RyunixElement}
1697
- */
1698
- const RouterProvider = ({ routes, children }) => {
1699
- // SSR: Return server-safe version without hooks
1700
- if (typeof window === 'undefined') {
1701
- const location = getSsrPathname();
1702
- const currentRouteData = findRoute(routes, location);
1703
- /** @type {RyunixRouterContextValue} */
1704
- const contextValue = {
1705
- location,
1706
- params: currentRouteData.params || {},
1707
- query: {},
1708
- navigate: () => { },
1709
- route: currentRouteData.route,
1710
- };
1711
- return createElement(
1712
- /** @type {string | symbol | Function} */ RouterContext.Provider, { value: contextValue }, Fragment({ children }));
1318
+ let nextFiber = fiber;
1319
+ while (nextFiber) {
1320
+ if (state.isHydrating && nextFiber.dom) {
1321
+ state.hydrateCursor = nextValidSibling$1(nextFiber.dom.nextSibling);
1322
+ }
1323
+ if (nextFiber.sibling) {
1324
+ return nextFiber.sibling;
1325
+ }
1326
+ nextFiber = nextFiber.parent;
1713
1327
  }
1714
- const [location, setLocation] =
1715
- /** @type {[string, (action: unknown, priority?: number) => void]} */ useStore(window.location.pathname);
1716
- useEffect(() => {
1717
- const update = () => setLocation(window.location.pathname);
1718
- window.addEventListener('popstate', update);
1719
- window.addEventListener('hashchange', update);
1720
- return () => {
1721
- window.removeEventListener('popstate', update);
1722
- window.removeEventListener('hashchange', update);
1723
- };
1724
- }, []);
1725
- /** @param {string} path */
1726
- const navigate = (path) => {
1727
- if (typeof window !== 'undefined' && window.__RYUNIX_MPA__) {
1728
- window.location.assign(path);
1729
- return;
1328
+ return null;
1329
+ }
1330
+ const workLoop = (deadline) => {
1331
+ const state = getState();
1332
+ let shouldYield = false;
1333
+ while ((state.nextUnitOfWork || workQueue.length > 0) && !shouldYield) {
1334
+ if (!state.nextUnitOfWork && workQueue.length > 0) {
1335
+ const nextRoot = workQueue.shift();
1336
+ if (!nextRoot)
1337
+ continue;
1338
+ state.wipRoot = nextRoot;
1339
+ state.nextUnitOfWork = nextRoot;
1340
+ state.deletions = [];
1341
+ if (nextRoot.isHydrating !== undefined) {
1342
+ state.isHydrating = nextRoot.isHydrating;
1343
+ state.hydrateCursor = nextRoot.hydrateCursor;
1344
+ }
1730
1345
  }
1731
- window.history.pushState({}, '', path);
1732
- setLocation(path);
1733
- };
1734
- const currentRouteData = findRoute(routes, location);
1735
- const query = useQuery();
1736
- /** @type {RyunixRouterContextValue} */
1737
- const contextValue = {
1738
- location,
1739
- params: currentRouteData.params || {},
1740
- query,
1741
- navigate,
1742
- route: currentRouteData.route,
1743
- };
1744
- return createElement(
1745
- /** @type {string | symbol | Function} */ RouterContext.Provider, { value: contextValue }, Fragment({ children }));
1746
- };
1747
- /**
1748
- * The function `useRouter` returns the context of the Router for navigation in a Ryunix application.
1749
- * @returns {RyunixRouterContextValue}
1750
- */
1751
- const useRouter = () => {
1752
- return RouterContext.useContext('ryunix.navigation');
1346
+ if (state.nextUnitOfWork) {
1347
+ state.nextUnitOfWork = performUnitOfWork(state.nextUnitOfWork);
1348
+ }
1349
+ shouldYield = deadline.timeRemaining() < 1;
1350
+ }
1351
+ if (!state.nextUnitOfWork && state.wipRoot) {
1352
+ commitRoot();
1353
+ runHydrationRecovery();
1354
+ }
1355
+ if (state.nextUnitOfWork || workQueue.length > 0) {
1356
+ rIC(workLoop);
1357
+ }
1358
+ else {
1359
+ isWorkLoopScheduled = false;
1360
+ }
1753
1361
  };
1754
- /**
1755
- * The `Children` function in JavaScript uses router hooks to handle scrolling to a specific element
1756
- * based on the hash in the URL.
1757
- * @returns {import('./createElement.js').RyunixElement | null}
1758
- */
1759
- const Children = () => {
1760
- const { route, params, query, location } = useRouter();
1761
- if (!route || !route.component)
1762
- return null;
1763
- const hash = useHash();
1764
- useEffect(() => {
1765
- if (hash) {
1766
- const id = /** @type {string} */ hash.slice(1);
1767
- const el = document.getElementById(id);
1768
- if (el)
1769
- el.scrollIntoView({ block: 'start', behavior: 'smooth' });
1362
+ const scheduleWork = (root, priority = getCurrentPriority()) => {
1363
+ const state = getState();
1364
+ if (state.wipRoot) {
1365
+ workQueue.push(root);
1366
+ }
1367
+ else {
1368
+ state.nextUnitOfWork = root;
1369
+ state.wipRoot = root;
1370
+ state.deletions = [];
1371
+ if (root.isHydrating !== undefined) {
1372
+ state.isHydrating = root.isHydrating;
1373
+ state.hydrateCursor = root.hydrateCursor;
1770
1374
  }
1771
- }, [hash]);
1772
- return createElement(
1773
- /** @type {string | symbol | Function} */ route.component, {
1774
- key: location,
1775
- params,
1776
- query,
1777
- hash,
1778
- location,
1779
- });
1375
+ }
1376
+ state.hookIndex = 0;
1377
+ state.effects = [];
1378
+ if (!isWorkLoopScheduled) {
1379
+ isWorkLoopScheduled = true;
1380
+ if (priority <= Priority.USER_BLOCKING) {
1381
+ Promise.resolve().then(() => {
1382
+ workLoop({ timeRemaining: () => 10, didTimeout: true });
1383
+ });
1384
+ }
1385
+ else {
1386
+ rIC(workLoop);
1387
+ }
1388
+ }
1780
1389
  };
1781
- /**
1782
- * usePathname - Returns the current pathname
1783
- * @returns {string}
1784
- */
1785
- const usePathname = () => {
1786
- const { location } = useRouter();
1787
- return location.split('?')[0].split('#')[0];
1390
+ setScheduleWork(scheduleWork);
1391
+
1392
+ const render = (element, container) => {
1393
+ const state = getState();
1394
+ clearContainer(container);
1395
+ const root = {
1396
+ dom: container,
1397
+ props: {
1398
+ children: [element],
1399
+ },
1400
+ alternate: state.currentRoot,
1401
+ isHydrating: false,
1402
+ hydrateCursor: null,
1403
+ };
1404
+ scheduleWork(root);
1405
+ return root;
1788
1406
  };
1789
- /**
1790
- * useSearchParams - Returns the current URLSearchParams object
1791
- * @returns {URLSearchParams}
1792
- */
1793
- const useSearchParams = () => {
1794
- const { query } = useRouter();
1795
- return new URLSearchParams(query);
1407
+ const SSR_ROOT_ATTR = 'data-ryunix-ssr-root';
1408
+ const nextValidSibling = (node) => {
1409
+ let next = node;
1410
+ while (next &&
1411
+ ((next.nodeType === 3 && !next.nodeValue?.trim()) ||
1412
+ next.nodeType === 8 ||
1413
+ (next.nodeType === 1 &&
1414
+ next.hasAttribute('data-ryunix-ssr')))) {
1415
+ next = next.nextSibling;
1416
+ }
1417
+ return next;
1796
1418
  };
1797
- /**
1798
- * Link - Base link component for SPA navigation
1799
- * Supports optional prefetching of lazy components.
1800
- * @param {{ to: string, prefetch?: boolean, children?: import('../types/internal.js').RyunixNode } & Record<string, unknown>} props
1801
- * @returns {import('./createElement.js').RyunixElement}
1802
- */
1803
- const Link = ({ to, prefetch = true, ...props }) => {
1804
- const { navigate } = useRouter();
1805
- /** @param {MouseEvent} e */
1806
- const handleClick = (e) => {
1807
- if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
1808
- return;
1809
- }
1810
- e.preventDefault();
1811
- navigate(to);
1812
- };
1813
- const handleMouseEnter = () => {
1419
+ const hydrate = (element, container) => {
1420
+ const state = getState();
1421
+ state.containerRoot = container;
1422
+ const root = {
1423
+ dom: container,
1424
+ props: {
1425
+ children: [element],
1426
+ },
1427
+ alternate: state.currentRoot,
1428
+ isHydrating: true,
1429
+ hydrateCursor: nextValidSibling(container.firstChild),
1814
1430
  };
1815
- const { className: _omitClassName, ...cleanedProps } = props;
1816
- return createElement('a', {
1817
- href: to,
1818
- onClick: handleClick,
1819
- onMouseEnter: handleMouseEnter,
1820
- className: props.className || props['ryunix-class'],
1821
- ...cleanedProps,
1822
- }, props.children);
1431
+ scheduleWork(root);
1432
+ return root;
1823
1433
  };
1824
- /**
1825
- * The NavLink function in JavaScript is a component that generates a link element with customizable
1826
- * classes and active state based on the current location.
1827
- * @param {{ to: string, exact?: boolean, children?: import('../types/internal.js').RyunixNode } & Record<string, unknown>} props
1828
- * @returns {import('./createElement.js').RyunixElement}
1829
- */
1830
- const NavLink = ({ to, exact = false, ...props }) => {
1831
- const { location, navigate } = useRouter();
1832
- const isActive = exact ? location === to : location.startsWith(to);
1833
- /** @param {string | ((args: { isActive: boolean }) => string) | undefined} cls */
1834
- const resolveClass = (cls) => typeof cls === 'function' ? cls({ isActive }) : cls || '';
1835
- /** @param {MouseEvent} e */
1836
- const handleClick = (e) => {
1837
- if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
1838
- return;
1434
+ const init = (MainElement, root = '__ryunix', _components = {}) => {
1435
+ const state = getState();
1436
+ const container = document.getElementById(root);
1437
+ state.containerRoot = container;
1438
+ if (!container) {
1439
+ if (process.env.NODE_ENV !== 'production') {
1440
+ console.warn(`[Ryunix] init: container #${root} not found.`);
1839
1441
  }
1840
- e.preventDefault();
1841
- navigate(to);
1842
- };
1843
- const classAttrName = props['ryunix-class'] ? 'ryunix-class' : 'className';
1844
- const classAttrValue = resolveClass(
1845
- /** @type {string | ((args: { isActive: boolean }) => string) | undefined} */ props['ryunix-class'] || props.className);
1846
- const { ['ryunix-class']: _omitRyunix, className: _omitClassName, ...cleanedProps } = props;
1847
- return createElement('a', {
1848
- href: to,
1849
- onClick: handleClick,
1850
- [classAttrName]: classAttrValue,
1851
- ...cleanedProps,
1852
- }, props.children);
1853
- };
1854
- /**
1855
- * useStore with priority support
1856
- * @param {unknown} initialState
1857
- * @returns {[unknown, (action: unknown, priority?: number) => void]}
1858
- */
1859
- const useStorePriority = (initialState) => {
1860
- /** @param {unknown} state @param {{ value: unknown, priority?: number }} action */
1861
- const reducer = (state, action) => typeof action === 'function'
1862
- ? /** @type {{ value: (s: unknown) => unknown }} */ action.value(state)
1863
- : action.value;
1864
- const [state, baseDispatch] = useReducer(reducer, initialState, undefined);
1865
- /** @param {unknown} action @param {number} [priority] */
1866
- const dispatch = (action, priority = Priority.NORMAL) => {
1867
- const wrappedAction = {
1868
- value: action,
1869
- priority,
1870
- };
1871
- scheduleUpdate(() => baseDispatch(wrappedAction, priority), priority);
1872
- };
1873
- return [state, dispatch];
1442
+ return undefined;
1443
+ }
1444
+ resetHydrationLogFlags();
1445
+ state.hydrationPolicy = getHydrationPolicy();
1446
+ state.scopedRecoveryQueue = [];
1447
+ state.hydrationRecover = false;
1448
+ state.isHydrating = false;
1449
+ state.hydrationFailed = false;
1450
+ if (state.currentRoot) {
1451
+ if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
1452
+ console.log(`[Ryunix Debug] init: existing root detected. Client render on #${root}`);
1453
+ }
1454
+ return render(MainElement, container);
1455
+ }
1456
+ const ssrEnabled = process.env.RYUNIX_SSR !== 'false';
1457
+ const isSsrPayload = ssrEnabled &&
1458
+ container.hasAttribute(SSR_ROOT_ATTR) &&
1459
+ container.hasChildNodes();
1460
+ if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
1461
+ console.log(`[Ryunix Debug] init: isSsrPayload=${isSsrPayload}, hasChildNodes=${container.hasChildNodes()}`);
1462
+ }
1463
+ if (isSsrPayload) {
1464
+ if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
1465
+ console.log(`[Ryunix Debug] init: hydrating SSR markup on #${root}`);
1466
+ }
1467
+ container.removeAttribute(SSR_ROOT_ATTR);
1468
+ return hydrate(MainElement, container);
1469
+ }
1470
+ if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
1471
+ console.log(`[Ryunix Debug] init: client render on #${root}`);
1472
+ }
1473
+ return render(MainElement, container);
1874
1474
  };
1875
- /**
1876
- * useTransition - Mark updates as non-urgent
1877
- * @returns {[boolean, (callback: () => void) => void]}
1878
- */
1879
- const useTransition = () => {
1880
- const [isPending, setIsPending] = useStorePriority(false);
1881
- /** @param {() => void} callback */
1882
- const startTransition = (callback) => {
1883
- setIsPending(true, Priority.IMMEDIATE);
1884
- setTimeout(() => {
1885
- runWithPriority(Priority.LOW, () => {
1886
- callback();
1887
- setIsPending(false, Priority.LOW);
1888
- });
1889
- }, 0);
1890
- };
1891
- return [/** @type {boolean} */ isPending, startTransition];
1475
+ const safeRender = (component, props, onError) => {
1476
+ try {
1477
+ return component(props);
1478
+ }
1479
+ catch (error) {
1480
+ if (process.env.NODE_ENV !== 'production') {
1481
+ console.error('Component error:', error);
1482
+ }
1483
+ if (onError)
1484
+ onError(error);
1485
+ return null;
1486
+ }
1892
1487
  };
1893
- /**
1894
- * useDeferredValue - Defer value updates
1895
- * @param {unknown} value
1896
- * @returns {unknown}
1897
- */
1898
- const useDeferredValue = (value) => {
1899
- const [deferredValue, setDeferredValue] = useStorePriority(value);
1900
- useEffect(() => {
1901
- const timeout = setTimeout(() => {
1902
- setDeferredValue(value, Priority.LOW);
1903
- }, 100);
1904
- return () => clearTimeout(timeout);
1905
- }, [value]);
1906
- return deferredValue;
1488
+
1489
+ let isBatching = false;
1490
+ let pendingUpdates = [];
1491
+ function batchUpdates(callback) {
1492
+ const wasBatching = isBatching;
1493
+ isBatching = true;
1494
+ try {
1495
+ callback();
1496
+ }
1497
+ finally {
1498
+ isBatching = wasBatching;
1499
+ if (!isBatching && pendingUpdates.length > 0) {
1500
+ flushUpdates();
1501
+ }
1502
+ }
1503
+ }
1504
+ function queueUpdate(update) {
1505
+ pendingUpdates.push(update);
1506
+ if (!isBatching) {
1507
+ flushUpdates();
1508
+ }
1509
+ }
1510
+ function flushUpdates() {
1511
+ if (pendingUpdates.length === 0)
1512
+ return;
1513
+ const updates = pendingUpdates;
1514
+ pendingUpdates = [];
1515
+ updates.forEach((update) => update());
1516
+ }
1517
+
1518
+ process.env.NODE_ENV !== 'production';
1519
+ const validateHookContext = (hookName = 'A hook') => {
1520
+ const state = getState();
1521
+ if (!state.wipFiber) {
1522
+ throw new Error(`${hookName} can only be called inside function components. ` +
1523
+ 'Make sure you are calling hooks at the top level of your component.');
1524
+ }
1525
+ const wipFiber = state.wipFiber;
1526
+ if (!Array.isArray(wipFiber.hooks)) {
1527
+ wipFiber.hooks = [];
1528
+ }
1907
1529
  };
1908
- /**
1909
- * The `usePersitentStore` function manages state using local storage in JavaScript, allowing for easy
1910
- * storage and retrieval of data.
1911
- * @param key - The `key` parameter in the `usePersitentStore` function is a string that represents the key
1912
- * under which the data will be stored in the browser's local storage. It is used to retrieve and store
1913
- * data associated with this specific key.
1914
- * @param [initialState] - The `initialState` parameter in the `usePersitentStore` function is the initial
1915
- * value that will be used if there is no data stored in the local storage under the specified `key`.
1916
- * It serves as the default value for the state if no data is retrieved from the local storage.
1917
- * @param {string} key
1918
- * @param {unknown} [initialState]
1919
- * @returns {[unknown, (value: unknown) => void]}
1920
- */
1921
- const usePersistentStore = (key, initialState = '') => {
1922
- const [state, dispatch] = useStore(() => {
1923
- try {
1924
- const item = window.localStorage.getItem(key);
1925
- return item ? JSON.parse(item) : initialState;
1530
+
1531
+ const INTERNAL_META_KEYS = new Set([
1532
+ 'title',
1533
+ 'pageTitle',
1534
+ 'canonical',
1535
+ 'titleTemplate',
1536
+ 'titleDefault',
1537
+ 'lastmod',
1538
+ 'changefreq',
1539
+ 'priority',
1540
+ 'custom',
1541
+ 'icon',
1542
+ 'appleTouchIcon',
1543
+ ]);
1544
+ const isTitleConfig = (value) => is.object(value) &&
1545
+ value !== null &&
1546
+ ('default' in value || 'template' in value);
1547
+ const pickString = (value) => typeof value === 'string' && value.trim() ? value.trim() : undefined;
1548
+ function resolvePageMetadata(meta = {}, options = {}) {
1549
+ const template = pickString(options.title?.template) ||
1550
+ pickString(meta.titleTemplate) ||
1551
+ (isTitleConfig(meta.title) ? pickString(meta.title.template) : undefined);
1552
+ const defaultTitle = pickString(options.title?.prefix) ||
1553
+ pickString(meta.titleDefault) ||
1554
+ (isTitleConfig(meta.title) ? pickString(meta.title.default) : undefined) ||
1555
+ 'Ryunix App';
1556
+ const pageTitle = pickString(meta.pageTitle) ||
1557
+ (typeof meta.title === 'string' ? pickString(meta.title) : undefined);
1558
+ let title = defaultTitle;
1559
+ if (pageTitle) {
1560
+ title =
1561
+ template && template.includes('%s')
1562
+ ? template.replace('%s', pageTitle)
1563
+ : pageTitle;
1564
+ }
1565
+ const tags = {};
1566
+ for (const [key, value] of Object.entries(meta)) {
1567
+ if (INTERNAL_META_KEYS.has(key))
1568
+ continue;
1569
+ if (key === 'title' && isTitleConfig(value))
1570
+ continue;
1571
+ if (Array.isArray(value)) {
1572
+ const items = value
1573
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
1574
+ .filter(Boolean);
1575
+ if (items.length > 0)
1576
+ tags[key] = items;
1577
+ continue;
1926
1578
  }
1927
- catch (error) {
1928
- return initialState;
1579
+ if (typeof value === 'string' && value.trim()) {
1580
+ tags[key] = value.trim();
1929
1581
  }
1930
- });
1931
- /**
1932
- * The function `setValue` dispatches a value and stores it in the local storage as a JSON string,
1933
- * handling any errors with a console log.
1934
- * @param value - The `value` parameter in the `setValue` function is the data that you want to set.
1935
- * It is dispatched to update the state and then stored in the browser's local storage after being
1936
- * converted to a JSON string.
1937
- * @param {unknown} value
1938
- */
1939
- const setValue = (value) => {
1940
- try {
1941
- dispatch(value);
1942
- window.localStorage.setItem(key, JSON.stringify(value));
1582
+ }
1583
+ if (pickString(meta.canonical)) {
1584
+ tags.canonical = meta.canonical;
1585
+ }
1586
+ if (pickString(meta.icon)) {
1587
+ tags.icon = meta.icon;
1588
+ }
1589
+ if (pickString(meta.appleTouchIcon)) {
1590
+ tags.appleTouchIcon = meta.appleTouchIcon;
1591
+ }
1592
+ return { title, tags };
1593
+ }
1594
+ function mergeRouteMetadata(base = {}, next = {}) {
1595
+ const merged = { ...base, ...next };
1596
+ if (typeof next.title === 'string' && isTitleConfig(base.title)) {
1597
+ if (!pickString(merged.titleTemplate) && pickString(base.title.template)) {
1598
+ merged.titleTemplate = base.title.template;
1943
1599
  }
1944
- catch (error) {
1945
- console.error(error);
1600
+ if (!pickString(merged.titleDefault) && pickString(base.title.default)) {
1601
+ merged.titleDefault = base.title.default;
1946
1602
  }
1947
- };
1948
- return [state, setValue];
1603
+ }
1604
+ return merged;
1605
+ }
1606
+
1607
+ const haveDepsChanged = (oldDeps, newDeps) => {
1608
+ if (!oldDeps || !newDeps)
1609
+ return true;
1610
+ if (oldDeps.length !== newDeps.length)
1611
+ return true;
1612
+ return oldDeps.some((dep, i) => !Object.is(dep, newDeps[i]));
1949
1613
  };
1950
- /**
1951
- * The `useSwitch` function returns a state value and a toggle function to switch the state between
1952
- * true and false.
1953
- * @param [initialState=false] - The `initialState` parameter in the `useSwitch` function is used to
1954
- * set the initial value of the state. If no value is provided when calling `useSwitch`, the default
1955
- * initial state will be `false`.
1956
- * @param {boolean} [initialState]
1957
- * @returns {[boolean, () => void]}
1958
- */
1959
- const useSwitch = (initialState = false) => {
1960
- const [state, dispatch] = useStore(initialState);
1961
- /**
1962
- * The function `toggle` toggles the state by dispatching the opposite value of the current state.
1963
- * Uses functional update to avoid stale closure issues with rapid calls.
1964
- */
1965
- const toggle = () => {
1966
- dispatch(/** @param {boolean} prev */ (prev) => !prev);
1614
+ const useStore = (initialState, priority = getCurrentPriority()) => {
1615
+ if (typeof window === 'undefined') {
1616
+ return [
1617
+ is.function(initialState)
1618
+ ? initialState()
1619
+ : initialState,
1620
+ () => { },
1621
+ ];
1622
+ }
1623
+ const state = getState();
1624
+ if (state.isServerRendering) {
1625
+ return [
1626
+ is.function(initialState)
1627
+ ? initialState()
1628
+ : initialState,
1629
+ () => { },
1630
+ ];
1631
+ }
1632
+ const reducer = (state, action) => is.function(action) ? action(state) : action;
1633
+ return useReducer(reducer, initialState, undefined, priority);
1634
+ };
1635
+ const useReducer = (reducer, initialState, init, defaultPriority = getCurrentPriority()) => {
1636
+ if (typeof window === 'undefined') {
1637
+ return [init ? init(initialState) : initialState, () => { }];
1638
+ }
1639
+ const state = getState();
1640
+ if (state.isServerRendering) {
1641
+ return [init ? init(initialState) : initialState, () => { }];
1642
+ }
1643
+ validateHookContext();
1644
+ const { hookIndex } = state;
1645
+ const wipFiber = state.wipFiber;
1646
+ if (!wipFiber.hooks)
1647
+ wipFiber.hooks = [];
1648
+ const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1649
+ const hook = {
1650
+ hookID: hookIndex,
1651
+ type: RYUNIX_TYPES.RYUNIX_STORE,
1652
+ state: oldHook ? oldHook.state : init ? init(initialState) : initialState,
1653
+ queue: [],
1967
1654
  };
1968
- return [/** @type {boolean} */ state, toggle];
1969
- };
1970
- /**
1971
- * useLayoutEffect - Like useEffect but runs synchronously after DOM mutations
1972
- * and before the browser paints. Use for DOM measurements.
1973
- * @param {() => void | (() => void)} callback - Effect callback
1974
- * @param {unknown[] | undefined} deps - Dependencies array
1975
- * @returns {void}
1976
- */
1977
- const useLayoutEffect = (callback, deps) => {
1978
- // SSR safety check - more reliable than state.isServerRendering
1655
+ if (oldHook?.queue) {
1656
+ oldHook.queue.forEach((action) => {
1657
+ try {
1658
+ hook.state = reducer(hook.state, action);
1659
+ }
1660
+ catch (error) {
1661
+ if (process.env.NODE_ENV !== 'production') {
1662
+ console.error('Error in reducer:', error);
1663
+ }
1664
+ }
1665
+ });
1666
+ }
1667
+ const dispatch = (action, priority = defaultPriority) => {
1668
+ if (action === undefined) {
1669
+ if (process.env.NODE_ENV !== 'production') {
1670
+ console.warn('dispatch called with undefined action');
1671
+ }
1672
+ return;
1673
+ }
1674
+ hook.queue.push(action);
1675
+ const currentState = getState();
1676
+ const activeRoot = currentState.currentRoot ||
1677
+ currentState.wipRoot;
1678
+ if (!activeRoot)
1679
+ return;
1680
+ const newRoot = {
1681
+ dom: activeRoot.dom,
1682
+ props: activeRoot.props,
1683
+ alternate: currentState.currentRoot || null,
1684
+ };
1685
+ queueUpdate(() => scheduleWork$1(newRoot, priority));
1686
+ };
1687
+ wipFiber.hooks[hookIndex] = hook;
1688
+ state.hookIndex++;
1689
+ return [hook.state, dispatch];
1690
+ };
1691
+ const useEffect = (callback, deps) => {
1979
1692
  if (typeof window === 'undefined') {
1980
1693
  return;
1981
1694
  }
@@ -1985,13 +1698,15 @@ const useLayoutEffect = (callback, deps) => {
1985
1698
  }
1986
1699
  validateHookContext();
1987
1700
  if (!is.function(callback)) {
1988
- throw new Error('useLayoutEffect callback must be a function');
1701
+ throw new Error('useEffect callback must be a function');
1989
1702
  }
1990
1703
  if (deps !== undefined && !Array.isArray(deps)) {
1991
- throw new Error('useLayoutEffect dependencies must be an array or undefined');
1704
+ throw new Error('useEffect dependencies must be an array or undefined');
1992
1705
  }
1993
1706
  const { hookIndex } = state;
1994
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1707
+ const wipFiber = state.wipFiber;
1708
+ if (!wipFiber.hooks)
1709
+ wipFiber.hooks = [];
1995
1710
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1996
1711
  const hasChanged = haveDepsChanged(oldHook?.deps, deps);
1997
1712
  const hook = {
@@ -2000,789 +1715,505 @@ const useLayoutEffect = (callback, deps) => {
2000
1715
  deps,
2001
1716
  effect: hasChanged ? callback : null,
2002
1717
  cancel: oldHook?.cancel,
2003
- isLayout: true, // Flag to run synchronously during commit
2004
1718
  };
2005
1719
  wipFiber.hooks[hookIndex] = hook;
2006
1720
  state.hookIndex++;
2007
1721
  };
2008
- // Counter for deterministic ID generation
2009
- let idCounter = 0;
2010
- /**
2011
- * Reset the idCounter for useId - call this before each SSR renderToString
2012
- * to ensure deterministic IDs across multiple renders
2013
- * @returns {void}
2014
- */
2015
- const resetIdCounter = () => {
2016
- idCounter = 0;
2017
- };
2018
- /**
2019
- * useId - Generate a deterministic, unique ID that is stable across SSR and hydration.
2020
- * @returns {string} A unique ID string
2021
- */
2022
- const useId = () => {
1722
+ const useRef = (initialValue) => {
1723
+ if (typeof window === 'undefined') {
1724
+ return { current: initialValue };
1725
+ }
2023
1726
  const state = getState();
2024
1727
  if (state.isServerRendering) {
2025
- // On server, use a simple incrementing counter (reset per renderToString call)
2026
- return `:r${idCounter++}:`;
1728
+ return { current: initialValue };
2027
1729
  }
2028
1730
  validateHookContext();
2029
1731
  const { hookIndex } = state;
2030
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1732
+ const wipFiber = state.wipFiber;
1733
+ if (!wipFiber.hooks)
1734
+ wipFiber.hooks = [];
2031
1735
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
2032
1736
  const hook = {
2033
1737
  hookID: hookIndex,
2034
1738
  type: RYUNIX_TYPES.RYUNIX_REF,
2035
1739
  value: oldHook
2036
- ? /** @type {{ value: string }} */ oldHook.value
2037
- : `:r${idCounter++}:`,
1740
+ ? oldHook.value
1741
+ : { current: initialValue },
2038
1742
  };
2039
1743
  wipFiber.hooks[hookIndex] = hook;
2040
1744
  state.hookIndex++;
2041
- return /** @type {string} */ /** @type {{ value: string }} */ hook.value;
2042
- };
2043
- /**
2044
- * useDebounce - Returns a debounced version of the value that only updates
2045
- * after the specified delay has passed since the last change.
2046
- * @param {unknown} value - Value to debounce
2047
- * @param {number} delay - Delay in milliseconds (default: 300)
2048
- * @returns {unknown} Debounced value
2049
- */
2050
- const useDebounce = (value, delay = 300) => {
2051
- const [debouncedValue, setDebouncedValue] = useStore(value);
2052
- useEffect(() => {
2053
- const timer = setTimeout(() => {
2054
- setDebouncedValue(value);
2055
- }, delay);
2056
- return () => clearTimeout(timer);
2057
- }, [value, delay]);
2058
- return debouncedValue;
1745
+ return hook.value;
2059
1746
  };
2060
- /**
2061
- * useThrottle - Returns a throttled version of the value that only updates
2062
- * at most once per specified interval.
2063
- * @param {unknown} value - Value to throttle
2064
- * @param {number} interval - Minimum interval in milliseconds (default: 300)
2065
- * @returns {unknown} Throttled value
2066
- */
2067
- const useThrottle = (value, interval = 300) => {
2068
- const [throttledValue, setThrottledValue] = useStore(value);
2069
- const lastUpdated = useRef(Date.now());
2070
- useEffect(() => {
2071
- const now = Date.now();
2072
- const elapsed = now - lastUpdated.current;
2073
- if (elapsed >= interval) {
2074
- lastUpdated.current = now;
2075
- setThrottledValue(value);
1747
+ const useMemo = (compute, deps) => {
1748
+ if (typeof window === 'undefined') {
1749
+ return compute();
1750
+ }
1751
+ const state = getState();
1752
+ if (state.isServerRendering) {
1753
+ return compute();
1754
+ }
1755
+ validateHookContext();
1756
+ if (!is.function(compute)) {
1757
+ throw new Error('useMemo callback must be a function');
1758
+ }
1759
+ if (!Array.isArray(deps)) {
1760
+ throw new Error('useMemo requires a dependencies array');
1761
+ }
1762
+ const { hookIndex } = state;
1763
+ const wipFiber = state.wipFiber;
1764
+ if (!wipFiber.hooks)
1765
+ wipFiber.hooks = [];
1766
+ const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1767
+ let value;
1768
+ if (oldHook && !haveDepsChanged(oldHook.deps, deps)) {
1769
+ value = oldHook.value;
1770
+ }
1771
+ else {
1772
+ try {
1773
+ value = compute();
2076
1774
  }
2077
- else {
2078
- const timer = setTimeout(() => {
2079
- lastUpdated.current = Date.now();
2080
- setThrottledValue(value);
2081
- }, interval - elapsed);
2082
- return () => clearTimeout(timer);
1775
+ catch (error) {
1776
+ if (process.env.NODE_ENV !== 'production') {
1777
+ console.error('Error in useMemo computation:', error);
1778
+ }
1779
+ throw error;
2083
1780
  }
2084
- }, [value, interval]);
2085
- return throttledValue;
2086
- };
2087
-
2088
- var hooks = /*#__PURE__*/Object.freeze({
2089
- __proto__: null,
2090
- Children: Children,
2091
- Link: Link,
2092
- NavLink: NavLink,
2093
- RouterProvider: RouterProvider,
2094
- createContext: createContext,
2095
- resetIdCounter: resetIdCounter,
2096
- useCallback: useCallback,
2097
- useDebounce: useDebounce,
2098
- useDeferredValue: useDeferredValue,
2099
- useEffect: useEffect,
2100
- useHash: useHash,
2101
- useId: useId,
2102
- useLayoutEffect: useLayoutEffect,
2103
- useMemo: useMemo,
2104
- useMetadata: useMetadata,
2105
- usePathname: usePathname,
2106
- usePersistentStore: usePersistentStore,
2107
- usePersitentStore: usePersistentStore,
2108
- useQuery: useQuery,
2109
- useReducer: useReducer,
2110
- useRef: useRef,
2111
- useRouter: useRouter,
2112
- useSearchParams: useSearchParams,
2113
- useStore: useStore,
2114
- useStorePriority: useStorePriority,
2115
- useSwitch: useSwitch,
2116
- useThrottle: useThrottle,
2117
- useTransition: useTransition
2118
- });
2119
-
2120
- const getHydrationPolicy = () => {
2121
- const env = typeof process !== 'undefined' && process.env ? process.env : {};
2122
- const recoverRaw = env.RYUNIX_HYDRATION_RECOVER || 'boundary';
2123
- const boundariesRaw = env.RYUNIX_HYDRATION_BOUNDARIES || 'route';
2124
- const strict = env.RYUNIX_HYDRATION_STRICT === 'true';
2125
- const recover = recoverRaw === 'none' || recoverRaw === 'root' ? recoverRaw : 'boundary';
2126
- const boundaries = boundariesRaw === 'server-only' || boundariesRaw === 'all-layouts'
2127
- ? boundariesRaw
2128
- : 'route';
2129
- return {
2130
- recover,
2131
- boundaries,
2132
- strict,
1781
+ }
1782
+ const hook = {
1783
+ hookID: hookIndex,
1784
+ type: RYUNIX_TYPES.RYUNIX_MEMO,
1785
+ value,
1786
+ deps,
2133
1787
  };
1788
+ wipFiber.hooks[hookIndex] = hook;
1789
+ state.hookIndex++;
1790
+ return value;
2134
1791
  };
2135
- /**
2136
- */
2137
- const findNearestHydrationBoundary = (fiber) => {
2138
- let current = fiber || null;
2139
- while (current) {
2140
- const props = current.props;
2141
- if (props &&
2142
- Object.prototype.hasOwnProperty.call(props, 'data-ryunix-hydrate-boundary')) {
2143
- return current;
2144
- }
2145
- const type = current.type;
2146
- const maybeTyped = type;
2147
- if (type &&
2148
- typeof type === 'function' &&
2149
- (maybeTyped?.ryunix_type === 'RYUNIX_HYDRATION_BOUNDARY' ||
2150
- maybeTyped?.ryunix_type === 'RYUNIX_SERVER_BOUNDARY')) {
2151
- return current;
2152
- }
2153
- current = current.parent || null;
1792
+ const useCallback = (callback, deps) => {
1793
+ if (!is.function(callback)) {
1794
+ throw new Error('useCallback requires a function as first argument');
2154
1795
  }
2155
- return null;
1796
+ return useMemo(() => callback, deps);
2156
1797
  };
2157
- /**
2158
- */
2159
- const getBoundaryDom = (fiber) => {
2160
- if (!fiber)
2161
- return null;
2162
- if (fiber.dom && fiber.dom.nodeType === 1) {
2163
- const el = fiber.dom;
2164
- if (el.hasAttribute('data-ryunix-hydrate-boundary'))
2165
- return el;
2166
- }
2167
- let child = fiber.child || null;
2168
- while (child) {
2169
- if (child.dom && child.dom.nodeType === 1) {
2170
- const el = child.dom;
2171
- if (el.hasAttribute('data-ryunix-hydrate-boundary'))
2172
- return el;
1798
+ const createContext = (contextId = RYUNIX_TYPES.RYUNIX_CONTEXT, defaultValue = {}) => {
1799
+ const Provider = ({ value, children, }) => {
1800
+ return createElement(RYUNIX_TYPES.RYUNIX_CONTEXT, { value, children, _contextId: contextId }, ...flattenArray([children]));
1801
+ };
1802
+ Provider._contextId = contextId;
1803
+ const useContext = (ctxID = contextId) => {
1804
+ const state = getState();
1805
+ if (state.isServerRendering) {
1806
+ const ssrContexts = state.ssrContexts;
1807
+ return ssrContexts && ssrContexts[ctxID] !== undefined
1808
+ ? ssrContexts[ctxID]
1809
+ : defaultValue;
2173
1810
  }
2174
- child = child.child || child.sibling || null;
2175
- }
2176
- return null;
2177
- };
2178
- /**
2179
- */
2180
- const skipHydrationSubtree = (cursor, boundaryRoot) => {
2181
- if (!cursor || !boundaryRoot)
2182
- return cursor;
2183
- if (cursor === boundaryRoot)
2184
- return boundaryRoot.nextSibling;
2185
- if (boundaryRoot.contains(cursor))
2186
- return boundaryRoot.nextSibling;
2187
- return cursor;
2188
- };
2189
- /**
2190
- */
2191
- const enqueueScopedRecovery = (boundaryFiber, boundaryDom, resumeCursor) => {
2192
- if (!boundaryFiber || !boundaryDom)
2193
- return;
2194
- const state = getState();
2195
- const queue = state.scopedRecoveryQueue || [];
2196
- queue.push({
2197
- boundaryFiber,
2198
- boundaryDom,
2199
- resumeCursor,
2200
- element: (Array.isArray(boundaryFiber.props?.children)
2201
- ? boundaryFiber.props.children[0]
2202
- : boundaryFiber.props?.children),
2203
- });
2204
- state.scopedRecoveryQueue = queue;
2205
- };
2206
-
2207
- /**
2208
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
2209
- * @typedef {import('../types/internal.js').RyunixComponent} RyunixComponent
2210
- * @typedef {import('../types/internal.js').RyunixNode} RyunixNode
2211
- */
2212
- /**
2213
- * @param {RyunixFiber} fiber
2214
- */
2215
- const updateFunctionComponent = (fiber) => {
2216
- const state = getState();
2217
- state.wipFiber = fiber;
2218
- state.hookIndex = 0;
2219
- /** @type {RyunixFiber} */ state.wipFiber.hooks = [];
2220
- if (state.isHydrating) {
2221
- fiber.effectTag = EFFECT_TAGS.HYDRATE;
2222
- }
2223
- // Memo bailout: skip re-render if props haven't changed
2224
- const componentType =
2225
- /** @type {RyunixComponent & { _arePropsEqual?: (prev: Record<string, unknown>, next: Record<string, unknown>) => boolean }} */ fiber.type;
2226
- if (componentType._isMemo && fiber.alternate) {
2227
- const { children: _pc, ...prevRest } = fiber.alternate.props || {};
2228
- const { children: _nc, ...nextRest } = fiber.props || {};
2229
- if (componentType._arePropsEqual?.(prevRest, nextRest)) {
2230
- fiber.hooks = fiber.alternate.hooks;
2231
- const oldChild = fiber.alternate.child;
2232
- if (oldChild) {
2233
- oldChild.parent = fiber;
2234
- fiber.child = oldChild;
1811
+ validateHookContext();
1812
+ let fiber = state.wipFiber;
1813
+ while (fiber) {
1814
+ if (fiber._contextId === ctxID && fiber._contextValue !== undefined) {
1815
+ return fiber._contextValue;
2235
1816
  }
2236
- return;
1817
+ const fiberType = fiber.type;
1818
+ if (fiberType?._contextId === ctxID && fiber.props?.value !== undefined) {
1819
+ return fiber.props.value;
1820
+ }
1821
+ fiber = fiber.parent;
2237
1822
  }
2238
- }
2239
- let children = [
2240
- /** @type {RyunixNode} */ /** @type {(props?: Record<string, unknown>) => unknown} */ componentType(fiber.props),
2241
- ];
2242
- if (componentType._contextId && fiber.props?.value !== undefined) {
2243
- fiber._contextId = componentType._contextId;
2244
- fiber._contextValue = fiber.props.value;
2245
- }
2246
- reconcileChildren(fiber, children);
1823
+ return defaultValue;
1824
+ };
1825
+ return {
1826
+ Provider: Provider,
1827
+ useContext,
1828
+ };
2247
1829
  };
2248
- /**
2249
- * @param {RyunixFiber | null | undefined} fiber
2250
- * @returns {boolean}
2251
- */
2252
- const isUnderClientOnlyBoundary = (fiber) => {
2253
- let current = fiber?.parent || null;
2254
- while (current) {
2255
- if (current._hydrateClientOnly)
2256
- return true;
2257
- current = current.parent || null;
1830
+ const useQuery = () => {
1831
+ if (typeof window === 'undefined')
1832
+ return {};
1833
+ const searchParams = new URLSearchParams(window.location.search);
1834
+ const query = {};
1835
+ for (const [key, value] of searchParams.entries()) {
1836
+ query[key] = value;
2258
1837
  }
2259
- return false;
1838
+ return query;
2260
1839
  };
2261
- /**
2262
- * @param {RyunixFiber} fiber
2263
- */
2264
- const updateHostComponent = (fiber) => {
1840
+ const useHash = () => {
1841
+ if (typeof window === 'undefined')
1842
+ return '';
1843
+ const [hash, setHash] = useStore(window.location.hash);
1844
+ useEffect(() => {
1845
+ const onHashChange = () => setHash(window.location.hash);
1846
+ window.addEventListener('hashchange', onHashChange);
1847
+ return () => window.removeEventListener('hashchange', onHashChange);
1848
+ }, []);
1849
+ return hash;
1850
+ };
1851
+ const useMetadata = (tags = {}, options = {}) => {
2265
1852
  const state = getState();
2266
- if (fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2267
- fiber._contextId =
2268
- /** @type {string | symbol | undefined} */ fiber.props?._contextId;
2269
- fiber._contextValue = fiber.props?.value;
2270
- }
2271
- const isPassthrough = fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT ||
2272
- fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT ||
2273
- fiber.type === Symbol.for('ryunix.portal');
2274
- if (state.isHydrating && isPassthrough) {
2275
- fiber.effectTag = EFFECT_TAGS.HYDRATE;
1853
+ if (state.isServerRendering) {
1854
+ state.ssrMetadata = mergeRouteMetadata((state.ssrMetadata || {}), tags);
1855
+ return;
2276
1856
  }
2277
- else if (state.isHydrating && isUnderClientOnlyBoundary(fiber)) {
2278
- if (!fiber.dom) {
2279
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
2280
- fiber.effectTag = EFFECT_TAGS.PLACEMENT;
1857
+ useEffect(() => {
1858
+ if (typeof document === 'undefined')
1859
+ return;
1860
+ let finalTitle = 'Ryunix App';
1861
+ const template = options.title?.template;
1862
+ const defaultTitle = options.title?.prefix || 'Ryunix App';
1863
+ const pageTitle = tags.pageTitle || tags.title;
1864
+ if (is.string(pageTitle) && pageTitle.trim()) {
1865
+ finalTitle = template?.includes('%s')
1866
+ ? template.replace('%s', pageTitle)
1867
+ : pageTitle;
2281
1868
  }
2282
- }
2283
- else if (!fiber.dom) {
2284
- if (state.isHydrating && state.hydrateCursor) {
2285
- const domNode = state.hydrateCursor;
2286
- const isText = fiber.type === RYUNIX_TYPES.TEXT_ELEMENT && domNode.nodeType === 3;
2287
- const isElement = typeof fiber.type === 'string' &&
2288
- domNode.nodeType === 1 &&
2289
- domNode.tagName.toLowerCase() === fiber.type.toLowerCase();
2290
- if (isText || isElement) {
2291
- fiber.dom = /** @type {HTMLElement | Text} */ domNode;
2292
- fiber.effectTag = EFFECT_TAGS.HYDRATE;
2293
- if (isText &&
2294
- fiber.props?.nodeValue != null &&
2295
- domNode.nodeValue !== String(fiber.props.nodeValue)) {
2296
- domNode.nodeValue = String(fiber.props.nodeValue);
2297
- logHydrationRecoverable('text');
2298
- }
2299
- if (isElement &&
2300
- domNode.hasAttribute('data-ryunix-hydrate-boundary')) {
2301
- fiber._hydrateClientOnly = true;
2302
- }
2303
- state.hydrateCursor = nextValidSibling$1(domNode.firstChild);
1869
+ else {
1870
+ finalTitle = defaultTitle;
1871
+ }
1872
+ document.title = finalTitle;
1873
+ if (tags.canonical) {
1874
+ let link = document.querySelector('link[rel="canonical"]');
1875
+ if (!link) {
1876
+ link = document.createElement('link');
1877
+ link.setAttribute('rel', 'canonical');
1878
+ document.head.appendChild(link);
2304
1879
  }
2305
- else {
2306
- const policy = getHydrationPolicy();
2307
- const detail = `Mismatch at ${getTypeLabel(fiber.type)}. Expected ${domNode.nodeType === 1 ? domNode.tagName : 'text'} but got ${String(fiber.type)}.`;
2308
- const boundaryFiber = findNearestHydrationBoundary(fiber);
2309
- const boundaryDom = boundaryFiber ? getBoundaryDom(boundaryFiber) : null;
2310
- if (policy.recover === 'boundary' && boundaryFiber && boundaryDom) {
2311
- logHydrationBoundaryMismatch(detail);
2312
- enqueueScopedRecovery(boundaryFiber, boundaryDom, state.hydrateCursor ?? null);
2313
- state.hydrateCursor = skipHydrationSubtree(state.hydrateCursor ?? null, boundaryDom);
2314
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
2315
- fiber.effectTag = EFFECT_TAGS.PLACEMENT;
2316
- }
2317
- else if (policy.recover === 'none') {
2318
- logHydrationFatal(detail);
2319
- state.isHydrating = false;
2320
- state.hydrateCursor = null;
2321
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
2322
- fiber.effectTag = EFFECT_TAGS.PLACEMENT;
2323
- }
2324
- else {
2325
- logHydrationMismatch(detail);
2326
- state.isHydrating = false;
2327
- state.hydrationFailed = true;
2328
- state.hydrateCursor = null;
2329
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
2330
- fiber.effectTag = EFFECT_TAGS.PLACEMENT;
2331
- }
1880
+ link.setAttribute('href', tags.canonical);
1881
+ }
1882
+ Object.entries(tags).forEach(([key, value]) => {
1883
+ if (['title', 'pageTitle', 'canonical'].includes(key))
1884
+ return;
1885
+ if (value == null)
1886
+ return;
1887
+ const isProperty = key.startsWith('og:') || key.startsWith('twitter:');
1888
+ const selector = `meta[${isProperty ? 'property' : 'name'}='${key}']`;
1889
+ let meta = document.head.querySelector(selector);
1890
+ if (!meta) {
1891
+ meta = document.createElement('meta');
1892
+ meta.setAttribute(isProperty ? 'property' : 'name', key);
1893
+ document.head.appendChild(meta);
2332
1894
  }
1895
+ meta.setAttribute('content', value);
1896
+ });
1897
+ }, [JSON.stringify(tags), JSON.stringify(options)]);
1898
+ };
1899
+ const RouterContext = createContext('ryunix.navigation', {
1900
+ location: '/',
1901
+ params: {},
1902
+ query: {},
1903
+ navigate: (_path) => { },
1904
+ route: null,
1905
+ });
1906
+ const findRoute = (routes, path) => {
1907
+ const pathname = path.split('?')[0].split('#')[0];
1908
+ const notFoundRoute = routes.find((route) => route.NotFound);
1909
+ const notFound = notFoundRoute
1910
+ ? { route: { component: notFoundRoute.NotFound ?? null }, params: {} }
1911
+ : { route: { component: null }, params: {} };
1912
+ for (const route of routes) {
1913
+ if (route.subRoutes) {
1914
+ const childRoute = findRoute(route.subRoutes, path);
1915
+ if (childRoute)
1916
+ return childRoute;
2333
1917
  }
2334
- else {
2335
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
1918
+ if (route.path === '*')
1919
+ return notFound;
1920
+ if (!route.path || typeof route.path !== 'string')
1921
+ continue;
1922
+ const keys = [];
1923
+ const pattern = new RegExp(`^${route.path.replace(/:(\.\.\.)?(\w+)/g, (match, isCatchAll, key) => {
1924
+ keys.push({ key, isCatchAll: !!isCatchAll });
1925
+ return isCatchAll ? '(.+)' : '([^/]+)';
1926
+ })}$`);
1927
+ const matchPath = pathname.match(pattern);
1928
+ if (matchPath) {
1929
+ const params = keys.reduce((acc, keyObj, index) => {
1930
+ const val = matchPath[index + 1];
1931
+ acc[keyObj.key] = keyObj.isCatchAll && val ? val.split('/') : val;
1932
+ return acc;
1933
+ }, {});
1934
+ return { route, params };
2336
1935
  }
2337
1936
  }
2338
- const children = fiber.props?.children || [];
2339
- reconcileChildren(fiber, children);
2340
- };
2341
- /**
2342
- * @param {string | symbol | RyunixComponent | object} type
2343
- * @returns {string}
2344
- */
2345
- const getTypeLabel = (type) => {
2346
- if (typeof type === 'symbol')
2347
- return type.description || type.toString();
2348
- if (typeof type === 'function')
2349
- return type.name || 'anonymous';
2350
- return String(type);
1937
+ return notFound;
2351
1938
  };
2352
- /**
2353
- * The Component `Image` takes in a `src` and other props, and returns an `img` element with the
2354
- * specified `src` and props.
2355
- * @returns The `Image` component is being returned. It is a functional component that renders an `img`
2356
- * element with the specified `src` and other props passed to it.
2357
- */
2358
- /**
2359
- * @param {{ src: string } & Record<string, unknown>} props
2360
- * @returns {import('./createElement.js').RyunixElement}
2361
- */
2362
- const Image = ({ src, ...props }) => {
2363
- return createElement('img', { ...props, src });
1939
+ const getSsrPathname = () => {
1940
+ const pathname = globalThis?.window?.location?.pathname;
1941
+ if (typeof pathname === 'string' && pathname) {
1942
+ return pathname.split('?')[0].split('#')[0];
1943
+ }
1944
+ return '/';
2364
1945
  };
2365
- const { Provider: MDXProvider, useContext: useMDXComponents } = createContext('ryunix.mdx',
2366
- /** @type {Record<string, RyunixComponent>} */ {});
2367
- /**
2368
- * Get merged MDX components from context and provided components
2369
- * @param {Record<string, RyunixComponent>} [components] - Additional components to merge
2370
- * @returns {Record<string, RyunixComponent>} Merged components object
2371
- */
2372
- const getMDXComponents = (components) => {
2373
- const contextComponents = useMDXComponents();
2374
- return {
2375
- ...contextComponents,
2376
- ...components,
1946
+ const RouterProvider = ({ routes, children, }) => {
1947
+ if (typeof window === 'undefined') {
1948
+ const location = getSsrPathname();
1949
+ const currentRouteData = findRoute(routes, location);
1950
+ const contextValue = {
1951
+ location,
1952
+ params: currentRouteData.params || {},
1953
+ query: {},
1954
+ navigate: () => { },
1955
+ route: currentRouteData.route,
1956
+ };
1957
+ return createElement(RouterContext.Provider, { value: contextValue }, Fragment({ children }));
1958
+ }
1959
+ const [location, setLocation] = useStore(window.location.pathname);
1960
+ useEffect(() => {
1961
+ const update = () => setLocation(window.location.pathname);
1962
+ window.addEventListener('popstate', update);
1963
+ window.addEventListener('hashchange', update);
1964
+ return () => {
1965
+ window.removeEventListener('popstate', update);
1966
+ window.removeEventListener('hashchange', update);
1967
+ };
1968
+ }, []);
1969
+ const navigate = (path) => {
1970
+ if (typeof window !== 'undefined' && window.__RYUNIX_MPA__) {
1971
+ window.location.assign(path);
1972
+ return;
1973
+ }
1974
+ window.history.pushState({}, '', path);
1975
+ setLocation(path);
2377
1976
  };
2378
- };
2379
- /**
2380
- * @param {string} tag
2381
- * @param {Record<string, unknown>} props
2382
- * @returns {RyunixNode}
2383
- */
2384
- const mdxHost = (tag, props) =>
2385
- /** @type {RyunixNode} */ createElement(tag, props);
2386
- /**
2387
- * Default MDX components with Ryunix-optimized rendering
2388
- * @type {Record<string, (props: Record<string, unknown>) => RyunixNode>}
2389
- */
2390
- const defaultComponents = {
2391
- // Headings
2392
- h1: (props) => mdxHost('h1', props),
2393
- h2: (props) => mdxHost('h2', props),
2394
- h3: (props) => mdxHost('h3', props),
2395
- h4: (props) => mdxHost('h4', props),
2396
- h5: (props) => mdxHost('h5', props),
2397
- h6: (props) => mdxHost('h6', props),
2398
- // Text
2399
- p: (props) => mdxHost('p', props),
2400
- a: (props) => mdxHost('a', props),
2401
- strong: (props) => mdxHost('strong', props),
2402
- em: (props) => mdxHost('em', props),
2403
- code: (props) => mdxHost('code', props),
2404
- // Lists
2405
- ul: (props) => mdxHost('ul', props),
2406
- ol: (props) => mdxHost('ol', props),
2407
- li: (props) => mdxHost('li', props),
2408
- // Blocks
2409
- blockquote: (props) => mdxHost('blockquote', props),
2410
- pre: (props) => mdxHost('pre', props),
2411
- hr: (props) => mdxHost('hr', props),
2412
- // Tables
2413
- table: (props) => mdxHost('table', props),
2414
- thead: (props) => mdxHost('thead', props),
2415
- tbody: (props) => mdxHost('tbody', props),
2416
- tr: (props) => mdxHost('tr', props),
2417
- th: (props) => mdxHost('th', props),
2418
- td: (props) => mdxHost('td', props),
2419
- // Media
2420
- img: (props) => mdxHost('img', props),
2421
- };
2422
- /**
2423
- * MDX Wrapper component
2424
- * Provides default styling and components for MDX content
2425
- */
2426
- /**
2427
- * @param {{ children?: RyunixNode, components?: Record<string, RyunixComponent> }} props
2428
- * @returns {import('./createElement.js').RyunixElement}
2429
- */
2430
- const MDXContent = ({ children, components = {} }) => {
2431
- const mergedComponents = getMDXComponents(components);
2432
- return createElement(
2433
- /** @type {string | symbol | Function} */ MDXProvider, { value: mergedComponents }, createElement('div', null, children));
2434
- };
2435
-
2436
- /**
2437
- * @param {RyunixNode} element
2438
- * @param {Element | DocumentFragment} container
2439
- */
2440
- const renderSubtree = (element, container) => {
2441
- clearContainer(/** @type {HTMLElement} */ container);
2442
- /** @type {RyunixRootFiber} */
2443
- const root = {
2444
- dom: container,
2445
- props: { children: [element] },
2446
- isHydrating: false,
2447
- hydrateCursor: null,
1977
+ const currentRouteData = findRoute(routes, location);
1978
+ const query = useQuery();
1979
+ const contextValue = {
1980
+ location: location,
1981
+ params: currentRouteData.params || {},
1982
+ query,
1983
+ navigate,
1984
+ route: currentRouteData.route,
2448
1985
  };
2449
- scheduleWork$1(root, undefined);
2450
- };
2451
- const recoverScopedHydrationFailures = () => {
2452
- const state = getState();
2453
- const queue = state.scopedRecoveryQueue;
2454
- if (!queue?.length)
2455
- return;
2456
- state.scopedRecoveryQueue = [];
2457
- for (const item of queue) {
2458
- logHydrationBoundaryRecovery();
2459
- renderSubtree(item.element, item.boundaryDom);
2460
- }
2461
- };
2462
- const recoverHydrationFailureIfNeeded = () => {
2463
- const state = getState();
2464
- if (!state.hydrationFailed || state.hydrationRecover)
2465
- return;
2466
- const policy = getHydrationPolicy();
2467
- if (policy.recover === 'none')
2468
- return;
2469
- const container = state.containerRoot || state.currentRoot?.dom;
2470
- const element = state.currentRoot?.props?.children?.[0];
2471
- if (!container || element == null)
2472
- return;
2473
- state.hydrationRecover = true;
2474
- state.hydrationFailed = false;
2475
- logHydrationFailure('');
2476
- logHydrationRecovery();
2477
- renderSubtree(/** @type {RyunixNode} */ element, container);
1986
+ return createElement(RouterContext.Provider, { value: contextValue }, Fragment({ children }));
2478
1987
  };
2479
- const runHydrationRecovery = () => {
2480
- recoverScopedHydrationFailures();
2481
- recoverHydrationFailureIfNeeded();
1988
+ const useRouter = () => {
1989
+ return RouterContext.useContext('ryunix.navigation');
2482
1990
  };
2483
-
2484
- /**
2485
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
2486
- * @typedef {import('../types/internal.js').RyunixRootFiber} RyunixRootFiber
2487
- */
2488
- /** @type {RyunixRootFiber[]} */
2489
- let workQueue = [];
2490
- /** @type {boolean} */
2491
- let isWorkLoopScheduled = false;
2492
- /**
2493
- * @param {RyunixFiber} fiber
2494
- * @returns {RyunixFiber | null}
2495
- */
2496
- function performUnitOfWork(fiber) {
2497
- const state = getState();
2498
- const isFunctionComponent = fiber.type instanceof Function || typeof fiber.type === 'function';
2499
- try {
2500
- if (isFunctionComponent) {
2501
- updateFunctionComponent(fiber);
2502
- }
2503
- else {
2504
- updateHostComponent(fiber);
2505
- }
2506
- }
2507
- catch (error) {
2508
- if (process.env.NODE_ENV !== 'production') {
2509
- console.error('[Ryunix ErrorBoundary] Caught error during render:', error);
2510
- try {
2511
- // Attempt to attach original JSX source map for DevOverlay lookup
2512
- const src = fiber.props && fiber.props.__source;
2513
- if (src && error && typeof error === 'object') {
2514
- error.__ryunix_source = src;
2515
- }
2516
- let targetFiber = fiber;
2517
- while (!error.__ryunix_source && targetFiber) {
2518
- if (targetFiber.props && targetFiber.props.__source) {
2519
- error.__ryunix_source = targetFiber.props.__source;
2520
- }
2521
- targetFiber = targetFiber.parent;
2522
- }
2523
- }
2524
- catch (e) { }
2525
- }
2526
- // Traverse upwards to find nearest ErrorBoundary
2527
- let boundaryFiber = fiber.parent;
2528
- let foundBoundary = false;
2529
- while (boundaryFiber) {
2530
- if (boundaryFiber.type &&
2531
- /** @type {{ ryunix_type?: string }} */ boundaryFiber.type
2532
- .ryunix_type === 'RYUNIX_ERROR_BOUNDARY') {
2533
- foundBoundary = true;
2534
- break;
2535
- }
2536
- boundaryFiber = boundaryFiber.parent;
2537
- }
2538
- if (foundBoundary) {
2539
- if (process.env.NODE_ENV !== 'production') {
2540
- console.warn('[Ryunix ErrorBoundary] Recovering tree at nearest boundary.');
2541
- }
2542
- // Assign the error state to the boundary so it can render the fallback
2543
- boundaryFiber.stateError = error;
2544
- // Discard the corrupted children of the crashing fiber to prevent undefined behavior
2545
- fiber.child = null;
2546
- // Rewind the rendering context to the ErrorBoundary fiber
2547
- // so the work loop immediately starts re-evaluating the boundary branch
2548
- return boundaryFiber;
2549
- }
2550
- else {
2551
- // Uncaught fatal error: stop the work loop entirely
2552
- console.error('[Ryunix] Fatal Uncaught Error. No ErrorBoundary was found in the tree to handle this exception:\n', error);
2553
- state.nextUnitOfWork = null;
2554
- return null;
2555
- }
2556
- }
2557
- if (fiber.child) {
2558
- return fiber.child;
2559
- }
2560
- let nextFiber = fiber;
2561
- while (nextFiber) {
2562
- // If we just finished a Host node during hydration,
2563
- // the next fiber (sibling) should start at the next DOM sibling.
2564
- if (state.isHydrating && nextFiber.dom) {
2565
- state.hydrateCursor = nextValidSibling$1(nextFiber.dom.nextSibling);
2566
- }
2567
- if (nextFiber.sibling) {
2568
- return nextFiber.sibling;
1991
+ const Children = () => {
1992
+ const { route, params, query, location } = useRouter();
1993
+ if (!route || !route.component)
1994
+ return null;
1995
+ const hash = useHash();
1996
+ useEffect(() => {
1997
+ if (hash) {
1998
+ const id = hash.slice(1);
1999
+ const el = document.getElementById(id);
2000
+ if (el)
2001
+ el.scrollIntoView({ block: 'start', behavior: 'smooth' });
2002
+ return;
2569
2003
  }
2570
- nextFiber = nextFiber.parent;
2571
- // When ascending, we don't need to do anything else,
2572
- // the loop will handle the parent's sibling or end.
2573
- }
2574
- }
2575
- /**
2576
- * @param {{ timeRemaining: () => number, didTimeout?: boolean }} deadline
2577
- */
2578
- const workLoop = (deadline) => {
2579
- const state = getState();
2580
- let shouldYield = false;
2581
- while ((state.nextUnitOfWork || workQueue.length > 0) && !shouldYield) {
2582
- if (!state.nextUnitOfWork && workQueue.length > 0) {
2583
- const nextRoot = workQueue.shift();
2584
- state.wipRoot = nextRoot;
2585
- state.nextUnitOfWork = nextRoot;
2586
- state.deletions = [];
2587
- // Restore specific hydration state for this root
2588
- if (nextRoot.isHydrating !== undefined) {
2589
- state.isHydrating = nextRoot.isHydrating;
2590
- state.hydrateCursor = nextRoot.hydrateCursor;
2591
- }
2004
+ if (typeof window !== 'undefined') {
2005
+ window.scrollTo(0, 0);
2592
2006
  }
2593
- if (state.nextUnitOfWork) {
2594
- state.nextUnitOfWork = performUnitOfWork(state.nextUnitOfWork);
2007
+ }, [location, hash]);
2008
+ return createElement(route.component, {
2009
+ params,
2010
+ query,
2011
+ hash,
2012
+ location,
2013
+ });
2014
+ };
2015
+ const usePathname = () => {
2016
+ const { location } = useRouter();
2017
+ return location.split('?')[0].split('#')[0];
2018
+ };
2019
+ const useSearchParams = () => {
2020
+ const { query } = useRouter();
2021
+ return new URLSearchParams(query);
2022
+ };
2023
+ const Link = ({ to, prefetch = true, ...props }) => {
2024
+ const { navigate } = useRouter();
2025
+ const handleClick = (e) => {
2026
+ if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
2027
+ return;
2595
2028
  }
2596
- shouldYield = deadline.timeRemaining() < 1;
2597
- }
2598
- if (!state.nextUnitOfWork && state.wipRoot) {
2599
- commitRoot();
2600
- runHydrationRecovery();
2601
- }
2602
- if (state.nextUnitOfWork || workQueue.length > 0) {
2603
- rIC(workLoop);
2604
- }
2605
- else {
2606
- isWorkLoopScheduled = false;
2607
- }
2029
+ e.preventDefault();
2030
+ navigate(to);
2031
+ };
2032
+ const handleMouseEnter = () => {
2033
+ };
2034
+ const { className: _omitClassName, ...cleanedProps } = props;
2035
+ return createElement('a', {
2036
+ href: to,
2037
+ onClick: handleClick,
2038
+ onMouseEnter: handleMouseEnter,
2039
+ className: (props.className || props['ryunix-class']),
2040
+ ...cleanedProps,
2041
+ }, props.children);
2608
2042
  };
2609
- /**
2610
- * @param {RyunixRootFiber} root
2611
- * @param {number} [priority]
2612
- */
2613
- const scheduleWork = (root, priority = getCurrentPriority()) => {
2614
- const state = getState();
2615
- if (state.wipRoot) {
2616
- workQueue.push(root);
2617
- }
2618
- else {
2619
- state.nextUnitOfWork = root;
2620
- state.wipRoot = root;
2621
- state.deletions = [];
2622
- // Set immediate hydration state
2623
- if (root.isHydrating !== undefined) {
2624
- state.isHydrating = root.isHydrating;
2625
- state.hydrateCursor = root.hydrateCursor;
2043
+ const NavLink = ({ to, exact = false, ...props }) => {
2044
+ const { location, navigate } = useRouter();
2045
+ const isActive = exact ? location === to : location.startsWith(to);
2046
+ const resolveClass = (cls) => (typeof cls === 'function' ? cls({ isActive }) : cls || '');
2047
+ const handleClick = (e) => {
2048
+ if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
2049
+ return;
2626
2050
  }
2627
- }
2628
- state.hookIndex = 0;
2629
- state.effects = [];
2630
- if (!isWorkLoopScheduled) {
2631
- isWorkLoopScheduled = true;
2632
- if (priority <= Priority.USER_BLOCKING) {
2633
- // High priority: run as soon as possible in a micro-task
2634
- // We provide a synthetic deadline that allows some work before yielding
2635
- Promise.resolve().then(() => {
2636
- workLoop({ timeRemaining: () => 10, didTimeout: true });
2051
+ e.preventDefault();
2052
+ navigate(to);
2053
+ };
2054
+ const classAttrName = props['ryunix-class'] ? 'ryunix-class' : 'className';
2055
+ const classAttrValue = resolveClass((props['ryunix-class'] || props.className));
2056
+ const { ['ryunix-class']: _omitRyunix, className: _omitClassName, ...cleanedProps } = props;
2057
+ return createElement('a', {
2058
+ href: to,
2059
+ onClick: handleClick,
2060
+ [classAttrName]: classAttrValue,
2061
+ ...cleanedProps,
2062
+ }, props.children);
2063
+ };
2064
+ const useStorePriority = (initialState) => {
2065
+ const reducer = (state, action) => typeof action === 'function'
2066
+ ? action.value(state)
2067
+ : action.value;
2068
+ const [state, baseDispatch] = useReducer(reducer, initialState, undefined);
2069
+ const dispatch = (action, priority = Priority.NORMAL) => {
2070
+ const wrappedAction = {
2071
+ value: action,
2072
+ priority,
2073
+ };
2074
+ scheduleUpdate(() => baseDispatch(wrappedAction, priority), priority);
2075
+ };
2076
+ return [state, dispatch];
2077
+ };
2078
+ const useTransition = () => {
2079
+ const [isPending, setIsPending] = useStorePriority(false);
2080
+ const startTransition = (callback) => {
2081
+ setIsPending(true, Priority.IMMEDIATE);
2082
+ setTimeout(() => {
2083
+ runWithPriority(Priority.LOW, () => {
2084
+ callback();
2085
+ setIsPending(false, Priority.LOW);
2637
2086
  });
2087
+ }, 0);
2088
+ };
2089
+ return [isPending, startTransition];
2090
+ };
2091
+ const useDeferredValue = (value) => {
2092
+ const [deferredValue, setDeferredValue] = useStorePriority(value);
2093
+ useEffect(() => {
2094
+ const timeout = setTimeout(() => {
2095
+ setDeferredValue(value, Priority.LOW);
2096
+ }, 100);
2097
+ return () => clearTimeout(timeout);
2098
+ }, [value]);
2099
+ return deferredValue;
2100
+ };
2101
+ const usePersistentStore = (key, initialState = '') => {
2102
+ const [state, dispatch] = useStore(() => {
2103
+ try {
2104
+ const item = window.localStorage.getItem(key);
2105
+ return item ? JSON.parse(item) : initialState;
2638
2106
  }
2639
- else {
2640
- // Low priority: wait for browser idleness
2641
- rIC(workLoop);
2107
+ catch (_error) {
2108
+ return initialState;
2109
+ }
2110
+ });
2111
+ const setValue = (value) => {
2112
+ try {
2113
+ dispatch(value);
2114
+ window.localStorage.setItem(key, JSON.stringify(value));
2115
+ }
2116
+ catch (error) {
2117
+ console.error(error);
2642
2118
  }
2643
- }
2644
- };
2645
- setScheduleWork(scheduleWork);
2646
-
2647
- /**
2648
- * @typedef {import('./createElement.js').RyunixNode} RyunixNode
2649
- * @typedef {import('../types/internal.js').RyunixRootFiber} RyunixRootFiber
2650
- * @typedef {import('../types/internal.js').RyunixComponent} RyunixComponent
2651
- */
2652
- /**
2653
- * The `render` function in JavaScript updates the DOM with a new element and schedules work to be done
2654
- * on the element.
2655
- * @param {RyunixNode} element
2656
- * @param {Element | DocumentFragment} container
2657
- * @returns {RyunixRootFiber}
2658
- */
2659
- const render = (element, container) => {
2660
- const state = getState();
2661
- // Clear container before CSR render to avoid duplication
2662
- clearContainer(/** @type {HTMLElement} */ container);
2663
- /** @type {RyunixRootFiber} */
2664
- const root = {
2665
- dom: container,
2666
- props: {
2667
- children: [
2668
- /** @type {import('../types/internal.js').RyunixNode} */ element,
2669
- ],
2670
- },
2671
- alternate: state.currentRoot,
2672
- isHydrating: false,
2673
- hydrateCursor: /** @type {ChildNode | null} */ null,
2674
2119
  };
2675
- scheduleWork(root);
2676
- return root;
2677
- };
2678
- /**
2679
- * @param {ChildNode | null} node
2680
- * @returns {ChildNode | null}
2681
- */
2682
- const nextValidSibling = (node) => {
2683
- let next = node;
2684
- while (next &&
2685
- ((next.nodeType === 3 && !next.nodeValue.trim()) ||
2686
- next.nodeType === 8 ||
2687
- (next.nodeType === 1 &&
2688
- /** @type {Element} */ next.hasAttribute('data-ryunix-ssr')))) {
2689
- next = next.nextSibling;
2690
- }
2691
- return next;
2120
+ return [state, setValue];
2692
2121
  };
2693
- /**
2694
- * The `hydrate` function attaches Ryunix to an existing server-rendered DOM tree.
2695
- * Instead of clearing and re-rendering, it walks the existing DOM nodes and
2696
- * attaches event listeners and reconciles state, preserving SSR HTML.
2697
- * @param {RyunixNode} element
2698
- * @param {Element | DocumentFragment} container
2699
- * @returns {RyunixRootFiber}
2700
- */
2701
- const hydrate = (element, container) => {
2702
- const state = getState();
2703
- state.containerRoot = container;
2704
- /** @type {RyunixRootFiber} */
2705
- const root = {
2706
- dom: container,
2707
- props: {
2708
- children: [
2709
- /** @type {import('../types/internal.js').RyunixNode} */ element,
2710
- ],
2711
- },
2712
- alternate: state.currentRoot,
2713
- isHydrating: true,
2714
- hydrateCursor: nextValidSibling(container.firstChild),
2122
+ const useSwitch = (initialState = false) => {
2123
+ const [state, dispatch] = useStore(initialState);
2124
+ const toggle = () => {
2125
+ dispatch((prev) => !prev);
2715
2126
  };
2716
- scheduleWork(root);
2717
- return root;
2127
+ return [state, toggle];
2718
2128
  };
2719
- /**
2720
- * @param {RyunixNode} MainElement
2721
- * @param {string} [root]
2722
- * @param {Record<string, unknown>} [components]
2723
- * @returns {RyunixRootFiber | undefined}
2724
- */
2725
- const init = (MainElement, root = '__ryunix', components = {}) => {
2129
+ const useLayoutEffect = (callback, deps) => {
2130
+ if (typeof window === 'undefined') {
2131
+ return;
2132
+ }
2726
2133
  const state = getState();
2727
- state.containerRoot = document.getElementById(root);
2728
- resetHydrationLogFlags();
2729
- state.hydrationPolicy = getHydrationPolicy();
2730
- state.scopedRecoveryQueue = [];
2731
- state.hydrationRecover = false;
2732
- // Reset any stale hydration flags
2733
- state.isHydrating = false;
2734
- state.hydrationFailed = false;
2735
- // Auto-detect SSR based on child nodes - no need to manually set process.env.RYUNIX_SSR
2736
- const hasChildNodes = state.containerRoot && state.containerRoot.hasChildNodes();
2737
- if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
2738
- console.log(`[Ryunix Debug] init: hasChildNodes=${hasChildNodes}, has SSR content detected.`);
2134
+ if (state.isServerRendering) {
2135
+ return;
2739
2136
  }
2740
- // Auto-detect: if there's existing content, try to hydrate (SSR)
2741
- // If explicitly disabled via RYUNIX_SSR=false, skip hydration
2742
- const ssrEnabled = process.env.RYUNIX_SSR !== 'false';
2743
- if (hasChildNodes && ssrEnabled) {
2744
- if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
2745
- console.log(`[Ryunix Debug] init: SSR content detected. Starting hydration on #${root}`);
2746
- }
2747
- const res = hydrate(MainElement, state.containerRoot);
2748
- return res;
2137
+ validateHookContext();
2138
+ if (!is.function(callback)) {
2139
+ throw new Error('useLayoutEffect callback must be a function');
2749
2140
  }
2750
- if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
2751
- console.log(`[Ryunix Debug] init: No SSR content or SSR disabled. Starting normal render on #${root}`);
2141
+ if (deps !== undefined && !Array.isArray(deps)) {
2142
+ throw new Error('useLayoutEffect dependencies must be an array or undefined');
2752
2143
  }
2753
- const res = render(MainElement, state.containerRoot);
2754
- return res;
2144
+ const { hookIndex } = state;
2145
+ const wipFiber = state.wipFiber;
2146
+ if (!wipFiber.hooks)
2147
+ wipFiber.hooks = [];
2148
+ const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
2149
+ const hasChanged = haveDepsChanged(oldHook?.deps, deps);
2150
+ const hook = {
2151
+ hookID: hookIndex,
2152
+ type: RYUNIX_TYPES.RYUNIX_EFFECT,
2153
+ deps,
2154
+ effect: hasChanged ? callback : null,
2155
+ cancel: oldHook?.cancel,
2156
+ isLayout: true,
2157
+ };
2158
+ wipFiber.hooks[hookIndex] = hook;
2159
+ state.hookIndex++;
2755
2160
  };
2756
- /**
2757
- * @param {RyunixComponent} component
2758
- * @param {Record<string, unknown>} props
2759
- * @param {(error: unknown) => void} [onError]
2760
- * @returns {RyunixNode}
2761
- */
2762
- const safeRender = (component, props, onError) => {
2763
- try {
2764
- return /** @type {RyunixNode} */ /** @type {(props: Record<string, unknown>) => RyunixNode} */ component(props);
2161
+ let idCounter = 0;
2162
+ const resetIdCounter = () => {
2163
+ idCounter = 0;
2164
+ };
2165
+ const useId = () => {
2166
+ const state = getState();
2167
+ if (state.isServerRendering) {
2168
+ return `:r${idCounter++}:`;
2765
2169
  }
2766
- catch (error) {
2767
- if (process.env.NODE_ENV !== 'production') {
2768
- console.error('Component error:', error);
2170
+ validateHookContext();
2171
+ const { hookIndex } = state;
2172
+ const wipFiber = state.wipFiber;
2173
+ if (!wipFiber.hooks)
2174
+ wipFiber.hooks = [];
2175
+ const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
2176
+ const hook = {
2177
+ hookID: hookIndex,
2178
+ type: RYUNIX_TYPES.RYUNIX_REF,
2179
+ value: oldHook ? oldHook.value : `:r${idCounter++}:`,
2180
+ };
2181
+ wipFiber.hooks[hookIndex] = hook;
2182
+ state.hookIndex++;
2183
+ return hook.value;
2184
+ };
2185
+ const useDebounce = (value, delay = 300) => {
2186
+ const [debouncedValue, setDebouncedValue] = useStore(value);
2187
+ useEffect(() => {
2188
+ const timer = setTimeout(() => {
2189
+ setDebouncedValue(value);
2190
+ }, delay);
2191
+ return () => clearTimeout(timer);
2192
+ }, [value, delay]);
2193
+ return debouncedValue;
2194
+ };
2195
+ const useThrottle = (value, interval = 300) => {
2196
+ const [throttledValue, setThrottledValue] = useStore(value);
2197
+ const lastUpdated = useRef(Date.now());
2198
+ useEffect(() => {
2199
+ const now = Date.now();
2200
+ const elapsed = now - lastUpdated.current;
2201
+ if (elapsed >= interval) {
2202
+ lastUpdated.current = now;
2203
+ setThrottledValue(value);
2769
2204
  }
2770
- if (onError)
2771
- onError(error);
2772
- return null;
2773
- }
2205
+ else {
2206
+ const timer = setTimeout(() => {
2207
+ lastUpdated.current = Date.now();
2208
+ setThrottledValue(value);
2209
+ }, interval - elapsed);
2210
+ return () => clearTimeout(timer);
2211
+ }
2212
+ }, [value, interval]);
2213
+ return throttledValue;
2774
2214
  };
2775
2215
 
2776
- /**
2777
- * @typedef {import('./createElement.js').RyunixNode} RyunixNode
2778
- * @typedef {import('./createElement.js').RyunixElement} RyunixElement
2779
- * @typedef {import('../types/internal.js').RyunixRenderToStringOptions} RyunixRenderToStringOptions
2780
- * @typedef {Promise<{ success: boolean, id: string, content: string, error?: unknown }>} RyunixSuspenseTask
2781
- */
2782
- /**
2783
- * @param {unknown} unsafe
2784
- * @returns {string}
2785
- */
2216
+ const ASYNC_RENDER_ERROR = 'Async components require renderToStringAsync or renderToReadableStream, not renderToString';
2786
2217
  const escapeHtml = (unsafe) => {
2787
2218
  if (typeof unsafe !== 'string')
2788
2219
  return String(unsafe);
@@ -2793,10 +2224,6 @@ const escapeHtml = (unsafe) => {
2793
2224
  .replace(/"/g, '&quot;')
2794
2225
  .replace(/'/g, '&#039;');
2795
2226
  };
2796
- /**
2797
- * @param {Record<string, unknown>} styleObj
2798
- * @returns {string}
2799
- */
2800
2227
  const renderStyle = (styleObj) => {
2801
2228
  if (!is.object(styleObj) || is.null(styleObj))
2802
2229
  return '';
@@ -2821,10 +2248,83 @@ const VOID_ELEMENTS = new Set([
2821
2248
  'track',
2822
2249
  'wbr',
2823
2250
  ]);
2824
- /**
2825
- * @param {RyunixNode | RyunixNode[]} element
2826
- * @returns {string}
2827
- */
2251
+ const normalizeChildren = (children) => {
2252
+ if (children == null)
2253
+ return [];
2254
+ return Array.isArray(children) ? children : [children];
2255
+ };
2256
+ const assertSyncRenderResult = (rendered) => {
2257
+ if (rendered != null &&
2258
+ typeof rendered === 'object' &&
2259
+ 'then' in rendered &&
2260
+ typeof rendered.then === 'function') {
2261
+ throw new Error(ASYNC_RENDER_ERROR);
2262
+ }
2263
+ return rendered;
2264
+ };
2265
+ const buildHostProps = (props, renderChild) => {
2266
+ let attributes = '';
2267
+ let htmlChildren = '';
2268
+ let innerHTML = null;
2269
+ Object.entries(props).forEach(([key, value]) => {
2270
+ if (key === 'children') {
2271
+ if (Array.isArray(value)) {
2272
+ htmlChildren = value
2273
+ .map((child) => renderChild(child))
2274
+ .join('');
2275
+ }
2276
+ else {
2277
+ htmlChildren = renderChild(value);
2278
+ }
2279
+ }
2280
+ else if (key === 'dangerouslySetInnerHTML') {
2281
+ const inner = value;
2282
+ if (inner?.__html) {
2283
+ innerHTML = inner.__html;
2284
+ }
2285
+ }
2286
+ else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
2287
+ const styleString = renderStyle(value);
2288
+ if (styleString) {
2289
+ attributes += ` style="${escapeHtml(styleString)}"`;
2290
+ }
2291
+ }
2292
+ else if (key === STRINGS.CLASS_NAME || key === OLD_STRINGS.CLASS_NAME) {
2293
+ if (value) {
2294
+ attributes += ` class="${escapeHtml(value)}"`;
2295
+ }
2296
+ }
2297
+ else if (!key.startsWith('on') &&
2298
+ key !== 'key' &&
2299
+ key !== 'ref' &&
2300
+ key !== '__source' &&
2301
+ key !== '__self') {
2302
+ if (typeof value === 'boolean') {
2303
+ if (value)
2304
+ attributes += ` ${key}=""`;
2305
+ }
2306
+ else if (value != null) {
2307
+ const attrName = toSvgAttrName(key);
2308
+ const validatedValue = validateUri(attrName, value);
2309
+ attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
2310
+ }
2311
+ }
2312
+ });
2313
+ return { attributes, innerHTML, htmlChildren };
2314
+ };
2315
+ const formatHostOpenTag = (hostTag, attributes) => {
2316
+ if (VOID_ELEMENTS.has(hostTag)) {
2317
+ return `<${hostTag}${attributes} />`;
2318
+ }
2319
+ return `<${hostTag}${attributes}>`;
2320
+ };
2321
+ const beginSsrRender = () => {
2322
+ const state = getState();
2323
+ state.isServerRendering = true;
2324
+ state.ssrMetadata = {};
2325
+ state.ssrContexts = {};
2326
+ resetIdCounter();
2327
+ };
2828
2328
  const renderToStringImpl = (element) => {
2829
2329
  if (element == null || typeof element === 'boolean') {
2830
2330
  return '';
@@ -2835,101 +2335,50 @@ const renderToStringImpl = (element) => {
2835
2335
  if (Array.isArray(element)) {
2836
2336
  return element.map((child) => renderToStringImpl(child)).join('');
2837
2337
  }
2838
- /** @type {RyunixElement} */
2839
2338
  const vnode = element;
2840
2339
  if (vnode.type === RYUNIX_TYPES.TEXT_ELEMENT) {
2841
- return escapeHtml(
2842
- /** @type {import('./createElement.js').RyunixTextElement} */ vnode.props
2843
- .nodeValue);
2340
+ return escapeHtml(vnode.props.nodeValue);
2844
2341
  }
2845
2342
  if (vnode.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
2846
- const children = vnode.props?.children || [];
2343
+ const children = normalizeChildren(vnode.props?.children);
2847
2344
  return children.map((child) => renderToStringImpl(child)).join('');
2848
2345
  }
2849
2346
  if (vnode.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2850
- // Context Providers just render their children transparently on the server
2851
2347
  const state = getState();
2852
2348
  state.ssrContexts = state.ssrContexts || {};
2853
- const ctxProps =
2854
- /** @type {{ _contextId?: string, value?: unknown, children?: RyunixNode | RyunixNode[] }} */ vnode.props ||
2855
- {};
2349
+ const ctxProps = (vnode.props || {});
2856
2350
  const ctxId = ctxProps._contextId;
2857
- const prevCtx = state.ssrContexts[ctxId];
2351
+ const prevCtx = ctxId ? state.ssrContexts[ctxId] : undefined;
2858
2352
  if (ctxId) {
2859
2353
  state.ssrContexts[ctxId] = ctxProps.value;
2860
2354
  }
2861
- const children = ctxProps.children || [];
2862
- let result = '';
2863
- if (Array.isArray(children)) {
2864
- result = children.map((child) => renderToStringImpl(child)).join('');
2865
- }
2866
- else {
2867
- result = renderToStringImpl(children);
2868
- }
2869
- if (ctxId) {
2870
- state.ssrContexts[ctxId] = prevCtx;
2871
- }
2872
- return result;
2873
- }
2874
- if (typeof vnode.type === 'function') {
2875
- const type = vnode.type;
2876
- const props = vnode.props || {};
2877
- const renderedElement =
2878
- /** @type {(props: Record<string, unknown>) => RyunixNode} */ type(props);
2879
- return renderToStringImpl(renderedElement);
2880
- }
2881
- // It's a standard host element
2882
- const type = String(vnode.type);
2883
- const props = vnode.props || {};
2884
- let attributes = '';
2885
- let htmlChildren = '';
2886
- let innerHTML = null;
2887
- Object.entries(props).forEach(([key, value]) => {
2888
- if (key === 'children') {
2889
- if (Array.isArray(value)) {
2890
- htmlChildren = value.map((child) => renderToStringImpl(child)).join('');
2891
- }
2892
- else {
2893
- htmlChildren = renderToStringImpl(/** @type {RyunixNode} */ value);
2894
- }
2895
- }
2896
- else if (key === 'dangerouslySetInnerHTML') {
2897
- const inner = value;
2898
- if (inner?.__html) {
2899
- innerHTML = inner.__html;
2900
- }
2901
- }
2902
- else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
2903
- const styleString = renderStyle(
2904
- /** @type {Record<string, unknown>} */ value);
2905
- if (styleString) {
2906
- attributes += ` style="${escapeHtml(styleString)}"`;
2907
- }
2355
+ const children = ctxProps.children || [];
2356
+ let result = '';
2357
+ if (Array.isArray(children)) {
2358
+ result = children.map((child) => renderToStringImpl(child)).join('');
2908
2359
  }
2909
- else if (key === STRINGS.CLASS_NAME || key === OLD_STRINGS.CLASS_NAME) {
2910
- if (value) {
2911
- attributes += ` class="${escapeHtml(value)}"`;
2912
- }
2360
+ else {
2361
+ result = renderToStringImpl(children);
2913
2362
  }
2914
- else if (!key.startsWith('on') &&
2915
- key !== '__source' &&
2916
- key !== '__self') {
2917
- if (typeof value === 'boolean') {
2918
- if (value)
2919
- attributes += ` ${key}=""`;
2920
- }
2921
- else if (value != null) {
2922
- let attrName = toSvgAttrName(key);
2923
- let validatedValue = validateUri(attrName, value);
2924
- attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
2925
- }
2363
+ if (ctxId) {
2364
+ state.ssrContexts[ctxId] = prevCtx;
2926
2365
  }
2927
- });
2366
+ return result;
2367
+ }
2368
+ if (typeof vnode.type === 'function') {
2369
+ const type = vnode.type;
2370
+ const props = vnode.props || {};
2371
+ const renderedElement = assertSyncRenderResult(type(props));
2372
+ return renderToStringImpl(renderedElement);
2373
+ }
2374
+ const type = String(vnode.type);
2375
+ const props = vnode.props || {};
2376
+ const { attributes, innerHTML, htmlChildren } = buildHostProps(props, renderToStringImpl);
2928
2377
  if (VOID_ELEMENTS.has(type)) {
2929
- return `<${type}${attributes} />`;
2378
+ return formatHostOpenTag(type, attributes);
2930
2379
  }
2931
2380
  const finalContent = innerHTML !== null ? innerHTML : htmlChildren;
2932
- return `<${type}${attributes}>${finalContent}</${type}>`;
2381
+ return `${formatHostOpenTag(type, attributes)}${finalContent}</${type}>`;
2933
2382
  };
2934
2383
  const RC_SCRIPT = `
2935
2384
  function $RC(id, templateId) {
@@ -2943,21 +2392,12 @@ function $RC(id, templateId) {
2943
2392
  `
2944
2393
  .replace(/\s+/g, ' ')
2945
2394
  .trim();
2946
- /**
2947
- * @param {RyunixNode | RyunixNode[]} element
2948
- * @param {(chunk: string) => void} push
2949
- * @param {RyunixSuspenseTask[]} [suspenseTasks]
2950
- * @returns {Promise<void>}
2951
- */
2952
2395
  const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2953
- if (element == null || typeof element === 'boolean') {
2954
- return;
2955
- }
2956
- // Await the element if it's a promise (e.g. from an async Server Component directly rendered)
2957
2396
  if (element instanceof Promise) {
2958
2397
  element = await element;
2959
- if (element == null || typeof element === 'boolean')
2960
- return;
2398
+ }
2399
+ if (element == null || typeof element === 'boolean') {
2400
+ return;
2961
2401
  }
2962
2402
  if (typeof element === 'string' || typeof element === 'number') {
2963
2403
  push(escapeHtml(element));
@@ -2969,16 +2409,13 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2969
2409
  }
2970
2410
  return;
2971
2411
  }
2972
- /** @type {RyunixElement} */
2973
2412
  const vnode = element;
2974
2413
  if (vnode.type === RYUNIX_TYPES.TEXT_ELEMENT) {
2975
- push(escapeHtml(
2976
- /** @type {import('./createElement.js').RyunixTextElement} */ vnode
2977
- .props.nodeValue));
2414
+ push(escapeHtml(vnode.props.nodeValue));
2978
2415
  return;
2979
2416
  }
2980
2417
  if (vnode.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
2981
- const children = vnode.props?.children || [];
2418
+ const children = normalizeChildren(vnode.props?.children);
2982
2419
  for (const child of children) {
2983
2420
  await renderToStreamImpl(child, push, suspenseTasks);
2984
2421
  }
@@ -2987,11 +2424,9 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2987
2424
  if (vnode.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2988
2425
  const state = getState();
2989
2426
  state.ssrContexts = state.ssrContexts || {};
2990
- const ctxProps =
2991
- /** @type {{ _contextId?: string, value?: unknown, children?: RyunixNode | RyunixNode[] }} */ vnode.props ||
2992
- {};
2427
+ const ctxProps = (vnode.props || {});
2993
2428
  const ctxId = ctxProps._contextId;
2994
- const prevCtx = state.ssrContexts[ctxId];
2429
+ const prevCtx = ctxId ? state.ssrContexts[ctxId] : undefined;
2995
2430
  if (ctxId) {
2996
2431
  state.ssrContexts[ctxId] = ctxProps.value;
2997
2432
  }
@@ -3009,29 +2444,21 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
3009
2444
  }
3010
2445
  return;
3011
2446
  }
3012
- // Handle Suspense specifically
3013
2447
  const suspenseType = vnode.type;
3014
2448
  const isSuspenseBoundary = vnode.type === RYUNIX_TYPES.RYUNIX_SUSPENSE ||
3015
2449
  (typeof suspenseType === 'object' &&
3016
2450
  suspenseType != null &&
3017
- /** @type {{ type?: symbol }} */ suspenseType.type ===
3018
- RYUNIX_TYPES.RYUNIX_SUSPENSE);
2451
+ suspenseType.type === RYUNIX_TYPES.RYUNIX_SUSPENSE);
3019
2452
  if (isSuspenseBoundary) {
3020
- const suspenseProps =
3021
- /** @type {{ fallback?: RyunixNode, children?: RyunixNode | RyunixNode[] }} */ vnode.props ||
3022
- {};
2453
+ const suspenseProps = (vnode.props || {});
3023
2454
  const { fallback, children } = suspenseProps;
3024
2455
  const id = `s-${Math.random().toString(36).slice(2, 9)}`;
3025
- // In universal mode, Suspense renders children if ready, or fallback if pending.
3026
- // BUT we want to force a background task for the REAL children if we hit a lazy component.
3027
2456
  push(`<!--$?--><template id="B:${id}"></template><div id="S:${id}">`);
3028
- // 1. Start rendering the actual content in the background
3029
2457
  const task = (async () => {
3030
2458
  const state = getState();
3031
2459
  const wasBackground = state.isSuspenseBackground;
3032
2460
  state.isSuspenseBackground = true;
3033
2461
  let content = '';
3034
- /** @param {string} chunk */
3035
2462
  const subPush = (chunk) => {
3036
2463
  content += chunk;
3037
2464
  };
@@ -3045,67 +2472,30 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
3045
2472
  finally {
3046
2473
  state.isSuspenseBackground = wasBackground;
3047
2474
  }
3048
- })();
2475
+ });
3049
2476
  suspenseTasks.push(task);
3050
- // 2. Render fallback immediately for the main stream
3051
2477
  await renderToStreamImpl(fallback, push, suspenseTasks);
3052
2478
  push(`</div><!--$/-->`);
3053
2479
  return;
3054
2480
  }
3055
- let type = vnode.type;
3056
- let props = vnode.props || {};
2481
+ const type = vnode.type;
2482
+ const props = vnode.props || {};
3057
2483
  if (typeof type === 'function') {
3058
2484
  if (process.env.RYUNIX_DEBUG) {
3059
2485
  console.log('[SSR Debug] Rendering function:', type.name || 'anonymous');
3060
2486
  }
3061
- const renderedElement = await /** @type {(props: Record<string, unknown>) => RyunixNode | Promise<RyunixNode>} */ type(props);
2487
+ const renderedElement = await type(props);
3062
2488
  await renderToStreamImpl(renderedElement, push, suspenseTasks);
3063
2489
  return;
3064
2490
  }
3065
- // It's a standard host element
3066
2491
  const hostTag = String(type);
3067
- let attributes = '';
3068
- let innerHTML = null;
3069
- let children = props.children || [];
3070
- Object.entries(props).forEach(([key, value]) => {
3071
- if (key === 'children') ;
3072
- else if (key === 'dangerouslySetInnerHTML') {
3073
- const inner = value;
3074
- if (inner?.__html) {
3075
- innerHTML = inner.__html;
3076
- }
3077
- }
3078
- else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
3079
- const styleString = renderStyle(
3080
- /** @type {Record<string, unknown>} */ value);
3081
- if (styleString) {
3082
- attributes += ` style="${escapeHtml(styleString)}"`;
3083
- }
3084
- }
3085
- else if (key === STRINGS.CLASS_NAME || key === OLD_STRINGS.CLASS_NAME) {
3086
- if (value) {
3087
- attributes += ` class="${escapeHtml(value)}"`;
3088
- }
3089
- }
3090
- else if (!key.startsWith('on') &&
3091
- key !== '__source' &&
3092
- key !== '__self') {
3093
- if (typeof value === 'boolean') {
3094
- if (value)
3095
- attributes += ` ${key}=""`;
3096
- }
3097
- else if (value != null) {
3098
- const attrName = toSvgAttrName(key);
3099
- const validatedValue = validateUri(attrName, value);
3100
- attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
3101
- }
3102
- }
3103
- });
3104
- push(`<${hostTag}${attributes}>`);
2492
+ const children = props.children || [];
2493
+ const { attributes, innerHTML } = buildHostProps(props, () => '');
2494
+ push(formatHostOpenTag(hostTag, attributes));
3105
2495
  if (innerHTML !== null) {
3106
2496
  push(innerHTML);
3107
2497
  }
3108
- else {
2498
+ else if (!VOID_ELEMENTS.has(hostTag)) {
3109
2499
  if (Array.isArray(children)) {
3110
2500
  for (const child of children) {
3111
2501
  await renderToStreamImpl(child, push, suspenseTasks);
@@ -3114,46 +2504,42 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
3114
2504
  else {
3115
2505
  await renderToStreamImpl(children, push, suspenseTasks);
3116
2506
  }
3117
- }
3118
- if (!VOID_ELEMENTS.has(hostTag)) {
3119
2507
  push(`</${hostTag}>`);
3120
2508
  }
3121
2509
  };
3122
- /**
3123
- * @param {RyunixNode} element
3124
- * @param {RyunixRenderToStringOptions} [options]
3125
- * @returns {ReadableStream<Uint8Array>}
3126
- */
2510
+ const handleSuspenseTaskResult = (res, push, nonceAttr) => {
2511
+ if (res.success) {
2512
+ push(`<template id="P:${res.id}" data-ryunix-ssr>${res.content}</template>`);
2513
+ push(`<script${nonceAttr} data-ryunix-ssr>$RC("S:${res.id}", "P:${res.id}")</script>`);
2514
+ return;
2515
+ }
2516
+ const message = res.error instanceof Error
2517
+ ? res.error.message
2518
+ : String(res.error ?? 'Unknown error');
2519
+ if (process.env.NODE_ENV !== 'production') {
2520
+ console.error('[Ryunix SSR] Suspense boundary failed:', res.error);
2521
+ }
2522
+ push(`<!-- Ryunix Suspense error: ${escapeHtml(message)} -->`);
2523
+ };
3127
2524
  const renderToReadableStream = (element, options = {}) => {
3128
2525
  const state = getState();
3129
2526
  const encoder = new TextEncoder();
3130
- // Reset idCounter for deterministic useId values
3131
- resetIdCounter();
3132
2527
  return new ReadableStream({
3133
2528
  async start(controller) {
3134
2529
  const wasServerRendering = state.isServerRendering;
3135
- state.isServerRendering = true;
3136
- state.ssrMetadata = {};
3137
- /** @param {string} text */
2530
+ beginSsrRender();
3138
2531
  const push = (text) => controller.enqueue(encoder.encode(text));
3139
- /** @type {RyunixSuspenseTask[]} */
3140
2532
  const suspenseTasks = [];
3141
2533
  try {
3142
- // 0. Inject RC helper script first
3143
2534
  const nonceAttr = options.nonce ? ` nonce="${options.nonce}"` : '';
3144
2535
  push(`<script${nonceAttr} data-ryunix-ssr>${RC_SCRIPT}</script>`);
3145
- // 1. Render initial tree (with fallbacks)
3146
2536
  await renderToStreamImpl(element, push, suspenseTasks);
3147
- // 2. Process suspense tasks as they complete
3148
- // For now, we wait for all, but in a real streaming scenario,
3149
- // we could push them as they resolve.
3150
2537
  while (suspenseTasks.length > 0) {
3151
2538
  const task = suspenseTasks.shift();
3152
- const res = await task;
3153
- if (res.success) {
3154
- push(`<template id="P:${res.id}" data-ryunix-ssr>${res.content}</template>`);
3155
- push(`<script${nonceAttr} data-ryunix-ssr>$RC("S:${res.id}", "P:${res.id}")</script>`);
3156
- }
2539
+ if (!task)
2540
+ continue;
2541
+ const res = await task();
2542
+ handleSuspenseTaskResult(res, push, nonceAttr);
3157
2543
  }
3158
2544
  controller.close();
3159
2545
  }
@@ -3166,18 +2552,10 @@ const renderToReadableStream = (element, options = {}) => {
3166
2552
  },
3167
2553
  });
3168
2554
  };
3169
- /**
3170
- * @param {RyunixNode} element
3171
- * @param {RyunixRenderToStringOptions} [options]
3172
- * @returns {string}
3173
- */
3174
- const renderToString = (element, options = {}) => {
2555
+ const renderToString = (element, _options = {}) => {
3175
2556
  const state = getState();
3176
2557
  const wasServerRendering = state.isServerRendering;
3177
- state.isServerRendering = true;
3178
- state.ssrMetadata = {};
3179
- // Reset idCounter for deterministic useId values
3180
- resetIdCounter();
2558
+ beginSsrRender();
3181
2559
  try {
3182
2560
  return renderToStringImpl(element);
3183
2561
  }
@@ -3185,11 +2563,6 @@ const renderToString = (element, options = {}) => {
3185
2563
  state.isServerRendering = wasServerRendering;
3186
2564
  }
3187
2565
  };
3188
- /**
3189
- * @param {RyunixNode} element
3190
- * @param {RyunixRenderToStringOptions} [options]
3191
- * @returns {Promise<string>}
3192
- */
3193
2566
  const renderToStringAsync = async (element, options = {}) => {
3194
2567
  const stream = renderToReadableStream(element, options);
3195
2568
  const reader = stream.getReader();
@@ -3236,6 +2609,21 @@ function deepEqual(a, b) {
3236
2609
  return keysA.every((key) => deepEqual(a[key], b[key]));
3237
2610
  }
3238
2611
 
2612
+ function forwardRef(render) {
2613
+ if (typeof render !== 'function') {
2614
+ throw new Error('forwardRef requires a render function');
2615
+ }
2616
+ const ForwardRefComponent = ((props) => {
2617
+ const { ref, ...restProps } = props || {};
2618
+ return render(restProps, ref ?? null);
2619
+ });
2620
+ const named = render;
2621
+ ForwardRefComponent.displayName = `ForwardRef(${named.displayName || named.name || 'Component'})`;
2622
+ ForwardRefComponent._isForwardRef = true;
2623
+ ForwardRefComponent._render = render;
2624
+ return ForwardRefComponent;
2625
+ }
2626
+
3239
2627
  const SUSPENSE_STATUS = {
3240
2628
  PENDING: 'pending',
3241
2629
  RESOLVED: 'resolved',
@@ -3321,13 +2709,57 @@ const Suspense = ({ fallback, children, }) => {
3321
2709
  if (anyPending && !getState().isSuspenseBackground) {
3322
2710
  return fallback || null;
3323
2711
  }
3324
- return createElement(Fragment, { children });
2712
+ return createElement(Fragment, {
2713
+ children: children,
2714
+ });
3325
2715
  };
3326
2716
  Suspense.type = RYUNIX_TYPES.RYUNIX_SUSPENSE;
3327
2717
  function preload(importFn) {
3328
2718
  return importFn();
3329
2719
  }
3330
2720
 
2721
+ var index = /*#__PURE__*/Object.freeze({
2722
+ __proto__: null,
2723
+ Children: Children,
2724
+ INTERNAL_META_KEYS: INTERNAL_META_KEYS,
2725
+ Link: Link,
2726
+ NavLink: NavLink,
2727
+ RouterProvider: RouterProvider,
2728
+ Suspense: Suspense,
2729
+ createContext: createContext,
2730
+ deepEqual: deepEqual,
2731
+ forwardRef: forwardRef,
2732
+ lazy: lazy,
2733
+ memo: memo,
2734
+ mergeRouteMetadata: mergeRouteMetadata,
2735
+ preload: preload,
2736
+ resetIdCounter: resetIdCounter,
2737
+ resolvePageMetadata: resolvePageMetadata,
2738
+ shallowEqual: shallowEqual,
2739
+ useCallback: useCallback,
2740
+ useDebounce: useDebounce,
2741
+ useDeferredValue: useDeferredValue,
2742
+ useEffect: useEffect,
2743
+ useHash: useHash,
2744
+ useId: useId,
2745
+ useLayoutEffect: useLayoutEffect,
2746
+ useMemo: useMemo,
2747
+ useMetadata: useMetadata,
2748
+ usePathname: usePathname,
2749
+ usePersistentStore: usePersistentStore,
2750
+ usePersitentStore: usePersistentStore,
2751
+ useQuery: useQuery,
2752
+ useReducer: useReducer,
2753
+ useRef: useRef,
2754
+ useRouter: useRouter,
2755
+ useSearchParams: useSearchParams,
2756
+ useStore: useStore,
2757
+ useStorePriority: useStorePriority,
2758
+ useSwitch: useSwitch,
2759
+ useThrottle: useThrottle,
2760
+ useTransition: useTransition
2761
+ });
2762
+
3331
2763
  const perfNow = () => typeof performance !== 'undefined' ? performance.now() : Date.now();
3332
2764
  class Profiler {
3333
2765
  enabled;
@@ -3449,44 +2881,11 @@ function withProfiler(Component, name) {
3449
2881
  return Profiled;
3450
2882
  }
3451
2883
 
3452
- function forwardRef(render) {
3453
- if (typeof render !== 'function') {
3454
- throw new Error('forwardRef requires a render function');
3455
- }
3456
- const ForwardRefComponent = ((props) => {
3457
- const { ref, ...restProps } = props || {};
3458
- return render(restProps, ref ?? null);
3459
- });
3460
- const named = render;
3461
- ForwardRefComponent.displayName = `ForwardRef(${named.displayName || named.name || 'Component'})`;
3462
- ForwardRefComponent._isForwardRef = true;
3463
- ForwardRefComponent._render = render;
3464
- return ForwardRefComponent;
3465
- }
3466
-
3467
- /**
3468
- * Wraps content rendered exclusively on the server. During hydration Ryunix
3469
- * preserves the server HTML inside this boundary.
3470
- *
3471
- * @param {object} props
3472
- * @param {import('./createElement.js').RyunixNode} [props.children]
3473
- * @param {string} [props.id]
3474
- * @returns {import('./createElement.js').RyunixElement}
3475
- */
3476
- function ServerBoundary({ children, id }) {
2884
+ function ServerBoundary({ children, id, }) {
3477
2885
  return createElement('div', { 'data-ryunix-server': id, style: { display: 'contents' } }, children);
3478
2886
  }
3479
2887
  ServerBoundary.ryunix_type = 'RYUNIX_SERVER_BOUNDARY';
3480
- /**
3481
- * Marks a DOM subtree for scoped hydration recovery. Mismatches inside this
3482
- * boundary can be recovered locally without remounting the full app root.
3483
- *
3484
- * @param {object} props
3485
- * @param {import('./createElement.js').RyunixNode} [props.children]
3486
- * @param {string} [props.id]
3487
- * @returns {import('./createElement.js').RyunixElement}
3488
- */
3489
- function HydrationBoundary({ children, id }) {
2888
+ function HydrationBoundary({ children, id, }) {
3490
2889
  return createElement('div', {
3491
2890
  'data-ryunix-hydrate-boundary': id ?? '',
3492
2891
  suppressHydrationWarning: true,
@@ -3510,11 +2909,6 @@ function ErrorBoundary({ children, fallback, }) {
3510
2909
  ErrorBoundary.ryunix_type =
3511
2910
  'RYUNIX_ERROR_BOUNDARY';
3512
2911
 
3513
- /**
3514
- * Client proxy for a compiled server action.
3515
- * @param {string} actionId - Build-time action identifier
3516
- * @returns {(...args: unknown[]) => Promise<unknown>}
3517
- */
3518
2912
  function createActionProxy(actionId) {
3519
2913
  return async function (...args) {
3520
2914
  const response = await fetch('/_ryunix/action', {
@@ -3526,87 +2920,71 @@ function createActionProxy(actionId) {
3526
2920
  body: JSON.stringify({ actionId, args }),
3527
2921
  });
3528
2922
  if (!response.ok) {
3529
- const errorData = await response.json().catch(() => ({}));
2923
+ const errorData = (await response.json().catch(() => ({})));
3530
2924
  throw new Error(errorData.error || 'Server Action failed');
3531
2925
  }
3532
2926
  return response.json();
3533
2927
  };
3534
2928
  }
3535
2929
 
3536
- /**
3537
- * @typedef {object} OverlayError
3538
- * @property {string} [name]
3539
- * @property {string} [message]
3540
- * @property {string | string[]} [stack]
3541
- * @property {{ fileName?: string, lineNumber?: number }} [__ryunix_source]
3542
- */
3543
- /**
3544
- * @param {unknown} propsOrError
3545
- */
3546
2930
  function RyunixDevOverlay(propsOrError) {
3547
- /** @type {Record<string, unknown> | Error | null | undefined} */
3548
- const propsInput =
3549
- /** @type {Record<string, unknown> | Error | null | undefined} */ propsOrError;
3550
- // If propsOrError is an event or wrapped object, try to extract error
2931
+ const propsInput = propsOrError;
3551
2932
  const rawError = propsInput &&
3552
2933
  typeof propsInput === 'object' &&
3553
2934
  !(propsInput instanceof Error) &&
3554
2935
  propsInput.nativeEvent
3555
2936
  ? propsInput.error
3556
2937
  : propsInput;
3557
- /** @type {OverlayError | null} */
3558
2938
  let error = null;
3559
2939
  if (rawError instanceof Error) {
3560
2940
  error = rawError;
3561
2941
  }
3562
2942
  else if (rawError && typeof rawError === 'object') {
3563
2943
  if ('message' in rawError) {
3564
- error = /** @type {OverlayError} */ rawError;
2944
+ error = rawError;
3565
2945
  }
3566
2946
  else if ('error' in rawError) {
3567
- const nested = /** @type {Record<string, unknown>} */ rawError.error;
2947
+ const nested = rawError.error;
3568
2948
  error =
3569
- nested && typeof nested === 'object'
3570
- ? /** @type {OverlayError} */ nested
3571
- : null;
2949
+ nested && typeof nested === 'object' ? nested : null;
3572
2950
  }
3573
2951
  else {
3574
- error = /** @type {OverlayError} */ rawError;
2952
+ error = rawError;
3575
2953
  }
3576
2954
  }
3577
- // Debug string if error is broken
3578
2955
  const debugObjectStr = JSON.stringify(propsInput, Object.getOwnPropertyNames(propsInput || {}));
3579
2956
  const [snippetState, setSnippet] = useStore(null);
3580
2957
  const [startLineState, setStartLine] = useStore(1);
3581
2958
  const [errorFileState, setErrorFile] = useStore('');
3582
2959
  const [errorLineState, setErrorLine] = useStore(0);
3583
- const snippet = /** @type {string | null} */ snippetState;
3584
- const startLine = /** @type {number} */ startLineState;
3585
- const errorFile = /** @type {string} */ errorFileState;
3586
- const errorLine = /** @type {number} */ errorLineState;
3587
- // Normalize stack ensuring we have lines
3588
- /** @type {string[]} */
2960
+ const snippet = snippetState;
2961
+ const startLine = startLineState;
2962
+ const errorFile = errorFileState;
2963
+ const errorLine = errorLineState;
3589
2964
  let stackLines = [];
3590
2965
  if (error && error.stack) {
3591
2966
  stackLines =
3592
2967
  typeof error.stack === 'string'
3593
- ? error.stack.split('\n').filter((/** @type {string} */ line) => {
2968
+ ? error.stack.split('\n').filter((line) => {
3594
2969
  const trimmed = line.trim();
3595
2970
  if (!trimmed)
3596
2971
  return false;
3597
2972
  if (trimmed.includes('node_modules'))
3598
2973
  return false;
3599
- // Filter out internal Ryunix core framework files to isolate user code
3600
2974
  const isInternal = [
3601
- 'components.js',
2975
+ 'fiber-update.js',
3602
2976
  'workers.js',
3603
2977
  'reconciler.js',
3604
2978
  'commits.js',
2979
+ 'hooks/hooks.js',
3605
2980
  'hooks.js',
2981
+ 'error-boundary.js',
3606
2982
  'errorBoundary.js',
2983
+ 'boundaries.js',
3607
2984
  'serverBoundary.js',
3608
2985
  'app-router.js',
3609
2986
  'app-router-server.js',
2987
+ 'render/render.js',
3610
2988
  'render.js',
3611
2989
  'createElement.js',
3612
2990
  'index.js',
@@ -3614,7 +2992,7 @@ function RyunixDevOverlay(propsOrError) {
3614
2992
  return !isInternal;
3615
2993
  })
3616
2994
  : Array.isArray(error.stack)
3617
- ? /** @type {string[]} */ error.stack
2995
+ ? error.stack
3618
2996
  : [];
3619
2997
  }
3620
2998
  const errorName = error && typeof error.name === 'string' ? error.name : 'Unknown Error Type';
@@ -3624,22 +3002,18 @@ function RyunixDevOverlay(propsOrError) {
3624
3002
  useEffect(() => {
3625
3003
  let targetPath = null;
3626
3004
  let targetLine = null;
3627
- // 1. Direct JSX __source mapping (injected by Webpack/SWC)
3628
3005
  const ryunixSource = error?.__ryunix_source;
3629
3006
  if (ryunixSource?.fileName) {
3630
3007
  targetPath = ryunixSource.fileName;
3631
3008
  targetLine = ryunixSource.lineNumber ?? null;
3632
3009
  }
3633
- // 2. Fallback to Regex Stack Parsing
3634
3010
  if (!targetPath || !targetLine) {
3635
3011
  for (let i = 0; i < stackLines.length; i++) {
3636
3012
  const line = stackLines[i];
3637
3013
  if (!line.includes(':'))
3638
3014
  continue;
3639
- // Deterministic string-based parsing (no regex on uncontrolled data)
3640
3015
  let matchedPath = null;
3641
3016
  let matchedLine = null;
3642
- // V8 format: "at fn (file:line:col)" — extract content between parens
3643
3017
  const parenOpen = line.indexOf('(');
3644
3018
  const parenClose = line.lastIndexOf(')');
3645
3019
  if (parenOpen !== -1 && parenClose > parenOpen) {
@@ -3655,7 +3029,6 @@ function RyunixDevOverlay(propsOrError) {
3655
3029
  }
3656
3030
  }
3657
3031
  }
3658
- // V8 format without parens: "at file:line:col"
3659
3032
  if (!matchedPath) {
3660
3033
  const trimmed = line.trim();
3661
3034
  if (trimmed.startsWith('at ')) {
@@ -3672,7 +3045,6 @@ function RyunixDevOverlay(propsOrError) {
3672
3045
  }
3673
3046
  }
3674
3047
  }
3675
- // Ryunix format: "file.ryx:line" or "file.jsx:line"
3676
3048
  if (!matchedPath) {
3677
3049
  const exts = ['.ryx', '.jsx', '.js', '.ts', '.tsx'];
3678
3050
  const c1 = line.lastIndexOf(':');
@@ -3780,7 +3152,6 @@ function RyunixDevOverlay(propsOrError) {
3780
3152
  maxHeight: '150px',
3781
3153
  height: 'auto',
3782
3154
  };
3783
- /** @param {boolean} isErrorLine */
3784
3155
  const lineStyle = (isErrorLine) => ({
3785
3156
  display: 'flex',
3786
3157
  backgroundColor: isErrorLine ? 'rgba(239, 68, 68, 0.15)' : 'transparent',
@@ -3838,9 +3209,7 @@ function RyunixDevOverlay(propsOrError) {
3838
3209
  }, createElement('path', {
3839
3210
  d: 'M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z',
3840
3211
  }), createElement('polyline', { points: '13 2 13 9 20 9' })), errorFile, ':', errorLine), snippet &&
3841
- createElement('div', { style: snippetContainerStyle }, createElement('div', { style: { display: 'flex', flexDirection: 'column' } }, ...snippetLines.map((
3842
- /** @type {string} */ lineText,
3843
- /** @type {number} */ index) => {
3212
+ createElement('div', { style: snippetContainerStyle }, createElement('div', { style: { display: 'flex', flexDirection: 'column' } }, ...snippetLines.map((lineText, index) => {
3844
3213
  const currentLineNumber = startLine + index;
3845
3214
  const isErrorLine = currentLineNumber === errorLine;
3846
3215
  return createElement('div', { key: index, style: lineStyle(isErrorLine) }, createElement('span', {
@@ -3877,16 +3246,14 @@ function RyunixDevOverlay(propsOrError) {
3877
3246
  flexDirection: 'column',
3878
3247
  gap: '12px',
3879
3248
  },
3880
- }, ...stackLines.map((/** @type {string} */ line, /** @type {number} */ i) => {
3249
+ }, ...stackLines.map((line, i) => {
3881
3250
  if (i === 0 &&
3882
3251
  (line.startsWith('Error:') ||
3883
3252
  line.startsWith('TypeError:')))
3884
3253
  return null;
3885
- // Deterministic string-based stack frame parsing (no polynomial regex)
3886
3254
  const trimmed = line.trim();
3887
3255
  let fnName = '<anonymous>';
3888
3256
  let filePath = line;
3889
- // V8 format: "at fnName (file:line:col)" or "at file:line:col"
3890
3257
  if (trimmed.startsWith('at ')) {
3891
3258
  const rest = trimmed.slice(3);
3892
3259
  const parenOpen = rest.indexOf('(');
@@ -3897,7 +3264,6 @@ function RyunixDevOverlay(propsOrError) {
3897
3264
  filePath = rest.slice(parenOpen + 1, parenClose);
3898
3265
  }
3899
3266
  else {
3900
- // "at file:line:col" — no function name
3901
3267
  if (rest.includes(':')) {
3902
3268
  fnName = '<anonymous>';
3903
3269
  }
@@ -3907,22 +3273,18 @@ function RyunixDevOverlay(propsOrError) {
3907
3273
  filePath = rest;
3908
3274
  }
3909
3275
  }
3910
- // Firefox format: "fnName@file:line:col"
3911
3276
  else if (trimmed.includes('@')) {
3912
3277
  const atIdx = trimmed.indexOf('@');
3913
3278
  fnName = trimmed.slice(0, atIdx) || '<anonymous>';
3914
3279
  filePath = trimmed.slice(atIdx + 1);
3915
3280
  }
3916
- // Ryunix format: "fnName file.ext:line"
3917
3281
  else {
3918
3282
  const exts = ['.ryx', '.jsx', '.js', '.ts', '.tsx'];
3919
3283
  const parts = trimmed.split(/\s+/);
3920
3284
  if (parts.length >= 2) {
3921
3285
  const lastPart = parts[parts.length - 1];
3922
3286
  const colonIdx = lastPart.indexOf(':');
3923
- const fileCandidate = colonIdx > 0
3924
- ? lastPart.slice(0, colonIdx)
3925
- : lastPart;
3287
+ const fileCandidate = colonIdx > 0 ? lastPart.slice(0, colonIdx) : lastPart;
3926
3288
  if (exts.some((ext) => fileCandidate.endsWith(ext))) {
3927
3289
  fnName = parts.slice(0, -1).join(' ');
3928
3290
  filePath = lastPart;
@@ -3960,47 +3322,545 @@ function RyunixDevOverlay(propsOrError) {
3960
3322
  : createElement('div', { style: { color: '#6b7280', fontStyle: 'italic' } }, 'No stack trace available.'))))));
3961
3323
  }
3962
3324
 
3963
- /**
3964
- * Public API surface of `@unsetsoft/ryunixjs`.
3965
- * Re-exports createElement, hooks, render, SSR helpers, and related utilities.
3966
- *
3967
- * @module lib/index
3968
- */
3325
+ const DEFAULT_THEME_COOKIE_NAME = 'ryunix_theme';
3326
+ const THEME_PREFERENCES = ['light', 'system', 'dark'];
3327
+ function createThemeController(options = {}) {
3328
+ const cookieName = options.cookieName ?? DEFAULT_THEME_COOKIE_NAME;
3329
+ const defaultTheme = options.defaultTheme ?? 'dark';
3330
+ const darkClass = options.darkClass ?? 'dark';
3331
+ const maxAgeSeconds = options.maxAgeSeconds ?? 365 * 24 * 60 * 60;
3332
+ const isThemePreference = (theme) => THEME_PREFERENCES.includes(theme);
3333
+ const getThemeCookie = () => {
3334
+ if (typeof document === 'undefined')
3335
+ return null;
3336
+ const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${cookieName}=(light|system|dark)(?:;|$)`));
3337
+ return match ? match[1] : null;
3338
+ };
3339
+ const setThemeCookie = (theme) => {
3340
+ if (typeof document === 'undefined' || !isThemePreference(theme))
3341
+ return;
3342
+ document.cookie = `${cookieName}=${theme}; path=/; max-age=${maxAgeSeconds}; SameSite=Lax`;
3343
+ };
3344
+ const resolveThemeFromCookie = () => getThemeCookie() || defaultTheme;
3345
+ const getSystemColorScheme = () => {
3346
+ if (typeof window === 'undefined')
3347
+ return 'dark';
3348
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
3349
+ ? 'dark'
3350
+ : 'light';
3351
+ };
3352
+ const resolveEffectiveTheme = (theme) => {
3353
+ const choice = isThemePreference(theme) ? theme : defaultTheme;
3354
+ if (choice === 'system')
3355
+ return getSystemColorScheme();
3356
+ return choice;
3357
+ };
3358
+ const applyTheme = (theme) => {
3359
+ if (typeof document === 'undefined')
3360
+ return;
3361
+ const preference = isThemePreference(theme) ? theme : defaultTheme;
3362
+ const effective = resolveEffectiveTheme(preference);
3363
+ const root = document.documentElement;
3364
+ root.classList.toggle(darkClass, effective === 'dark');
3365
+ root.dataset.theme = preference;
3366
+ root.dataset.themeEffective = effective;
3367
+ root.style.colorScheme = effective === 'dark' ? 'dark' : 'light';
3368
+ };
3369
+ const getInitScript = () => `(function(){try{var m=document.cookie.match(/(?:^|;\\s*)${cookieName}=(light|system|dark)(?:;|$)/);var t=m?m[1]:'${defaultTheme}';var dark=t==='dark'||(t==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);var r=document.documentElement;r.classList.toggle('${darkClass}',dark);r.dataset.theme=t;r.dataset.themeEffective=dark?'dark':'light';r.style.colorScheme=dark?'dark':'light';}catch(e){document.documentElement.classList.add('${darkClass}');}})();`;
3370
+ const watchSystemTheme = (onChange) => {
3371
+ if (typeof window === 'undefined')
3372
+ return () => { };
3373
+ const media = window.matchMedia('(prefers-color-scheme: dark)');
3374
+ const handler = () => onChange(getSystemColorScheme());
3375
+ media.addEventListener('change', handler);
3376
+ return () => media.removeEventListener('change', handler);
3377
+ };
3378
+ return {
3379
+ cookieName,
3380
+ defaultTheme,
3381
+ themes: THEME_PREFERENCES,
3382
+ getThemeCookie,
3383
+ setThemeCookie,
3384
+ resolveThemeFromCookie,
3385
+ getSystemColorScheme,
3386
+ resolveEffectiveTheme,
3387
+ applyTheme,
3388
+ getInitScript,
3389
+ watchSystemTheme,
3390
+ };
3391
+ }
3392
+ const themeController = createThemeController();
3393
+ const { cookieName: THEME_COOKIE_NAME, defaultTheme, themes, getThemeCookie, setThemeCookie, resolveThemeFromCookie, getSystemColorScheme, resolveEffectiveTheme, applyTheme, watchSystemTheme, } = themeController;
3394
+ const themeInitScript = themeController.getInitScript();
3395
+
3396
+ const SunIcon = ({ className = '' }) => createElement('svg', {
3397
+ xmlns: 'http://www.w3.org/2000/svg',
3398
+ viewBox: '0 0 24 24',
3399
+ fill: 'none',
3400
+ stroke: 'currentColor',
3401
+ strokeWidth: 1.75,
3402
+ strokeLinecap: 'round',
3403
+ strokeLinejoin: 'round',
3404
+ className,
3405
+ 'aria-hidden': true,
3406
+ }, createElement('circle', { cx: 12, cy: 12, r: 4 }), createElement('path', {
3407
+ d: 'M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41',
3408
+ }));
3409
+ const MonitorIcon = ({ className = '' }) => createElement('svg', {
3410
+ xmlns: 'http://www.w3.org/2000/svg',
3411
+ viewBox: '0 0 24 24',
3412
+ fill: 'none',
3413
+ stroke: 'currentColor',
3414
+ strokeWidth: 1.75,
3415
+ strokeLinecap: 'round',
3416
+ strokeLinejoin: 'round',
3417
+ className,
3418
+ 'aria-hidden': true,
3419
+ }, createElement('rect', { x: 2, y: 3, width: 20, height: 14, rx: 2 }), createElement('path', { d: 'M8 21h8M12 17v4' }));
3420
+ const MoonIcon = ({ className = '' }) => createElement('svg', {
3421
+ xmlns: 'http://www.w3.org/2000/svg',
3422
+ viewBox: '0 0 24 24',
3423
+ fill: 'none',
3424
+ stroke: 'currentColor',
3425
+ strokeWidth: 1.75,
3426
+ strokeLinecap: 'round',
3427
+ strokeLinejoin: 'round',
3428
+ className,
3429
+ 'aria-hidden': true,
3430
+ }, createElement('path', {
3431
+ d: 'M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z',
3432
+ }));
3433
+ const ICONS = {
3434
+ light: SunIcon,
3435
+ system: MonitorIcon,
3436
+ dark: MoonIcon,
3437
+ };
3438
+ function ThemeToggle({ labels, className = '', controller = themeController, }) {
3439
+ const [theme, setTheme] = useStore(controller.defaultTheme);
3440
+ useEffect(() => {
3441
+ const saved = controller.resolveThemeFromCookie();
3442
+ setTheme(saved);
3443
+ controller.applyTheme(saved);
3444
+ }, []);
3445
+ useEffect(() => {
3446
+ if (theme !== 'system')
3447
+ return undefined;
3448
+ return controller.watchSystemTheme(() => controller.applyTheme('system'));
3449
+ }, [theme]);
3450
+ const selectTheme = (next) => {
3451
+ if (next === theme)
3452
+ return;
3453
+ setTheme(next);
3454
+ controller.setThemeCookie(next);
3455
+ controller.applyTheme(next);
3456
+ };
3457
+ return createElement('div', {
3458
+ className: `ryx-theme-segment ${className}`.trim(),
3459
+ role: 'radiogroup',
3460
+ 'aria-label': labels.title,
3461
+ }, ...controller.themes.map((id) => {
3462
+ const active = theme === id;
3463
+ const Icon = ICONS[id];
3464
+ return createElement('button', {
3465
+ key: id,
3466
+ type: 'button',
3467
+ role: 'radio',
3468
+ 'aria-checked': active ? 'true' : 'false',
3469
+ title: labels[id],
3470
+ className: `ryx-theme-segment-btn${active ? ' ryx-theme-segment-btn--active' : ''}`,
3471
+ onClick: () => selectTheme(id),
3472
+ }, Icon({ className: 'ryx-theme-segment-icon' }), createElement('span', { className: 'ryx-theme-segment-sr-only' }, labels[id]));
3473
+ }));
3474
+ }
3475
+ function ThemeInitScript({ controller = themeController, } = {}) {
3476
+ return createElement('script', {
3477
+ dangerouslySetInnerHTML: { __html: controller.getInitScript() },
3478
+ });
3479
+ }
3480
+
3481
+ const renderBrand = ({ image, title, href = '/', imageAlt, blockClass, linkClass, imageClass, titleClass, }) => {
3482
+ if (!image && !title)
3483
+ return null;
3484
+ const content = [
3485
+ image
3486
+ ? createElement('img', {
3487
+ src: image,
3488
+ alt: imageAlt ?? title ?? '',
3489
+ className: imageClass,
3490
+ })
3491
+ : null,
3492
+ title ? createElement('span', { className: titleClass }, title) : null,
3493
+ ].filter(Boolean);
3494
+ return createElement('div', { className: blockClass }, createElement('a', { href, className: linkClass }, ...content));
3495
+ };
3496
+ const renderChildren = (children) => {
3497
+ if (children == null || children === false)
3498
+ return [];
3499
+ return flattenArray(Array.isArray(children) ? children : [children]).filter((child) => child != null && child !== false);
3500
+ };
3501
+ const renderSlot = (className, children) => {
3502
+ const items = renderChildren(children);
3503
+ if (items.length === 0)
3504
+ return null;
3505
+ return createElement('div', { className }, ...items);
3506
+ };
3507
+ function Main({ maxWidth, className = '', innerClassName = '', children, }) {
3508
+ const innerStyle = maxWidth != null && maxWidth !== ''
3509
+ ? { maxWidth, marginInline: 'auto', width: '100%' }
3510
+ : undefined;
3511
+ return createElement('main', {
3512
+ className: `ryx-main ${className}`.trim(),
3513
+ }, createElement('div', {
3514
+ className: `ryx-main-inner ${innerClassName}`.trim(),
3515
+ style: innerStyle,
3516
+ }, ...renderChildren(children)));
3517
+ }
3518
+ function Header({ image, title, href = '/', imageAlt, sticky = true, className = '', children, }) {
3519
+ const headerClass = [
3520
+ 'ryx-header',
3521
+ sticky !== false ? 'ryx-header--sticky' : '',
3522
+ className,
3523
+ ]
3524
+ .filter(Boolean)
3525
+ .join(' ');
3526
+ return createElement('header', {
3527
+ className: headerClass,
3528
+ }, createElement('div', { className: 'ryx-header-inner' }, createElement('div', { className: 'ryx-header-start' }, renderBrand({
3529
+ image,
3530
+ title,
3531
+ href,
3532
+ imageAlt,
3533
+ blockClass: 'ryx-header-brand',
3534
+ linkClass: 'ryx-header-brand-link',
3535
+ imageClass: 'ryx-header-brand-image',
3536
+ titleClass: 'ryx-header-brand-title',
3537
+ })), renderSlot('ryx-header-end', children)));
3538
+ }
3539
+ function Footer({ image, title, description, href = '/', imageAlt, className = '', children, bottomStart, bottomEnd, }) {
3540
+ const brand = renderBrand({
3541
+ image,
3542
+ title,
3543
+ href,
3544
+ imageAlt,
3545
+ blockClass: 'ryx-footer-brand',
3546
+ linkClass: 'ryx-footer-brand-link',
3547
+ imageClass: 'ryx-footer-brand-image',
3548
+ titleClass: 'ryx-footer-brand-title',
3549
+ });
3550
+ const brandColumn = brand || description
3551
+ ? createElement('div', { className: 'ryx-footer-brand-column' }, brand, description
3552
+ ? createElement('p', { className: 'ryx-footer-description' }, description)
3553
+ : null)
3554
+ : null;
3555
+ const columns = renderChildren(children).map((child, index) => createElement('div', { key: index, className: 'ryx-footer-column' }, child));
3556
+ const bottomStartSlot = renderSlot('ryx-footer-bottom-start', bottomStart);
3557
+ const bottomEndSlot = renderSlot('ryx-footer-bottom-end', bottomEnd);
3558
+ const bottomBar = bottomStartSlot || bottomEndSlot
3559
+ ? createElement('div', { className: 'ryx-footer-bottom' }, bottomStartSlot, bottomEndSlot)
3560
+ : null;
3561
+ return createElement('footer', {
3562
+ className: `ryx-footer ${className}`.trim(),
3563
+ }, createElement('div', {
3564
+ className: 'ryx-footer-accent',
3565
+ 'aria-hidden': 'true',
3566
+ }), createElement('div', {
3567
+ className: 'ryx-footer-glow',
3568
+ 'aria-hidden': 'true',
3569
+ }), createElement('div', { className: 'ryx-footer-inner' }, brandColumn || columns.length > 0
3570
+ ? createElement('div', { className: 'ryx-footer-grid' }, brandColumn, ...columns)
3571
+ : null, bottomBar));
3572
+ }
3573
+
3574
+ function getRyunixI18nConfig() {
3575
+ try {
3576
+ const value = ryunix.config.i18n;
3577
+ if (value && Array.isArray(value.locales) && value.locales.length > 0) {
3578
+ return value;
3579
+ }
3580
+ }
3581
+ catch {
3582
+ }
3583
+ return null;
3584
+ }
3585
+
3586
+ function defineMessages(messages) {
3587
+ return messages;
3588
+ }
3589
+ function resolveMessageKey(tree, key) {
3590
+ if (!tree)
3591
+ return undefined;
3592
+ const parts = key.split('.');
3593
+ let node = tree;
3594
+ for (const part of parts) {
3595
+ if (node == null || typeof node === 'string')
3596
+ return undefined;
3597
+ node = node[part];
3598
+ }
3599
+ return typeof node === 'string' ? node : undefined;
3600
+ }
3601
+ function formatMessage(template, params) {
3602
+ if (!params)
3603
+ return template;
3604
+ return Object.entries(params).reduce((value, [name, replacement]) => value.replace(new RegExp(`\\{${escapeRegExp$2(name)}\\}`, 'g'), String(replacement)), template);
3605
+ }
3606
+ function escapeRegExp$2(value) {
3607
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3608
+ }
3609
+
3610
+ function normalizeI18nConfig(config) {
3611
+ const locales = [...new Set(config.locales.filter(Boolean))];
3612
+ if (locales.length === 0) {
3613
+ throw new Error('[Ryunix i18n] At least one locale is required.');
3614
+ }
3615
+ const defaultLocale = locales.includes(config.defaultLocale)
3616
+ ? config.defaultLocale
3617
+ : locales[0];
3618
+ return { ...config, locales, defaultLocale };
3619
+ }
3620
+ function pickLocale(record, locale, fallback) {
3621
+ return record?.[locale] ?? record?.[fallback];
3622
+ }
3623
+ function getLocaleFromPath(pathname, locales) {
3624
+ if (typeof pathname !== 'string' || locales.length === 0)
3625
+ return null;
3626
+ const pattern = new RegExp(`^/(${locales.map(escapeRegExp$1).join('|')})(/|$)`);
3627
+ const match = pathname.match(pattern);
3628
+ return match ? match[1] : null;
3629
+ }
3630
+ function localePath(locale, path = '', locales) {
3631
+ const normalized = path.startsWith('/') ? path : path ? `/${path}` : '';
3632
+ if (!locales.includes(locale))
3633
+ return normalized || '/';
3634
+ if (!normalized || normalized === '/')
3635
+ return `/${locale}`;
3636
+ return `/${locale}${normalized}`;
3637
+ }
3638
+ function swapLocalePath(pathname, targetLocale, options) {
3639
+ const { locales } = options;
3640
+ if (!locales.includes(targetLocale))
3641
+ return pathname;
3642
+ const current = getLocaleFromPath(pathname, locales);
3643
+ if (current) {
3644
+ const rest = pathname.slice(current.length + 1) || '';
3645
+ return localePath(targetLocale, rest, locales);
3646
+ }
3647
+ if (pathname.startsWith('/')) {
3648
+ return localePath(targetLocale, pathname, locales);
3649
+ }
3650
+ return localePath(targetLocale, '', locales);
3651
+ }
3652
+ function escapeRegExp$1(value) {
3653
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3654
+ }
3655
+
3656
+ const DEFAULT_LOCALE_COOKIE_NAME = 'ryunix_locale';
3657
+ function resolveCookieOptions(cookie, cookieName, maxAgeSeconds) {
3658
+ const enabled = cookie !== false;
3659
+ const cookieConfig = typeof cookie === 'object' ? cookie : {};
3660
+ return {
3661
+ enabled,
3662
+ cookieName: cookieName ?? cookieConfig.name ?? DEFAULT_LOCALE_COOKIE_NAME,
3663
+ maxAgeSeconds: maxAgeSeconds ?? cookieConfig.maxAgeSeconds ?? 365 * 24 * 60 * 60,
3664
+ };
3665
+ }
3666
+ function normalizeOptions(options) {
3667
+ const base = normalizeI18nConfig({
3668
+ locales: options.locales,
3669
+ defaultLocale: options.defaultLocale,
3670
+ });
3671
+ const cookie = resolveCookieOptions(options.cookie, options.cookieName, options.maxAgeSeconds);
3672
+ const localeLabels = { ...options.localeLabels };
3673
+ for (const locale of base.locales) {
3674
+ if (!localeLabels[locale])
3675
+ localeLabels[locale] = locale.toUpperCase();
3676
+ }
3677
+ return {
3678
+ locales: base.locales,
3679
+ defaultLocale: base.defaultLocale,
3680
+ localeLabels,
3681
+ cookieName: cookie.cookieName,
3682
+ maxAgeSeconds: cookie.maxAgeSeconds,
3683
+ cookieEnabled: cookie.enabled,
3684
+ messages: options.messages ?? {},
3685
+ };
3686
+ }
3687
+ function createTranslate(messages, locale, defaultLocale) {
3688
+ return (key, params) => {
3689
+ const value = resolveMessageKey(messages[locale], key) ??
3690
+ resolveMessageKey(messages[defaultLocale], key) ??
3691
+ key;
3692
+ return formatMessage(value, params);
3693
+ };
3694
+ }
3695
+ function createI18n(options) {
3696
+ const config = normalizeOptions(options);
3697
+ const { Provider: I18nContextProvider, useContext } = createContext('ryunix.i18n', {
3698
+ locale: config.defaultLocale,
3699
+ defaultLocale: config.defaultLocale,
3700
+ locales: config.locales,
3701
+ localeLabels: config.localeLabels,
3702
+ t: (key) => key,
3703
+ });
3704
+ const isLocale = (value) => typeof value === 'string' && config.locales.includes(value);
3705
+ const getLocaleCookie = () => {
3706
+ if (!config.cookieEnabled || typeof document === 'undefined')
3707
+ return null;
3708
+ const pattern = new RegExp(`(?:^|;\\s*)${config.cookieName}=(${config.locales.map(escapeRegExp).join('|')})(?:;|$)`);
3709
+ const match = document.cookie.match(pattern);
3710
+ return match ? match[1] : null;
3711
+ };
3712
+ const setLocaleCookie = (locale) => {
3713
+ if (!config.cookieEnabled ||
3714
+ typeof document === 'undefined' ||
3715
+ !isLocale(locale))
3716
+ return;
3717
+ document.cookie = `${config.cookieName}=${locale}; path=/; max-age=${config.maxAgeSeconds}; SameSite=Lax`;
3718
+ };
3719
+ const resolveLocaleFromCookie = () => getLocaleCookie() || config.defaultLocale;
3720
+ const getLocaleRedirectScript = () => {
3721
+ if (!config.cookieEnabled)
3722
+ return '';
3723
+ const localesPattern = config.locales.map(escapeRegExp).join('|');
3724
+ return `(function(){try{var m=document.cookie.match(/(?:^|;\\s*)${config.cookieName}=(${localesPattern})(?:;|$)/);var l=m?m[1]:'${config.defaultLocale}';var p=location.pathname;var r=new RegExp('^/(${localesPattern})(/|$)');if(!r.test(p)){location.replace('/'+l+(p==='/'?'':p));}}catch(e){}})();`;
3725
+ };
3726
+ const Provider = ({ locale, children, }) => {
3727
+ const resolved = isLocale(locale) ? locale : config.defaultLocale;
3728
+ const value = {
3729
+ locale: resolved,
3730
+ defaultLocale: config.defaultLocale,
3731
+ locales: config.locales,
3732
+ localeLabels: config.localeLabels,
3733
+ t: createTranslate(config.messages, resolved, config.defaultLocale),
3734
+ };
3735
+ return createElement(I18nContextProvider, { value, children });
3736
+ };
3737
+ const useI18n = () => useContext();
3738
+ const useLocale = () => useI18n().locale;
3739
+ const useTranslations = () => useI18n().t;
3740
+ const generateStaticParams = () => config.locales.map((locale) => ({ locale }));
3741
+ const LocaleSwitcher = ({ className = '' }) => {
3742
+ const { location, navigate } = useRouter();
3743
+ const { locale: current, localeLabels, locales } = useI18n();
3744
+ const onSelect = (locale) => {
3745
+ if (locale === current)
3746
+ return;
3747
+ setLocaleCookie(locale);
3748
+ const next = swapLocalePath(location, locale, {
3749
+ locales,
3750
+ defaultLocale: config.defaultLocale,
3751
+ });
3752
+ navigate(next.startsWith(`/${locale}`) ? next : `/${locale}`);
3753
+ };
3754
+ return createElement('div', {
3755
+ className: `ryunix-locale-switcher ${className}`.trim(),
3756
+ role: 'group',
3757
+ 'aria-label': 'Language',
3758
+ children: locales.map((locale) => createElement('button', {
3759
+ key: locale,
3760
+ type: 'button',
3761
+ onClick: () => onSelect(locale),
3762
+ className: locale === current
3763
+ ? 'ryunix-locale-switcher__btn is-active'
3764
+ : 'ryunix-locale-switcher__btn',
3765
+ 'aria-current': locale === current ? 'true' : undefined,
3766
+ }, localeLabels[locale] ?? locale)),
3767
+ });
3768
+ };
3769
+ return {
3770
+ config,
3771
+ Provider,
3772
+ useI18n,
3773
+ useLocale,
3774
+ useTranslations,
3775
+ LocaleSwitcher,
3776
+ generateStaticParams,
3777
+ getLocaleFromPath: (pathname) => getLocaleFromPath(pathname, config.locales),
3778
+ localePath: (locale, path = '') => localePath(locale, path, config.locales),
3779
+ swapLocalePath: (pathname, targetLocale) => swapLocalePath(pathname, targetLocale, {
3780
+ locales: config.locales,
3781
+ defaultLocale: config.defaultLocale,
3782
+ }),
3783
+ pickLocale: (record, locale) => pickLocale(record, locale, config.defaultLocale),
3784
+ getLocaleCookie,
3785
+ setLocaleCookie,
3786
+ resolveLocaleFromCookie,
3787
+ getLocaleRedirectScript,
3788
+ };
3789
+ }
3790
+ function createAppI18n(messages, fallback) {
3791
+ const fromConfig = getRyunixI18nConfig() ?? fallback;
3792
+ if (!fromConfig) {
3793
+ throw new Error('[Ryunix i18n] Missing ryunix.config.i18n. Add i18n: { locales, defaultLocale } to ryunix.config.js, pass a fallback to createAppI18n(), or call createI18n() directly.');
3794
+ }
3795
+ return createI18n({ ...fromConfig, messages });
3796
+ }
3797
+ function createI18nFromConfig(config, messages) {
3798
+ if (!config?.locales?.length) {
3799
+ throw new Error('[Ryunix i18n] Invalid i18n config: locales array is required.');
3800
+ }
3801
+ return createI18n({ ...config, messages });
3802
+ }
3803
+ function escapeRegExp(value) {
3804
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3805
+ }
3969
3806
 
3970
3807
  var Ryunix = /*#__PURE__*/Object.freeze({
3971
3808
  __proto__: null,
3972
3809
  Children: Children,
3810
+ DEFAULT_LOCALE_COOKIE_NAME: DEFAULT_LOCALE_COOKIE_NAME,
3811
+ DEFAULT_THEME_COOKIE_NAME: DEFAULT_THEME_COOKIE_NAME,
3973
3812
  ErrorBoundary: ErrorBoundary,
3813
+ Footer: Footer,
3974
3814
  Fragment: Fragment,
3975
- Hooks: hooks,
3815
+ Header: Header,
3816
+ Hooks: index,
3976
3817
  HydrationBoundary: HydrationBoundary,
3818
+ INTERNAL_META_KEYS: INTERNAL_META_KEYS,
3977
3819
  Link: Link,
3820
+ Main: Main,
3978
3821
  NavLink: NavLink,
3979
3822
  Priority: Priority,
3980
3823
  RouterProvider: RouterProvider,
3981
3824
  RyunixDevOverlay: RyunixDevOverlay,
3982
3825
  ServerBoundary: ServerBoundary,
3983
3826
  Suspense: Suspense,
3827
+ THEME_PREFERENCES: THEME_PREFERENCES,
3828
+ ThemeInitScript: ThemeInitScript,
3829
+ ThemeToggle: ThemeToggle,
3830
+ applyTheme: applyTheme,
3984
3831
  batchUpdates: batchUpdates,
3985
3832
  cloneElement: cloneElement,
3986
3833
  createActionProxy: createActionProxy,
3834
+ createAppI18n: createAppI18n,
3987
3835
  createContext: createContext,
3988
3836
  createElement: createElement,
3837
+ createI18n: createI18n,
3838
+ createI18nFromConfig: createI18nFromConfig,
3989
3839
  createPortal: createPortal,
3840
+ createThemeController: createThemeController,
3990
3841
  deepEqual: deepEqual,
3842
+ defineMessages: defineMessages,
3991
3843
  escapeHtml: escapeHtml,
3992
3844
  forwardRef: forwardRef,
3845
+ getLocaleFromPath: getLocaleFromPath,
3846
+ getRyunixI18nConfig: getRyunixI18nConfig,
3993
3847
  getState: getState,
3848
+ getSystemColorScheme: getSystemColorScheme,
3849
+ getThemeCookie: getThemeCookie,
3994
3850
  hydrate: hydrate,
3995
3851
  init: init,
3996
3852
  isValidElement: isValidElement,
3997
3853
  lazy: lazy,
3854
+ localePath: localePath,
3998
3855
  logHydrationBoundaryMismatch: logHydrationBoundaryMismatch,
3999
3856
  logHydrationBoundaryRecovery: logHydrationBoundaryRecovery,
4000
3857
  logHydrationFatal: logHydrationFatal,
4001
3858
  logHydrationInfo: logHydrationInfo,
4002
3859
  logHydrationRecoverable: logHydrationRecoverable,
4003
3860
  memo: memo,
3861
+ mergeRouteMetadata: mergeRouteMetadata,
3862
+ normalizeI18nConfig: normalizeI18nConfig,
3863
+ pickLocale: pickLocale,
4004
3864
  preload: preload,
4005
3865
  profiler: profiler,
4006
3866
  render: render,
@@ -4008,8 +3868,15 @@ var Ryunix = /*#__PURE__*/Object.freeze({
4008
3868
  renderToString: renderToString,
4009
3869
  renderToStringAsync: renderToStringAsync,
4010
3870
  resetIdCounter: resetIdCounter,
3871
+ resolveEffectiveTheme: resolveEffectiveTheme,
3872
+ resolvePageMetadata: resolvePageMetadata,
3873
+ resolveThemeFromCookie: resolveThemeFromCookie,
4011
3874
  safeRender: safeRender,
3875
+ setThemeCookie: setThemeCookie,
4012
3876
  shallowEqual: shallowEqual,
3877
+ swapLocalePath: swapLocalePath,
3878
+ themeController: themeController,
3879
+ themeInitScript: themeInitScript,
4013
3880
  useCallback: useCallback,
4014
3881
  useDebounce: useDebounce,
4015
3882
  useDeferredValue: useDeferredValue,
@@ -4033,10 +3900,79 @@ var Ryunix = /*#__PURE__*/Object.freeze({
4033
3900
  useSwitch: useSwitch,
4034
3901
  useThrottle: useThrottle,
4035
3902
  useTransition: useTransition,
3903
+ watchSystemTheme: watchSystemTheme,
4036
3904
  withProfiler: withProfiler
4037
3905
  });
4038
3906
 
4039
- if (typeof window !== 'undefined') window.Ryunix = Ryunix;
3907
+ const { Provider: MDXProvider, useContext: useMDXComponents } = createContext('ryunix.mdx', {});
3908
+ const getMDXComponents = (components) => {
3909
+ const contextComponents = useMDXComponents();
3910
+ return {
3911
+ ...contextComponents,
3912
+ ...components,
3913
+ };
3914
+ };
3915
+ const RYUNIX_STYLE_ENABLED = globalThis.process && String(globalThis.process.env?.RYUNIX_STYLE) === 'true';
3916
+ const ryxProps = (props) => {
3917
+ const { unstyled, ...rest } = props;
3918
+ if (unstyled || rest['data-ryx-unstyled']) {
3919
+ return { ...rest, 'data-ryx-unstyled': true };
3920
+ }
3921
+ return rest;
3922
+ };
3923
+ const mergeClassName = (existing, base) => {
3924
+ if (!existing)
3925
+ return base;
3926
+ if (Array.isArray(existing))
3927
+ return [...existing, base].join(' ');
3928
+ return `${existing} ${base}`;
3929
+ };
3930
+ const styledMdxHost = (tag, ryxClass, props) => {
3931
+ const next = ryxProps(props);
3932
+ if (!RYUNIX_STYLE_ENABLED || !ryxClass || next['data-ryx-unstyled']) {
3933
+ return createElement(tag, next);
3934
+ }
3935
+ return createElement(tag, {
3936
+ ...next,
3937
+ className: mergeClassName(next.className, ryxClass),
3938
+ });
3939
+ };
3940
+ const defaultComponents = {
3941
+ h1: (props) => styledMdxHost('h1', 'ryx-h1', props),
3942
+ h2: (props) => styledMdxHost('h2', 'ryx-h2', props),
3943
+ h3: (props) => styledMdxHost('h3', 'ryx-h3', props),
3944
+ h4: (props) => styledMdxHost('h4', 'ryx-h4', props),
3945
+ h5: (props) => styledMdxHost('h5', 'ryx-h5', props),
3946
+ h6: (props) => styledMdxHost('h6', 'ryx-h6', props),
3947
+ p: (props) => styledMdxHost('p', 'ryx-p', props),
3948
+ a: (props) => styledMdxHost('a', 'ryx-a', props),
3949
+ strong: (props) => styledMdxHost('strong', 'ryx-strong', props),
3950
+ em: (props) => styledMdxHost('em', 'ryx-em', props),
3951
+ code: (props) => styledMdxHost('code', 'ryx-code', props),
3952
+ ul: (props) => styledMdxHost('ul', 'ryx-ul', props),
3953
+ ol: (props) => styledMdxHost('ol', 'ryx-ol', props),
3954
+ li: (props) => styledMdxHost('li', 'ryx-li', props),
3955
+ blockquote: (props) => styledMdxHost('blockquote', 'ryx-blockquote', props),
3956
+ pre: (props) => styledMdxHost('pre', 'ryx-pre', props),
3957
+ hr: (props) => styledMdxHost('hr', 'ryx-hr', props),
3958
+ table: (props) => styledMdxHost('table', 'ryx-table', props),
3959
+ thead: (props) => styledMdxHost('thead', 'ryx-thead', props),
3960
+ tbody: (props) => styledMdxHost('tbody', 'ryx-tbody', props),
3961
+ tr: (props) => styledMdxHost('tr', 'ryx-tr', props),
3962
+ th: (props) => styledMdxHost('th', 'ryx-th', props),
3963
+ td: (props) => styledMdxHost('td', 'ryx-td', props),
3964
+ img: (props) => styledMdxHost('img', 'ryx-img', props),
3965
+ };
3966
+ const Image = ({ src, ...props }) => {
3967
+ return createElement('img', { ...props, src });
3968
+ };
3969
+ const MDXContent = ({ children, components = {}, }) => {
3970
+ const mergedComponents = getMDXComponents(components);
3971
+ return createElement(MDXProvider, { value: mergedComponents }, createElement('div', null, children));
3972
+ };
3973
+
3974
+ if (typeof window !== 'undefined')
3975
+ window.Ryunix = Ryunix;
4040
3976
 
4041
- export { Children, ErrorBoundary, Fragment, hooks as Hooks, HydrationBoundary, Image, Link, MDXContent, MDXProvider, NavLink, Priority, RouterProvider, RyunixDevOverlay, ServerBoundary, Suspense, batchUpdates, cloneElement, createActionProxy, createContext, createElement, createPortal, deepEqual, Ryunix as default, defaultComponents, escapeHtml, forwardRef, getMDXComponents, getState, hydrate, init, isValidElement, lazy, logHydrationBoundaryMismatch, logHydrationBoundaryRecovery, logHydrationFatal, logHydrationInfo, logHydrationRecoverable, memo, preload, profiler, render, renderToReadableStream, renderToString, renderToStringAsync, resetIdCounter, safeRender, shallowEqual, useCallback, useDebounce, useDeferredValue, useEffect, useHash, useId, useLayoutEffect, useMDXComponents, useMemo, useMetadata, usePathname, usePersistentStore, usePersistentStore as usePersitentStore, useProfiler, useQuery, useReducer, useRef, useRouter, useSearchParams, useStore, useStorePriority, useSwitch, useThrottle, useTransition, withProfiler };
3977
+ export { Children, DEFAULT_LOCALE_COOKIE_NAME, DEFAULT_THEME_COOKIE_NAME, ErrorBoundary, Footer, Fragment, Header, index as Hooks, HydrationBoundary, INTERNAL_META_KEYS, Image, Link, MDXContent, MDXProvider, Main, NavLink, Priority, RouterProvider, RyunixDevOverlay, ServerBoundary, Suspense, THEME_PREFERENCES, ThemeInitScript, ThemeToggle, applyTheme, batchUpdates, cloneElement, createActionProxy, createAppI18n, createContext, createElement, createI18n, createI18nFromConfig, createPortal, createThemeController, deepEqual, Ryunix as default, defaultComponents, defineMessages, escapeHtml, forwardRef, getLocaleFromPath, getMDXComponents, getRyunixI18nConfig, getState, getSystemColorScheme, getThemeCookie, hydrate, init, isValidElement, lazy, localePath, logHydrationBoundaryMismatch, logHydrationBoundaryRecovery, logHydrationFatal, logHydrationInfo, logHydrationRecoverable, memo, mergeRouteMetadata, normalizeI18nConfig, pickLocale, preload, profiler, render, renderToReadableStream, renderToString, renderToStringAsync, resetIdCounter, resolveEffectiveTheme, resolvePageMetadata, resolveThemeFromCookie, ryxProps, safeRender, setThemeCookie, shallowEqual, swapLocalePath, themeController, themeInitScript, useCallback, useDebounce, useDeferredValue, useEffect, useHash, useId, useLayoutEffect, useMDXComponents, useMemo, useMetadata, usePathname, usePersistentStore, usePersistentStore as usePersitentStore, useProfiler, useQuery, useReducer, useRef, useRouter, useSearchParams, useStore, useStorePriority, useSwitch, useThrottle, useTransition, watchSystemTheme, withProfiler };
4042
3978
  //# sourceMappingURL=Ryunix.esm.js.map