@unsetsoft/ryunixjs 1.2.5-canary.10 → 1.2.5-canary.12

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