@unsetsoft/ryunixjs 1.2.5-canary.11 → 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,25 +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
725
  if (state.hydrationFailed) ;
897
726
  else {
898
- // If there is a cursor left, it means these are SSR nodes that weren't matched
899
- // by any client fiber. We must remove them to avoid duplication.
900
727
  let cursor = state.hydrateCursor;
901
728
  let removed = 0;
902
729
  if (cursor &&
@@ -919,23 +746,17 @@ function commitRoot() {
919
746
  state.hydrateCursor = null;
920
747
  }
921
748
  commitWork(finishedWork.child);
922
- // If wipRoot was not reassigned by a synchronous dispatch during effects, clear it
923
749
  if (state.wipRoot === finishedWork) {
924
750
  state.wipRoot = null;
925
751
  }
926
752
  }
927
- /**
928
- * @param {RyunixFiber | null | undefined} fiber
929
- */
930
753
  function commitWork(fiber) {
931
754
  if (!fiber) {
932
755
  return;
933
756
  }
934
- // Handle portal fibers — they render into a different container
935
757
  if (fiber.type === RYUNIX_PORTAL || fiber._isPortal) {
936
758
  const portalContainer = fiber.containerInfo;
937
759
  if (portalContainer) {
938
- // Process portal children into the portal container
939
760
  const portalFiber = fiber.child;
940
761
  if (portalFiber) {
941
762
  commitPortalWork(portalFiber, portalContainer);
@@ -959,9 +780,7 @@ function commitWork(fiber) {
959
780
  }
960
781
  domParent.appendChild(fiber.dom);
961
782
  }
962
- // Layout effects run synchronously during commit
963
783
  runLayoutEffects(fiber);
964
- // Normal effects run after paint
965
784
  runNormalEffects(fiber);
966
785
  }
967
786
  else if (fiber.effectTag === EFFECT_TAGS.UPDATE) {
@@ -978,8 +797,6 @@ function commitWork(fiber) {
978
797
  if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
979
798
  console.log('[Ryunix Debug] Hydration fallback PLACEMENT:', fiber.type);
980
799
  }
981
- // Since container is cleared on fallback, treat as normal placement
982
- // No need to check fiber.dom.parentNode !== domParent because the container was cleared.
983
800
  if (fiber.dom != null) {
984
801
  domParent.appendChild(fiber.dom);
985
802
  }
@@ -998,7 +815,6 @@ function commitWork(fiber) {
998
815
  }
999
816
  }
1000
817
  else if (fiber.effectTag === EFFECT_TAGS.DELETION) {
1001
- // Run cleanups BEFORE removing DOM to allow cleanup functions to read DOM state
1002
818
  cancelEffectsDeep(fiber);
1003
819
  commitDeletion(fiber);
1004
820
  return;
@@ -1006,11 +822,6 @@ function commitWork(fiber) {
1006
822
  commitWork(fiber.child);
1007
823
  commitWork(fiber.sibling);
1008
824
  }
1009
- /**
1010
- * Commit work for portal children into a specific container
1011
- * @param {RyunixFiber | null | undefined} fiber
1012
- * @param {Element | DocumentFragment} portalContainer
1013
- */
1014
825
  const commitPortalWork = (fiber, portalContainer) => {
1015
826
  if (!fiber)
1016
827
  return;
@@ -1037,10 +848,6 @@ const commitPortalWork = (fiber, portalContainer) => {
1037
848
  commitPortalWork(fiber.child, portalContainer);
1038
849
  commitPortalWork(fiber.sibling, portalContainer);
1039
850
  };
1040
- /**
1041
- * @param {RyunixFiber} fiber
1042
- * @param {Node} domParent
1043
- */
1044
851
  const commitDeletion = (fiber, domParent) => {
1045
852
  if (fiber.dom) {
1046
853
  if (fiber.dom.parentNode) {
@@ -1130,852 +937,635 @@ const reconcileChildren = (wipFiber, elements) => {
1130
937
  });
1131
938
  };
1132
939
 
1133
- /**
1134
- * Bridge module to break circular dependencies between hooks and workers.
1135
- */
1136
- /** @type {import('../types/internal.js').ScheduleWorkFn | null} */
1137
- let scheduleWorkFn = null;
1138
- /**
1139
- * @param {import('../types/internal.js').ScheduleWorkFn} fn
1140
- */
1141
- const setScheduleWork = (fn) => {
1142
- scheduleWorkFn = fn;
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
+ };
1143
954
  };
1144
- /**
1145
- * @param {import('../types/internal.js').RyunixRootFiber} root
1146
- * @param {number} [priority]
1147
- */
1148
- const scheduleWork$1 = (root, priority) => {
1149
- if (scheduleWorkFn) {
1150
- return scheduleWorkFn(root, priority);
1151
- }
1152
- if (process.env.NODE_ENV !== 'production') {
1153
- console.warn('[Ryunix] scheduleWork called before being initialized.');
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;
1154
972
  }
973
+ return null;
1155
974
  };
1156
-
1157
- let isBatching = false;
1158
- let pendingUpdates = [];
1159
- function batchUpdates(callback) {
1160
- const wasBatching = isBatching;
1161
- isBatching = true;
1162
- try {
1163
- callback();
1164
- }
1165
- finally {
1166
- isBatching = wasBatching;
1167
- if (!isBatching && pendingUpdates.length > 0) {
1168
- flushUpdates();
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;
1169
981
  }
982
+ current = current.parentNode;
1170
983
  }
1171
- }
1172
- function queueUpdate(update) {
1173
- pendingUpdates.push(update);
1174
- if (!isBatching) {
1175
- flushUpdates();
1176
- }
1177
- }
1178
- function flushUpdates() {
1179
- if (pendingUpdates.length === 0)
1180
- return;
1181
- const updates = pendingUpdates;
1182
- pendingUpdates = [];
1183
- updates.forEach((update) => update());
1184
- }
1185
-
1186
- /**
1187
- * @typedef {import('../types/internal.js').RyunixComponent} RyunixComponent
1188
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
1189
- */
1190
- /**
1191
- * Development warnings
1192
- */
1193
- process.env.NODE_ENV !== 'production';
1194
- /**
1195
- * Hook call validation
1196
- */
1197
- const validateHookContext = (hookName = 'A hook') => {
1198
- const state = getState();
1199
- if (!state.wipFiber) {
1200
- throw new Error(`${hookName} can only be called inside function components. ` +
1201
- 'Make sure you are calling hooks at the top level of your component.');
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;
1202
993
  }
1203
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1204
- if (!Array.isArray(wipFiber.hooks)) {
1205
- 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;
1206
1002
  }
1003
+ return null;
1207
1004
  };
1208
-
1209
- /**
1210
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
1211
- * @typedef {import('../types/internal.js').RyunixHook} RyunixHook
1212
- * @typedef {import('../types/internal.js').RyunixRoute} RyunixRoute
1213
- * @typedef {import('../types/internal.js').RyunixRouterContextValue} RyunixRouterContextValue
1214
- * @typedef {import('../types/internal.js').RyunixMetadataTags} RyunixMetadataTags
1215
- * @typedef {import('../types/internal.js').RyunixMetadataOptions} RyunixMetadataOptions
1216
- * @typedef {import('../types/internal.js').RyunixRootFiber} RyunixRootFiber
1217
- * @typedef {import('../types/internal.js').RyunixComponent} RyunixComponent
1218
- */
1219
- /**
1220
- * @typedef {{ route: RyunixRoute | { component: RyunixComponent | null }, params: Record<string, string | string[]> }} RouteMatch
1221
- */
1222
- /**
1223
- * @param {unknown[] | undefined} oldDeps
1224
- * @param {unknown[] | undefined} newDeps
1225
- * @returns {boolean}
1226
- */
1227
- const haveDepsChanged = (oldDeps, newDeps) => {
1228
- if (!oldDeps || !newDeps)
1229
- return true;
1230
- if (oldDeps.length !== newDeps.length)
1231
- return true;
1232
- return oldDeps.some((dep, i) => !Object.is(dep, newDeps[i]));
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;
1233
1013
  };
1234
- /**
1235
- * @param {unknown} initialState
1236
- * @param {number} [priority]
1237
- * @returns {[unknown, (action: unknown, priority?: number) => void]}
1238
- */
1239
- const useStore = (initialState, priority = getCurrentPriority()) => {
1240
- // SSR safety check - more reliable than state.isServerRendering
1241
- if (typeof window === 'undefined') {
1242
- return [
1243
- is.function(initialState)
1244
- ? /** @type {() => unknown} */ initialState()
1245
- : initialState,
1246
- () => { },
1247
- ];
1248
- }
1014
+ const enqueueScopedRecovery = (boundaryFiber, boundaryDom, resumeCursor) => {
1015
+ if (!boundaryFiber || !boundaryDom)
1016
+ return;
1249
1017
  const state = getState();
1250
- if (state.isServerRendering) {
1251
- return [
1252
- is.function(initialState)
1253
- ? /** @type {() => unknown} */ initialState()
1254
- : initialState,
1255
- () => { },
1256
- ];
1257
- }
1258
- /**
1259
- * @param {unknown} state
1260
- * @param {unknown} action
1261
- */
1262
- const reducer = (state, action) => is.function(action) ? action(state) : action;
1263
- return useReducer(reducer, initialState, undefined, priority);
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;
1264
1028
  };
1265
- /**
1266
- * @param {(state: unknown, action: unknown) => unknown} reducer
1267
- * @param {unknown} initialState
1268
- * @param {((initial: unknown) => unknown)=} [init]
1269
- * @param {number} [defaultPriority]
1270
- * @returns {[unknown, (action: unknown, priority?: number) => void]}
1271
- */
1272
- const useReducer = (reducer, initialState, init, defaultPriority = getCurrentPriority()) => {
1273
- // SSR safety check - more reliable than state.isServerRendering
1274
- if (typeof window === 'undefined') {
1275
- return [init ? init(initialState) : initialState, () => { }];
1276
- }
1029
+
1030
+ const updateFunctionComponent = (fiber) => {
1277
1031
  const state = getState();
1278
- if (state.isServerRendering) {
1279
- return [init ? init(initialState) : initialState, () => { }];
1280
- }
1281
- validateHookContext();
1282
- const { hookIndex } = state;
1283
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1284
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1285
- const hook = {
1286
- hookID: hookIndex,
1287
- type: RYUNIX_TYPES.RYUNIX_STORE,
1288
- state: oldHook ? oldHook.state : init ? init(initialState) : initialState,
1289
- queue: /** @type {unknown[]} */ [],
1290
- };
1291
- if (oldHook?.queue) {
1292
- oldHook.queue.forEach(
1293
- /** @param {unknown} action */ (action) => {
1294
- try {
1295
- hook.state = reducer(hook.state, action);
1296
- }
1297
- catch (error) {
1298
- if (process.env.NODE_ENV !== 'production') {
1299
- console.error('Error in reducer:', error);
1300
- }
1301
- }
1302
- });
1032
+ state.wipFiber = fiber;
1033
+ state.hookIndex = 0;
1034
+ state.wipFiber.hooks = [];
1035
+ if (state.isHydrating) {
1036
+ fiber.effectTag = EFFECT_TAGS.HYDRATE;
1303
1037
  }
1304
- /** @param {unknown} action @param {number} [priority] */
1305
- const dispatch = (action, priority = defaultPriority) => {
1306
- if (action === undefined) {
1307
- if (process.env.NODE_ENV !== 'production') {
1308
- console.warn('dispatch called with undefined action');
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;
1309
1048
  }
1310
1049
  return;
1311
1050
  }
1312
- hook.queue.push(action);
1313
- const currentState = getState();
1314
- const activeRoot =
1315
- /** @type {RyunixRootFiber | null | undefined} */ currentState.currentRoot ||
1316
- currentState.wipRoot;
1317
- if (!activeRoot)
1318
- return;
1319
- const newRoot = /** @type {RyunixRootFiber} */ {
1320
- dom: activeRoot.dom,
1321
- props: activeRoot.props,
1322
- alternate:
1323
- /** @type {RyunixRootFiber | null} */ currentState.currentRoot || null,
1324
- };
1325
- queueUpdate(() => scheduleWork$1(newRoot, priority));
1326
- };
1327
- wipFiber.hooks[hookIndex] = hook;
1328
- state.hookIndex++;
1329
- return [hook.state, dispatch];
1330
- };
1331
- /**
1332
- * The `useEffect` function in JavaScript is used to manage side effects in functional components by
1333
- * comparing dependencies and executing a callback function when dependencies change.
1334
- * @param callback - The `callback` parameter in the `useEffect` function is a function that will be
1335
- * executed as the effect. This function can perform side effects like data fetching, subscriptions, or
1336
- * DOM manipulations.
1337
- * @param deps - The `deps` parameter in the `useEffect` function stands for dependencies. It is an
1338
- * optional array that contains values that the effect depends on. The effect will only re-run if any
1339
- * of the values in the `deps` array have changed since the last render. If the `deps` array
1340
- * @param {() => void | (() => void)} callback
1341
- * @param {unknown[] | undefined} deps
1342
- * @returns {void}
1343
- */
1344
- const useEffect = (callback, deps) => {
1345
- // SSR safety check - more reliable than state.isServerRendering
1346
- if (typeof window === 'undefined') {
1347
- return;
1348
- }
1349
- const state = getState();
1350
- if (state.isServerRendering) {
1351
- return;
1352
- }
1353
- validateHookContext();
1354
- if (!is.function(callback)) {
1355
- throw new Error('useEffect callback must be a function');
1356
1051
  }
1357
- if (deps !== undefined && !Array.isArray(deps)) {
1358
- 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;
1359
1058
  }
1360
- const { hookIndex } = state;
1361
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1362
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1363
- const hasChanged = haveDepsChanged(oldHook?.deps, deps);
1364
- const hook = {
1365
- hookID: hookIndex,
1366
- type: RYUNIX_TYPES.RYUNIX_EFFECT,
1367
- deps,
1368
- effect: hasChanged ? callback : null,
1369
- cancel: oldHook?.cancel,
1370
- };
1371
- wipFiber.hooks[hookIndex] = hook;
1372
- state.hookIndex++;
1059
+ reconcileChildren(fiber, children);
1373
1060
  };
1374
- /**
1375
- * The useRef function in JavaScript creates a reference object with an initial value for use in functional components.
1376
- * @param initialValue - The `initialValue` parameter in the `useRef` function represents the initial
1377
- * value that will be assigned to the `current` property of the reference object. This initial value
1378
- * will be used if there is no previous value stored in the hook.
1379
- * @param {unknown} initialValue
1380
- * @returns {{ current: unknown }}
1381
- */
1382
- const useRef = (initialValue) => {
1383
- // SSR safety check - more reliable than state.isServerRendering
1384
- if (typeof window === 'undefined') {
1385
- return { current: initialValue };
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;
1386
1067
  }
1068
+ return false;
1069
+ };
1070
+ const updateHostComponent = (fiber) => {
1387
1071
  const state = getState();
1388
- if (state.isServerRendering) {
1389
- return { current: initialValue };
1072
+ if (fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
1073
+ fiber._contextId =
1074
+ fiber.props?._contextId;
1075
+ fiber._contextValue = fiber.props?.value;
1390
1076
  }
1391
- validateHookContext();
1392
- const { hookIndex } = state;
1393
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1394
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1395
- const hook = {
1396
- hookID: hookIndex,
1397
- type: RYUNIX_TYPES.RYUNIX_REF,
1398
- value: oldHook
1399
- ? /** @type {{ value: { current: unknown } }} */ oldHook.value
1400
- : { current: initialValue },
1401
- };
1402
- wipFiber.hooks[hookIndex] = hook;
1403
- state.hookIndex++;
1404
- return /** @type {{ current: unknown }} */ hook.value;
1405
- };
1406
- /**
1407
- * The useMemo function in JavaScript is used to memoize the result of a computation based on
1408
- * dependencies.
1409
- * @param compute - The `compute` parameter in the `useMemo` function is a callback function that
1410
- * calculates the value that `useMemo` will memoize and return. This function will be called to compute
1411
- * the memoized value when necessary.
1412
- * @param deps - The `deps` parameter in the `useMemo` function refers to an array of dependencies.
1413
- * These dependencies are used to determine whether the memoized value needs to be recalculated or if
1414
- * the previously calculated value can be reused. The `useMemo` hook will recompute the memoized value
1415
- * only if
1416
- * @param {() => unknown} compute
1417
- * @param {unknown[]} deps
1418
- * @returns {unknown}
1419
- */
1420
- const useMemo = (compute, deps) => {
1421
- // SSR safety check - more reliable than state.isServerRendering
1422
- if (typeof window === 'undefined') {
1423
- return compute();
1424
- }
1425
- const state = getState();
1426
- if (state.isServerRendering) {
1427
- return compute();
1428
- }
1429
- validateHookContext();
1430
- if (!is.function(compute)) {
1431
- throw new Error('useMemo callback must be a function');
1432
- }
1433
- if (!Array.isArray(deps)) {
1434
- throw new Error('useMemo requires a dependencies array');
1435
- }
1436
- const { hookIndex } = state;
1437
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1438
- const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1439
- let value;
1440
- if (oldHook && !haveDepsChanged(oldHook.deps, deps)) {
1441
- value = /** @type {{ value?: unknown }} */ oldHook.value;
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;
1442
1082
  }
1443
- else {
1444
- try {
1445
- value = compute();
1083
+ else if (state.isHydrating && isUnderClientOnlyBoundary(fiber)) {
1084
+ if (!fiber.dom) {
1085
+ fiber.dom = createDom(fiber);
1086
+ fiber.effectTag = EFFECT_TAGS.PLACEMENT;
1446
1087
  }
1447
- catch (error) {
1448
- if (process.env.NODE_ENV !== 'production') {
1449
- 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);
1450
1110
  }
1451
- throw error;
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
+ }
1139
+ }
1140
+ }
1141
+ else {
1142
+ fiber.dom = createDom(fiber);
1452
1143
  }
1453
1144
  }
1454
- const hook = {
1455
- hookID: hookIndex,
1456
- type: RYUNIX_TYPES.RYUNIX_MEMO,
1457
- value,
1458
- deps,
1459
- };
1460
- wipFiber.hooks[hookIndex] = hook;
1461
- state.hookIndex++;
1462
- return value;
1145
+ const children = fiber.props?.children || [];
1146
+ reconcileChildren(fiber, children);
1463
1147
  };
1464
- /**
1465
- * The useCallback function in JavaScript ensures that a callback function is memoized based on its
1466
- * dependencies.
1467
- * @param callback - A function that you want to memoize and return for later use.
1468
- * @param deps - The `deps` parameter in the `useCallback` function refers to an array of dependencies.
1469
- * These dependencies are used to determine when the callback function should be re-evaluated and
1470
- * memoized. If any of the dependencies change, the callback function will be re-executed and the
1471
- * memoized value will
1472
- * @param {(...args: never[]) => unknown} callback
1473
- * @param {unknown[]} deps
1474
- * @returns {(...args: never[]) => unknown}
1475
- */
1476
- const useCallback = (callback, deps) => {
1477
- if (!is.function(callback)) {
1478
- throw new Error('useCallback requires a function as first argument');
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);
1479
1163
  }
1480
- return /** @type {(...args: never[]) => unknown} */ useMemo(() => callback, deps);
1481
- };
1482
- /**
1483
- * The createContext function creates a context provider and useContext hook in JavaScript.
1484
- * @param [contextId] - The `contextId` parameter in the `createContext` function is used to specify
1485
- * the unique identifier for the context being created. It defaults to `RYUNIX_TYPES.RYUNIX_CONTEXT` if
1486
- * not provided.
1487
- * @param [defaultValue] - The `defaultValue` parameter in the `createContext` function is used to
1488
- * specify the default value that will be returned by the `useContext` hook if no provider is found in
1489
- * the component tree. It is an optional parameter, and if not provided, an empty object `{}` will be
1490
- * used as
1491
- * @param {string | symbol} [contextId]
1492
- * @param {unknown} [defaultValue]
1493
- * @returns {{ Provider: RyunixComponent & { _contextId?: string | symbol }, useContext: (ctxID?: string | symbol) => unknown }}
1494
- */
1495
- const createContext = (contextId = RYUNIX_TYPES.RYUNIX_CONTEXT, defaultValue = {}) => {
1496
- /** @param {{ value?: unknown, children?: import('../types/internal.js').RyunixNode }} props */
1497
- const Provider = ({ value, children }) => {
1498
- return createElement(RYUNIX_TYPES.RYUNIX_CONTEXT, { value, children, _contextId: contextId }, ...flattenArray([children]));
1499
- };
1500
- Provider._contextId = contextId;
1501
- /** @param {string | symbol} [ctxID] */
1502
- const useContext = (ctxID = contextId) => {
1503
- const state = getState();
1504
- if (state.isServerRendering) {
1505
- const ssrContexts =
1506
- /** @type {Record<string | symbol, unknown> | undefined} */ state.ssrContexts;
1507
- return ssrContexts && ssrContexts[ctxID] !== undefined
1508
- ? ssrContexts[ctxID]
1509
- : defaultValue;
1510
- }
1511
- validateHookContext();
1512
- /** @type {RyunixFiber | null | undefined} */
1513
- let fiber = /** @type {RyunixFiber} */ state.wipFiber;
1514
- while (fiber) {
1515
- if (fiber._contextId === ctxID && fiber._contextValue !== undefined) {
1516
- return fiber._contextValue;
1517
- }
1518
- const fiberType = fiber.type;
1519
- if (fiberType?._contextId === ctxID && fiber.props?.value !== undefined) {
1520
- return fiber.props.value;
1521
- }
1522
- fiber = fiber.parent;
1523
- }
1524
- return defaultValue;
1525
- };
1526
- return {
1527
- Provider:
1528
- /** @type {RyunixComponent & { _contextId?: string | symbol }} */ Provider,
1529
- useContext,
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,
1530
1176
  };
1177
+ scheduleWork$1(root, undefined);
1531
1178
  };
1532
- /**
1533
- * The `useQuery` function extracts query parameters from the URL in a browser environment.
1534
- * @returns {Record<string, string>}
1535
- */
1536
- const useQuery = () => {
1537
- if (typeof window === 'undefined')
1538
- return {};
1539
- const searchParams = new URLSearchParams(window.location.search);
1540
- /** @type {Record<string, string>} */
1541
- const query = {};
1542
- for (const [key, value] of searchParams.entries()) {
1543
- query[key] = value;
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);
1544
1188
  }
1545
- return query;
1546
1189
  };
1547
- /**
1548
- * The function `useHash` in JavaScript is used to manage and update the hash portion of the URL in a
1549
- * web application.
1550
- * @returns {string}
1551
- */
1552
- const useHash = () => {
1553
- if (typeof window === 'undefined')
1554
- return '';
1555
- const [hash, setHash] = useStore(window.location.hash);
1556
- useEffect(() => {
1557
- const onHashChange = () => setHash(window.location.hash);
1558
- window.addEventListener('hashchange', onHashChange);
1559
- return () => window.removeEventListener('hashchange', onHashChange);
1560
- }, []);
1561
- return /** @type {string} */ hash;
1562
- };
1563
- /**
1564
- * The `useMetadata` function in JavaScript is used to dynamically update metadata tags in the document
1565
- * head based on provided tags and options.
1566
- * @param [tags] - The `tags` parameter in the `useMetadata` function is an object that contains
1567
- * metadata information for the webpage. It can include properties like `pageTitle`, `canonical`, and
1568
- * other custom metadata tags like `og:title`, `og:description`, `twitter:title`,
1569
- * `twitter:description`, etc. These tags
1570
- * @param [options] - The `options` parameter in the `useMetadata` function is an object that can
1571
- * contain the following properties:
1572
- * - `title`: An object that can have the following properties:
1573
- * - `template`: A string that defines the template for the page title. It can include a placeholder
1574
- * `%s` that will be replaced with the actual page title.
1575
- * - `prefix`: A string that will be used as the default title if no specific page title is provided.
1576
- * This hook can't be reached by google crawler.
1577
- * @param {RyunixMetadataTags} [tags]
1578
- * @param {RyunixMetadataOptions} [options]
1579
- * @returns {void}
1580
- */
1581
- const useMetadata = (tags = {}, options = {}) => {
1190
+ const recoverHydrationFailureIfNeeded = () => {
1582
1191
  const state = getState();
1583
- if (state.isServerRendering) {
1584
- state.ssrMetadata = { ...state.ssrMetadata, ...tags };
1192
+ if (!state.hydrationFailed || state.hydrationRecover)
1585
1193
  return;
1586
- }
1587
- useEffect(() => {
1588
- if (typeof document === 'undefined')
1589
- return;
1590
- // ...
1591
- let finalTitle = 'Ryunix App';
1592
- const template = options.title?.template;
1593
- const defaultTitle = options.title?.prefix || 'Ryunix App';
1594
- const pageTitle = tags.pageTitle || tags.title;
1595
- if (is.string(pageTitle) && pageTitle.trim()) {
1596
- finalTitle = template?.includes('%s')
1597
- ? template.replace('%s', pageTitle)
1598
- : pageTitle;
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);
1599
1220
  }
1600
1221
  else {
1601
- finalTitle = defaultTitle;
1222
+ updateHostComponent(fiber);
1602
1223
  }
1603
- document.title = finalTitle;
1604
- if (tags.canonical) {
1605
- let link = document.querySelector('link[rel="canonical"]');
1606
- if (!link) {
1607
- link = document.createElement('link');
1608
- link.setAttribute('rel', 'canonical');
1609
- document.head.appendChild(link);
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
+ }
1610
1240
  }
1611
- link.setAttribute('href', tags.canonical);
1241
+ catch (e) { }
1612
1242
  }
1613
- Object.entries(tags).forEach(([key, value]) => {
1614
- if (['title', 'pageTitle', 'canonical'].includes(key))
1615
- return;
1616
- const isProperty = key.startsWith('og:') || key.startsWith('twitter:');
1617
- const selector = `meta[${isProperty ? 'property' : 'name'}='${key}']`;
1618
- let meta = document.head.querySelector(selector);
1619
- if (!meta) {
1620
- meta = document.createElement('meta');
1621
- meta.setAttribute(isProperty ? 'property' : 'name', key);
1622
- document.head.appendChild(meta);
1623
- }
1624
- meta.setAttribute('content', value);
1625
- });
1626
- }, [JSON.stringify(tags), JSON.stringify(options)]);
1627
- };
1628
- // Router Context
1629
- /** @type {ReturnType<typeof createContext>} */
1630
- const RouterContext = createContext('ryunix.navigation',
1631
- /** @type {RyunixRouterContextValue} */ {
1632
- location: '/',
1633
- params: {},
1634
- query: {},
1635
- /** @param {string} _path */
1636
- navigate: (_path) => { },
1637
- route: null,
1638
- });
1639
- /**
1640
- * @param {RyunixRoute[]} routes
1641
- * @param {string} path
1642
- * @returns {RouteMatch}
1643
- */
1644
- const findRoute = (routes, path) => {
1645
- const pathname = path.split('?')[0].split('#')[0];
1646
- const notFoundRoute = routes.find((route) => route.NotFound);
1647
- const notFound = notFoundRoute
1648
- ? { route: { component: notFoundRoute.NotFound }, params: {} }
1649
- : { route: { component: null }, params: {} };
1650
- for (const route of routes) {
1651
- if (route.subRoutes) {
1652
- const childRoute = findRoute(route.subRoutes, path);
1653
- if (childRoute)
1654
- return childRoute;
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;
1655
1253
  }
1656
- if (route.path === '*')
1657
- return notFound;
1658
- if (!route.path || typeof route.path !== 'string')
1659
- continue;
1660
- /** @type {{ key: string, isCatchAll: boolean }[]} */
1661
- const keys = [];
1662
- const pattern = new RegExp(`^${route.path.replace(/:(\.\.\.)?(\w+)/g, (
1663
- /** @type {string} */ match,
1664
- /** @type {string | undefined} */ isCatchAll,
1665
- /** @type {string} */ key) => {
1666
- keys.push({ key, isCatchAll: !!isCatchAll });
1667
- return isCatchAll ? '(.+)' : '([^/]+)';
1668
- })}$`);
1669
- const matchPath = pathname.match(pattern);
1670
- if (matchPath) {
1671
- const params = keys.reduce((acc, keyObj, index) => {
1672
- const val = matchPath[index + 1];
1673
- acc[keyObj.key] = keyObj.isCatchAll && val ? val.split('/') : val;
1674
- return acc;
1675
- },
1676
- /** @type {Record<string, string | string[]>} */ {});
1677
- return { route, params };
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;
1678
1266
  }
1679
1267
  }
1680
- return notFound;
1681
- };
1682
- /**
1683
- * @returns {string}
1684
- */
1685
- const getSsrPathname = () => {
1686
- const pathname = globalThis?.window?.location?.pathname;
1687
- if (typeof pathname === 'string' && pathname) {
1688
- return pathname.split('?')[0].split('#')[0];
1268
+ if (fiber.child) {
1269
+ return fiber.child;
1689
1270
  }
1690
- return '/';
1691
- };
1692
- /**
1693
- * The `RouterProvider` component manages routing in a Ryunix application by updating the location based
1694
- * on window events and providing context for the current route.
1695
- * @param {{ routes: RyunixRoute[], children?: import('../types/internal.js').RyunixNode }} props
1696
- * @returns {import('./createElement.js').RyunixElement}
1697
- */
1698
- const RouterProvider = ({ routes, children }) => {
1699
- // SSR: Return server-safe version without hooks
1700
- if (typeof window === 'undefined') {
1701
- const location = getSsrPathname();
1702
- const currentRouteData = findRoute(routes, location);
1703
- /** @type {RyunixRouterContextValue} */
1704
- const contextValue = {
1705
- location,
1706
- params: currentRouteData.params || {},
1707
- query: {},
1708
- navigate: () => { },
1709
- route: currentRouteData.route,
1710
- };
1711
- return createElement(
1712
- /** @type {string | symbol | Function} */ RouterContext.Provider, { value: contextValue }, Fragment({ children }));
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;
1713
1280
  }
1714
- const [location, setLocation] =
1715
- /** @type {[string, (action: unknown, priority?: number) => void]} */ useStore(window.location.pathname);
1716
- useEffect(() => {
1717
- const update = () => setLocation(window.location.pathname);
1718
- window.addEventListener('popstate', update);
1719
- window.addEventListener('hashchange', update);
1720
- return () => {
1721
- window.removeEventListener('popstate', update);
1722
- window.removeEventListener('hashchange', update);
1723
- };
1724
- }, []);
1725
- /** @param {string} path */
1726
- const navigate = (path) => {
1727
- if (typeof window !== 'undefined' && window.__RYUNIX_MPA__) {
1728
- window.location.assign(path);
1729
- return;
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
+ }
1730
1295
  }
1731
- window.history.pushState({}, '', path);
1732
- setLocation(path);
1733
- };
1734
- const currentRouteData = findRoute(routes, location);
1735
- const query = useQuery();
1736
- /** @type {RyunixRouterContextValue} */
1737
- const contextValue = {
1738
- location,
1739
- params: currentRouteData.params || {},
1740
- query,
1741
- navigate,
1742
- route: currentRouteData.route,
1743
- };
1744
- return createElement(
1745
- /** @type {string | symbol | Function} */ RouterContext.Provider, { value: contextValue }, Fragment({ children }));
1746
- };
1747
- /**
1748
- * The function `useRouter` returns the context of the Router for navigation in a Ryunix application.
1749
- * @returns {RyunixRouterContextValue}
1750
- */
1751
- const useRouter = () => {
1752
- return RouterContext.useContext('ryunix.navigation');
1753
- };
1754
- /**
1755
- * The `Children` function in JavaScript uses router hooks to handle scrolling to a specific element
1756
- * based on the hash in the URL.
1757
- * @returns {import('./createElement.js').RyunixElement | null}
1758
- */
1759
- const Children = () => {
1760
- const { route, params, query, location } = useRouter();
1761
- if (!route || !route.component)
1762
- return null;
1763
- const hash = useHash();
1764
- useEffect(() => {
1765
- if (hash) {
1766
- const id = /** @type {string} */ hash.slice(1);
1767
- const el = document.getElementById(id);
1768
- if (el)
1769
- el.scrollIntoView({ block: 'start', behavior: 'smooth' });
1296
+ if (state.nextUnitOfWork) {
1297
+ state.nextUnitOfWork = performUnitOfWork(state.nextUnitOfWork);
1770
1298
  }
1771
- }, [hash]);
1772
- return createElement(
1773
- /** @type {string | symbol | Function} */ route.component, {
1774
- key: location,
1775
- params,
1776
- query,
1777
- hash,
1778
- location,
1779
- });
1780
- };
1781
- /**
1782
- * usePathname - Returns the current pathname
1783
- * @returns {string}
1784
- */
1785
- const usePathname = () => {
1786
- const { location } = useRouter();
1787
- return location.split('?')[0].split('#')[0];
1788
- };
1789
- /**
1790
- * useSearchParams - Returns the current URLSearchParams object
1791
- * @returns {URLSearchParams}
1792
- */
1793
- const useSearchParams = () => {
1794
- const { query } = useRouter();
1795
- 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
+ }
1796
1311
  };
1797
- /**
1798
- * Link - Base link component for SPA navigation
1799
- * Supports optional prefetching of lazy components.
1800
- * @param {{ to: string, prefetch?: boolean, children?: import('../types/internal.js').RyunixNode } & Record<string, unknown>} props
1801
- * @returns {import('./createElement.js').RyunixElement}
1802
- */
1803
- const Link = ({ to, prefetch = true, ...props }) => {
1804
- const { navigate } = useRouter();
1805
- /** @param {MouseEvent} e */
1806
- const handleClick = (e) => {
1807
- if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
1808
- return;
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;
1809
1324
  }
1810
- e.preventDefault();
1811
- navigate(to);
1812
- };
1813
- const handleMouseEnter = () => {
1814
- };
1815
- const { className: _omitClassName, ...cleanedProps } = props;
1816
- return createElement('a', {
1817
- href: to,
1818
- onClick: handleClick,
1819
- onMouseEnter: handleMouseEnter,
1820
- className: props.className || props['ryunix-class'],
1821
- ...cleanedProps,
1822
- }, props.children);
1823
- };
1824
- /**
1825
- * The NavLink function in JavaScript is a component that generates a link element with customizable
1826
- * classes and active state based on the current location.
1827
- * @param {{ to: string, exact?: boolean, children?: import('../types/internal.js').RyunixNode } & Record<string, unknown>} props
1828
- * @returns {import('./createElement.js').RyunixElement}
1829
- */
1830
- const NavLink = ({ to, exact = false, ...props }) => {
1831
- const { location, navigate } = useRouter();
1832
- const isActive = exact ? location === to : location.startsWith(to);
1833
- /** @param {string | ((args: { isActive: boolean }) => string) | undefined} cls */
1834
- const resolveClass = (cls) => typeof cls === 'function' ? cls({ isActive }) : cls || '';
1835
- /** @param {MouseEvent} e */
1836
- const handleClick = (e) => {
1837
- if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
1838
- return;
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
+ });
1839
1334
  }
1840
- e.preventDefault();
1841
- navigate(to);
1842
- };
1843
- const classAttrName = props['ryunix-class'] ? 'ryunix-class' : 'className';
1844
- const classAttrValue = resolveClass(
1845
- /** @type {string | ((args: { isActive: boolean }) => string) | undefined} */ props['ryunix-class'] || props.className);
1846
- const { ['ryunix-class']: _omitRyunix, className: _omitClassName, ...cleanedProps } = props;
1847
- return createElement('a', {
1848
- href: to,
1849
- onClick: handleClick,
1850
- [classAttrName]: classAttrValue,
1851
- ...cleanedProps,
1852
- }, props.children);
1335
+ else {
1336
+ rIC(workLoop);
1337
+ }
1338
+ }
1853
1339
  };
1854
- /**
1855
- * useStore with priority support
1856
- * @param {unknown} initialState
1857
- * @returns {[unknown, (action: unknown, priority?: number) => void]}
1858
- */
1859
- const useStorePriority = (initialState) => {
1860
- /** @param {unknown} state @param {{ value: unknown, priority?: number }} action */
1861
- const reducer = (state, action) => typeof action === 'function'
1862
- ? /** @type {{ value: (s: unknown) => unknown }} */ action.value(state)
1863
- : action.value;
1864
- const [state, baseDispatch] = useReducer(reducer, initialState, undefined);
1865
- /** @param {unknown} action @param {number} [priority] */
1866
- const dispatch = (action, priority = Priority.NORMAL) => {
1867
- const wrappedAction = {
1868
- value: action,
1869
- priority,
1870
- };
1871
- scheduleUpdate(() => baseDispatch(wrappedAction, priority), priority);
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,
1872
1355
  };
1873
- return [state, dispatch];
1356
+ scheduleWork(root);
1357
+ return root;
1874
1358
  };
1875
- /**
1876
- * useTransition - Mark updates as non-urgent
1877
- * @returns {[boolean, (callback: () => void) => void]}
1878
- */
1879
- const useTransition = () => {
1880
- const [isPending, setIsPending] = useStorePriority(false);
1881
- /** @param {() => void} callback */
1882
- const startTransition = (callback) => {
1883
- setIsPending(true, Priority.IMMEDIATE);
1884
- setTimeout(() => {
1885
- runWithPriority(Priority.LOW, () => {
1886
- callback();
1887
- setIsPending(false, Priority.LOW);
1888
- });
1889
- }, 0);
1890
- };
1891
- return [/** @type {boolean} */ isPending, startTransition];
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;
1892
1370
  };
1893
- /**
1894
- * useDeferredValue - Defer value updates
1895
- * @param {unknown} value
1896
- * @returns {unknown}
1897
- */
1898
- const useDeferredValue = (value) => {
1899
- const [deferredValue, setDeferredValue] = useStorePriority(value);
1900
- useEffect(() => {
1901
- const timeout = setTimeout(() => {
1902
- setDeferredValue(value, Priority.LOW);
1903
- }, 100);
1904
- return () => clearTimeout(timeout);
1905
- }, [value]);
1906
- return deferredValue;
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;
1907
1387
  };
1908
- /**
1909
- * The `usePersitentStore` function manages state using local storage in JavaScript, allowing for easy
1910
- * storage and retrieval of data.
1911
- * @param key - The `key` parameter in the `usePersitentStore` function is a string that represents the key
1912
- * under which the data will be stored in the browser's local storage. It is used to retrieve and store
1913
- * data associated with this specific key.
1914
- * @param [initialState] - The `initialState` parameter in the `usePersitentStore` function is the initial
1915
- * value that will be used if there is no data stored in the local storage under the specified `key`.
1916
- * It serves as the default value for the state if no data is retrieved from the local storage.
1917
- * @param {string} key
1918
- * @param {unknown} [initialState]
1919
- * @returns {[unknown, (value: unknown) => void]}
1920
- */
1921
- const usePersistentStore = (key, initialState = '') => {
1922
- const [state, dispatch] = useStore(() => {
1923
- try {
1924
- const item = window.localStorage.getItem(key);
1925
- return item ? JSON.parse(item) : initialState;
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.`);
1926
1395
  }
1927
- catch (error) {
1928
- 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}`);
1929
1407
  }
1930
- });
1931
- /**
1932
- * The function `setValue` dispatches a value and stores it in the local storage as a JSON string,
1933
- * handling any errors with a console log.
1934
- * @param value - The `value` parameter in the `setValue` function is the data that you want to set.
1935
- * It is dispatched to update the state and then stored in the browser's local storage after being
1936
- * converted to a JSON string.
1937
- * @param {unknown} value
1938
- */
1939
- const setValue = (value) => {
1940
- try {
1941
- dispatch(value);
1942
- window.localStorage.setItem(key, JSON.stringify(value));
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}`);
1943
1420
  }
1944
- catch (error) {
1945
- 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);
1946
1436
  }
1947
- };
1948
- return [state, setValue];
1437
+ if (onError)
1438
+ onError(error);
1439
+ return null;
1440
+ }
1949
1441
  };
1950
- /**
1951
- * The `useSwitch` function returns a state value and a toggle function to switch the state between
1952
- * true and false.
1953
- * @param [initialState=false] - The `initialState` parameter in the `useSwitch` function is used to
1954
- * set the initial value of the state. If no value is provided when calling `useSwitch`, the default
1955
- * initial state will be `false`.
1956
- * @param {boolean} [initialState]
1957
- * @returns {[boolean, () => void]}
1958
- */
1959
- const useSwitch = (initialState = false) => {
1960
- const [state, dispatch] = useStore(initialState);
1961
- /**
1962
- * The function `toggle` toggles the state by dispatching the opposite value of the current state.
1963
- * Uses functional update to avoid stale closure issues with rapid calls.
1964
- */
1965
- const toggle = () => {
1966
- dispatch(/** @param {boolean} prev */ (prev) => !prev);
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: [],
1967
1530
  };
1968
- return [/** @type {boolean} */ state, toggle];
1969
- };
1970
- /**
1971
- * useLayoutEffect - Like useEffect but runs synchronously after DOM mutations
1972
- * and before the browser paints. Use for DOM measurements.
1973
- * @param {() => void | (() => void)} callback - Effect callback
1974
- * @param {unknown[] | undefined} deps - Dependencies array
1975
- * @returns {void}
1976
- */
1977
- const useLayoutEffect = (callback, deps) => {
1978
- // SSR safety check - more reliable than state.isServerRendering
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) => {
1979
1569
  if (typeof window === 'undefined') {
1980
1570
  return;
1981
1571
  }
@@ -1985,13 +1575,13 @@ const useLayoutEffect = (callback, deps) => {
1985
1575
  }
1986
1576
  validateHookContext();
1987
1577
  if (!is.function(callback)) {
1988
- throw new Error('useLayoutEffect callback must be a function');
1578
+ throw new Error('useEffect callback must be a function');
1989
1579
  }
1990
1580
  if (deps !== undefined && !Array.isArray(deps)) {
1991
- throw new Error('useLayoutEffect dependencies must be an array or undefined');
1581
+ throw new Error('useEffect dependencies must be an array or undefined');
1992
1582
  }
1993
1583
  const { hookIndex } = state;
1994
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1584
+ const wipFiber = state.wipFiber;
1995
1585
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1996
1586
  const hasChanged = haveDepsChanged(oldHook?.deps, deps);
1997
1587
  const hook = {
@@ -2000,789 +1590,498 @@ const useLayoutEffect = (callback, deps) => {
2000
1590
  deps,
2001
1591
  effect: hasChanged ? callback : null,
2002
1592
  cancel: oldHook?.cancel,
2003
- isLayout: true, // Flag to run synchronously during commit
2004
1593
  };
2005
- wipFiber.hooks[hookIndex] = hook;
1594
+ wipFiber.hooks[hookIndex] =
1595
+ hook;
2006
1596
  state.hookIndex++;
2007
1597
  };
2008
- // Counter for deterministic ID generation
2009
- let idCounter = 0;
2010
- /**
2011
- * Reset the idCounter for useId - call this before each SSR renderToString
2012
- * to ensure deterministic IDs across multiple renders
2013
- * @returns {void}
2014
- */
2015
- const resetIdCounter = () => {
2016
- idCounter = 0;
2017
- };
2018
- /**
2019
- * useId - Generate a deterministic, unique ID that is stable across SSR and hydration.
2020
- * @returns {string} A unique ID string
2021
- */
2022
- const useId = () => {
1598
+ const useRef = (initialValue) => {
1599
+ if (typeof window === 'undefined') {
1600
+ return { current: initialValue };
1601
+ }
2023
1602
  const state = getState();
2024
1603
  if (state.isServerRendering) {
2025
- // On server, use a simple incrementing counter (reset per renderToString call)
2026
- return `:r${idCounter++}:`;
1604
+ return { current: initialValue };
2027
1605
  }
2028
1606
  validateHookContext();
2029
1607
  const { hookIndex } = state;
2030
- const wipFiber = /** @type {RyunixFiber} */ state.wipFiber;
1608
+ const wipFiber = state.wipFiber;
2031
1609
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
2032
1610
  const hook = {
2033
1611
  hookID: hookIndex,
2034
1612
  type: RYUNIX_TYPES.RYUNIX_REF,
2035
1613
  value: oldHook
2036
- ? /** @type {{ value: string }} */ oldHook.value
2037
- : `:r${idCounter++}:`,
1614
+ ? oldHook.value
1615
+ : { current: initialValue },
2038
1616
  };
2039
- wipFiber.hooks[hookIndex] = hook;
1617
+ wipFiber.hooks[hookIndex] =
1618
+ hook;
2040
1619
  state.hookIndex++;
2041
- return /** @type {string} */ /** @type {{ value: string }} */ hook.value;
2042
- };
2043
- /**
2044
- * useDebounce - Returns a debounced version of the value that only updates
2045
- * after the specified delay has passed since the last change.
2046
- * @param {unknown} value - Value to debounce
2047
- * @param {number} delay - Delay in milliseconds (default: 300)
2048
- * @returns {unknown} Debounced value
2049
- */
2050
- const useDebounce = (value, delay = 300) => {
2051
- const [debouncedValue, setDebouncedValue] = useStore(value);
2052
- useEffect(() => {
2053
- const timer = setTimeout(() => {
2054
- setDebouncedValue(value);
2055
- }, delay);
2056
- return () => clearTimeout(timer);
2057
- }, [value, delay]);
2058
- return debouncedValue;
1620
+ return hook.value;
2059
1621
  };
2060
- /**
2061
- * useThrottle - Returns a throttled version of the value that only updates
2062
- * at most once per specified interval.
2063
- * @param {unknown} value - Value to throttle
2064
- * @param {number} interval - Minimum interval in milliseconds (default: 300)
2065
- * @returns {unknown} Throttled value
2066
- */
2067
- const useThrottle = (value, interval = 300) => {
2068
- const [throttledValue, setThrottledValue] = useStore(value);
2069
- const lastUpdated = useRef(Date.now());
2070
- useEffect(() => {
2071
- const now = Date.now();
2072
- const elapsed = now - lastUpdated.current;
2073
- if (elapsed >= interval) {
2074
- lastUpdated.current = now;
2075
- setThrottledValue(value);
2076
- }
2077
- else {
2078
- const timer = setTimeout(() => {
2079
- lastUpdated.current = Date.now();
2080
- setThrottledValue(value);
2081
- }, interval - elapsed);
2082
- return () => clearTimeout(timer);
2083
- }
2084
- }, [value, interval]);
2085
- return throttledValue;
2086
- };
2087
-
2088
- var hooks = /*#__PURE__*/Object.freeze({
2089
- __proto__: null,
2090
- Children: Children,
2091
- Link: Link,
2092
- NavLink: NavLink,
2093
- RouterProvider: RouterProvider,
2094
- createContext: createContext,
2095
- resetIdCounter: resetIdCounter,
2096
- useCallback: useCallback,
2097
- useDebounce: useDebounce,
2098
- useDeferredValue: useDeferredValue,
2099
- useEffect: useEffect,
2100
- useHash: useHash,
2101
- useId: useId,
2102
- useLayoutEffect: useLayoutEffect,
2103
- useMemo: useMemo,
2104
- useMetadata: useMetadata,
2105
- usePathname: usePathname,
2106
- usePersistentStore: usePersistentStore,
2107
- usePersitentStore: usePersistentStore,
2108
- useQuery: useQuery,
2109
- useReducer: useReducer,
2110
- useRef: useRef,
2111
- useRouter: useRouter,
2112
- useSearchParams: useSearchParams,
2113
- useStore: useStore,
2114
- useStorePriority: useStorePriority,
2115
- useSwitch: useSwitch,
2116
- useThrottle: useThrottle,
2117
- useTransition: useTransition
2118
- });
2119
-
2120
- const getHydrationPolicy = () => {
2121
- const env = typeof process !== 'undefined' && process.env ? process.env : {};
2122
- const recoverRaw = env.RYUNIX_HYDRATION_RECOVER || 'boundary';
2123
- const boundariesRaw = env.RYUNIX_HYDRATION_BOUNDARIES || 'route';
2124
- const strict = env.RYUNIX_HYDRATION_STRICT === 'true';
2125
- const recover = recoverRaw === 'none' || recoverRaw === 'root' ? recoverRaw : 'boundary';
2126
- const boundaries = boundariesRaw === 'server-only' || boundariesRaw === 'all-layouts'
2127
- ? boundariesRaw
2128
- : 'route';
2129
- return {
2130
- recover,
2131
- boundaries,
2132
- strict,
2133
- };
2134
- };
2135
- /**
2136
- */
2137
- const findNearestHydrationBoundary = (fiber) => {
2138
- let current = fiber || null;
2139
- while (current) {
2140
- const props = current.props;
2141
- if (props &&
2142
- Object.prototype.hasOwnProperty.call(props, 'data-ryunix-hydrate-boundary')) {
2143
- return current;
2144
- }
2145
- const type = current.type;
2146
- const maybeTyped = type;
2147
- if (type &&
2148
- typeof type === 'function' &&
2149
- (maybeTyped?.ryunix_type === 'RYUNIX_HYDRATION_BOUNDARY' ||
2150
- maybeTyped?.ryunix_type === 'RYUNIX_SERVER_BOUNDARY')) {
2151
- return current;
2152
- }
2153
- current = current.parent || null;
1622
+ const useMemo = (compute, deps) => {
1623
+ if (typeof window === 'undefined') {
1624
+ return compute();
2154
1625
  }
2155
- return null;
2156
- };
2157
- /**
2158
- */
2159
- const getBoundaryDom = (fiber) => {
2160
- if (!fiber)
2161
- return null;
2162
- if (fiber.dom && fiber.dom.nodeType === 1) {
2163
- const el = fiber.dom;
2164
- if (el.hasAttribute('data-ryunix-hydrate-boundary'))
2165
- return el;
1626
+ const state = getState();
1627
+ if (state.isServerRendering) {
1628
+ return compute();
2166
1629
  }
2167
- let child = fiber.child || null;
2168
- while (child) {
2169
- if (child.dom && child.dom.nodeType === 1) {
2170
- const el = child.dom;
2171
- if (el.hasAttribute('data-ryunix-hydrate-boundary'))
2172
- return el;
2173
- }
2174
- child = child.child || child.sibling || null;
1630
+ validateHookContext();
1631
+ if (!is.function(compute)) {
1632
+ throw new Error('useMemo callback must be a function');
2175
1633
  }
2176
- return null;
2177
- };
2178
- /**
2179
- */
2180
- const skipHydrationSubtree = (cursor, boundaryRoot) => {
2181
- if (!cursor || !boundaryRoot)
2182
- return cursor;
2183
- if (cursor === boundaryRoot)
2184
- return boundaryRoot.nextSibling;
2185
- if (boundaryRoot.contains(cursor))
2186
- return boundaryRoot.nextSibling;
2187
- return cursor;
2188
- };
2189
- /**
2190
- */
2191
- const enqueueScopedRecovery = (boundaryFiber, boundaryDom, resumeCursor) => {
2192
- if (!boundaryFiber || !boundaryDom)
2193
- return;
2194
- const state = getState();
2195
- const queue = state.scopedRecoveryQueue || [];
2196
- queue.push({
2197
- boundaryFiber,
2198
- boundaryDom,
2199
- resumeCursor,
2200
- element: (Array.isArray(boundaryFiber.props?.children)
2201
- ? boundaryFiber.props.children[0]
2202
- : boundaryFiber.props?.children),
2203
- });
2204
- state.scopedRecoveryQueue = queue;
2205
- };
2206
-
2207
- /**
2208
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
2209
- * @typedef {import('../types/internal.js').RyunixComponent} RyunixComponent
2210
- * @typedef {import('../types/internal.js').RyunixNode} RyunixNode
2211
- */
2212
- /**
2213
- * @param {RyunixFiber} fiber
2214
- */
2215
- const updateFunctionComponent = (fiber) => {
2216
- const state = getState();
2217
- state.wipFiber = fiber;
2218
- state.hookIndex = 0;
2219
- /** @type {RyunixFiber} */ state.wipFiber.hooks = [];
2220
- if (state.isHydrating) {
2221
- fiber.effectTag = EFFECT_TAGS.HYDRATE;
1634
+ if (!Array.isArray(deps)) {
1635
+ throw new Error('useMemo requires a dependencies array');
2222
1636
  }
2223
- // Memo bailout: skip re-render if props haven't changed
2224
- const componentType =
2225
- /** @type {RyunixComponent & { _arePropsEqual?: (prev: Record<string, unknown>, next: Record<string, unknown>) => boolean }} */ fiber.type;
2226
- if (componentType._isMemo && fiber.alternate) {
2227
- const { children: _pc, ...prevRest } = fiber.alternate.props || {};
2228
- const { children: _nc, ...nextRest } = fiber.props || {};
2229
- if (componentType._arePropsEqual?.(prevRest, nextRest)) {
2230
- fiber.hooks = fiber.alternate.hooks;
2231
- const oldChild = fiber.alternate.child;
2232
- if (oldChild) {
2233
- oldChild.parent = fiber;
2234
- fiber.child = oldChild;
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);
2235
1651
  }
2236
- return;
1652
+ throw error;
2237
1653
  }
2238
1654
  }
2239
- let children = [
2240
- /** @type {RyunixNode} */ /** @type {(props?: Record<string, unknown>) => unknown} */ componentType(fiber.props),
2241
- ];
2242
- if (componentType._contextId && fiber.props?.value !== undefined) {
2243
- fiber._contextId = componentType._contextId;
2244
- fiber._contextValue = fiber.props.value;
2245
- }
2246
- reconcileChildren(fiber, children);
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;
2247
1665
  };
2248
- /**
2249
- * @param {RyunixFiber | null | undefined} fiber
2250
- * @returns {boolean}
2251
- */
2252
- const isUnderClientOnlyBoundary = (fiber) => {
2253
- let current = fiber?.parent || null;
2254
- while (current) {
2255
- if (current._hydrateClientOnly)
2256
- return true;
2257
- current = current.parent || null;
1666
+ const useCallback = (callback, deps) => {
1667
+ if (!is.function(callback)) {
1668
+ throw new Error('useCallback requires a function as first argument');
2258
1669
  }
2259
- return false;
1670
+ return useMemo(() => callback, deps);
2260
1671
  };
2261
- /**
2262
- * @param {RyunixFiber} fiber
2263
- */
2264
- const updateHostComponent = (fiber) => {
2265
- const state = getState();
2266
- if (fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2267
- fiber._contextId =
2268
- /** @type {string | symbol | undefined} */ fiber.props?._contextId;
2269
- fiber._contextValue = fiber.props?.value;
2270
- }
2271
- const isPassthrough = fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT ||
2272
- fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT ||
2273
- fiber.type === Symbol.for('ryunix.portal');
2274
- if (state.isHydrating && isPassthrough) {
2275
- fiber.effectTag = EFFECT_TAGS.HYDRATE;
2276
- }
2277
- else if (state.isHydrating && isUnderClientOnlyBoundary(fiber)) {
2278
- if (!fiber.dom) {
2279
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
2280
- fiber.effectTag = EFFECT_TAGS.PLACEMENT;
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;
2281
1684
  }
2282
- }
2283
- else if (!fiber.dom) {
2284
- if (state.isHydrating && state.hydrateCursor) {
2285
- const domNode = state.hydrateCursor;
2286
- const isText = fiber.type === RYUNIX_TYPES.TEXT_ELEMENT && domNode.nodeType === 3;
2287
- const isElement = typeof fiber.type === 'string' &&
2288
- domNode.nodeType === 1 &&
2289
- domNode.tagName.toLowerCase() === fiber.type.toLowerCase();
2290
- if (isText || isElement) {
2291
- fiber.dom = /** @type {HTMLElement | Text} */ domNode;
2292
- fiber.effectTag = EFFECT_TAGS.HYDRATE;
2293
- if (isText &&
2294
- fiber.props?.nodeValue != null &&
2295
- domNode.nodeValue !== String(fiber.props.nodeValue)) {
2296
- domNode.nodeValue = String(fiber.props.nodeValue);
2297
- logHydrationRecoverable('text');
2298
- }
2299
- if (isElement &&
2300
- domNode.hasAttribute('data-ryunix-hydrate-boundary')) {
2301
- fiber._hydrateClientOnly = true;
2302
- }
2303
- state.hydrateCursor = nextValidSibling$1(domNode.firstChild);
1685
+ validateHookContext();
1686
+ let fiber = state.wipFiber;
1687
+ while (fiber) {
1688
+ if (fiber._contextId === ctxID && fiber._contextValue !== undefined) {
1689
+ return fiber._contextValue;
2304
1690
  }
2305
- else {
2306
- const policy = getHydrationPolicy();
2307
- const detail = `Mismatch at ${getTypeLabel(fiber.type)}. Expected ${domNode.nodeType === 1 ? domNode.tagName : 'text'} but got ${String(fiber.type)}.`;
2308
- const boundaryFiber = findNearestHydrationBoundary(fiber);
2309
- const boundaryDom = boundaryFiber ? getBoundaryDom(boundaryFiber) : null;
2310
- if (policy.recover === 'boundary' && boundaryFiber && boundaryDom) {
2311
- logHydrationBoundaryMismatch(detail);
2312
- enqueueScopedRecovery(boundaryFiber, boundaryDom, state.hydrateCursor ?? null);
2313
- state.hydrateCursor = skipHydrationSubtree(state.hydrateCursor ?? null, boundaryDom);
2314
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
2315
- fiber.effectTag = EFFECT_TAGS.PLACEMENT;
2316
- }
2317
- else if (policy.recover === 'none') {
2318
- logHydrationFatal(detail);
2319
- state.isHydrating = false;
2320
- state.hydrateCursor = null;
2321
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
2322
- fiber.effectTag = EFFECT_TAGS.PLACEMENT;
2323
- }
2324
- else {
2325
- logHydrationMismatch(detail);
2326
- state.isHydrating = false;
2327
- state.hydrationFailed = true;
2328
- state.hydrateCursor = null;
2329
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
2330
- fiber.effectTag = EFFECT_TAGS.PLACEMENT;
2331
- }
1691
+ const fiberType = fiber.type;
1692
+ if (fiberType?._contextId === ctxID && fiber.props?.value !== undefined) {
1693
+ return fiber.props.value;
2332
1694
  }
1695
+ fiber = fiber.parent;
2333
1696
  }
2334
- else {
2335
- fiber.dom = /** @type {HTMLElement | Text | null} */ createDom(fiber);
2336
- }
2337
- }
2338
- const children = fiber.props?.children || [];
2339
- reconcileChildren(fiber, children);
2340
- };
2341
- /**
2342
- * @param {string | symbol | RyunixComponent | object} type
2343
- * @returns {string}
2344
- */
2345
- const getTypeLabel = (type) => {
2346
- if (typeof type === 'symbol')
2347
- return type.description || type.toString();
2348
- if (typeof type === 'function')
2349
- return type.name || 'anonymous';
2350
- return String(type);
2351
- };
2352
- /**
2353
- * The Component `Image` takes in a `src` and other props, and returns an `img` element with the
2354
- * specified `src` and props.
2355
- * @returns The `Image` component is being returned. It is a functional component that renders an `img`
2356
- * element with the specified `src` and other props passed to it.
2357
- */
2358
- /**
2359
- * @param {{ src: string } & Record<string, unknown>} props
2360
- * @returns {import('./createElement.js').RyunixElement}
2361
- */
2362
- const Image = ({ src, ...props }) => {
2363
- return createElement('img', { ...props, src });
2364
- };
2365
- const { Provider: MDXProvider, useContext: useMDXComponents } = createContext('ryunix.mdx',
2366
- /** @type {Record<string, RyunixComponent>} */ {});
2367
- /**
2368
- * Get merged MDX components from context and provided components
2369
- * @param {Record<string, RyunixComponent>} [components] - Additional components to merge
2370
- * @returns {Record<string, RyunixComponent>} Merged components object
2371
- */
2372
- const getMDXComponents = (components) => {
2373
- const contextComponents = useMDXComponents();
1697
+ return defaultValue;
1698
+ };
2374
1699
  return {
2375
- ...contextComponents,
2376
- ...components,
1700
+ Provider: Provider,
1701
+ useContext,
2377
1702
  };
2378
1703
  };
2379
- /**
2380
- * @param {string} tag
2381
- * @param {Record<string, unknown>} props
2382
- * @returns {RyunixNode}
2383
- */
2384
- const mdxHost = (tag, props) =>
2385
- /** @type {RyunixNode} */ createElement(tag, props);
2386
- /**
2387
- * Default MDX components with Ryunix-optimized rendering
2388
- * @type {Record<string, (props: Record<string, unknown>) => RyunixNode>}
2389
- */
2390
- const defaultComponents = {
2391
- // Headings
2392
- h1: (props) => mdxHost('h1', props),
2393
- h2: (props) => mdxHost('h2', props),
2394
- h3: (props) => mdxHost('h3', props),
2395
- h4: (props) => mdxHost('h4', props),
2396
- h5: (props) => mdxHost('h5', props),
2397
- h6: (props) => mdxHost('h6', props),
2398
- // Text
2399
- p: (props) => mdxHost('p', props),
2400
- a: (props) => mdxHost('a', props),
2401
- strong: (props) => mdxHost('strong', props),
2402
- em: (props) => mdxHost('em', props),
2403
- code: (props) => mdxHost('code', props),
2404
- // Lists
2405
- ul: (props) => mdxHost('ul', props),
2406
- ol: (props) => mdxHost('ol', props),
2407
- li: (props) => mdxHost('li', props),
2408
- // Blocks
2409
- blockquote: (props) => mdxHost('blockquote', props),
2410
- pre: (props) => mdxHost('pre', props),
2411
- hr: (props) => mdxHost('hr', props),
2412
- // Tables
2413
- table: (props) => mdxHost('table', props),
2414
- thead: (props) => mdxHost('thead', props),
2415
- tbody: (props) => mdxHost('tbody', props),
2416
- tr: (props) => mdxHost('tr', props),
2417
- th: (props) => mdxHost('th', props),
2418
- td: (props) => mdxHost('td', props),
2419
- // Media
2420
- img: (props) => mdxHost('img', props),
2421
- };
2422
- /**
2423
- * MDX Wrapper component
2424
- * Provides default styling and components for MDX content
2425
- */
2426
- /**
2427
- * @param {{ children?: RyunixNode, components?: Record<string, RyunixComponent> }} props
2428
- * @returns {import('./createElement.js').RyunixElement}
2429
- */
2430
- const MDXContent = ({ children, components = {} }) => {
2431
- const mergedComponents = getMDXComponents(components);
2432
- return createElement(
2433
- /** @type {string | symbol | Function} */ MDXProvider, { value: mergedComponents }, createElement('div', null, children));
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;
2434
1713
  };
2435
-
2436
- /**
2437
- * @param {RyunixNode} element
2438
- * @param {Element | DocumentFragment} container
2439
- */
2440
- const renderSubtree = (element, container) => {
2441
- clearContainer(/** @type {HTMLElement} */ container);
2442
- /** @type {RyunixRootFiber} */
2443
- const root = {
2444
- dom: container,
2445
- props: { children: [element] },
2446
- isHydrating: false,
2447
- hydrateCursor: null,
2448
- };
2449
- 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;
2450
1724
  };
2451
- const recoverScopedHydrationFailures = () => {
1725
+ const useMetadata = (tags = {}, options = {}) => {
2452
1726
  const state = getState();
2453
- const queue = state.scopedRecoveryQueue;
2454
- if (!queue?.length)
1727
+ if (state.isServerRendering) {
1728
+ state.ssrMetadata = { ...state.ssrMetadata, ...tags };
2455
1729
  return;
2456
- state.scopedRecoveryQueue = [];
2457
- for (const item of queue) {
2458
- logHydrationBoundaryRecovery();
2459
- renderSubtree(item.element, item.boundaryDom);
2460
1730
  }
2461
- };
2462
- const recoverHydrationFailureIfNeeded = () => {
2463
- const state = getState();
2464
- if (!state.hydrationFailed || state.hydrationRecover)
2465
- return;
2466
- const policy = getHydrationPolicy();
2467
- if (policy.recover === 'none')
2468
- return;
2469
- const container = state.containerRoot || state.currentRoot?.dom;
2470
- const element = state.currentRoot?.props?.children?.[0];
2471
- if (!container || element == null)
2472
- return;
2473
- state.hydrationRecover = true;
2474
- state.hydrationFailed = false;
2475
- logHydrationFailure('');
2476
- logHydrationRecovery();
2477
- renderSubtree(/** @type {RyunixNode} */ element, container);
2478
- };
2479
- const runHydrationRecovery = () => {
2480
- recoverScopedHydrationFailures();
2481
- recoverHydrationFailureIfNeeded();
2482
- };
2483
-
2484
- /**
2485
- * @typedef {import('../types/internal.js').RyunixFiber} RyunixFiber
2486
- * @typedef {import('../types/internal.js').RyunixRootFiber} RyunixRootFiber
2487
- */
2488
- /** @type {RyunixRootFiber[]} */
2489
- let workQueue = [];
2490
- /** @type {boolean} */
2491
- let isWorkLoopScheduled = false;
2492
- /**
2493
- * @param {RyunixFiber} fiber
2494
- * @returns {RyunixFiber | null}
2495
- */
2496
- function performUnitOfWork(fiber) {
2497
- const state = getState();
2498
- const isFunctionComponent = fiber.type instanceof Function || typeof fiber.type === 'function';
2499
- try {
2500
- if (isFunctionComponent) {
2501
- updateFunctionComponent(fiber);
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;
2502
1742
  }
2503
1743
  else {
2504
- updateHostComponent(fiber);
2505
- }
2506
- }
2507
- catch (error) {
2508
- if (process.env.NODE_ENV !== 'production') {
2509
- console.error('[Ryunix ErrorBoundary] Caught error during render:', error);
2510
- try {
2511
- // Attempt to attach original JSX source map for DevOverlay lookup
2512
- const src = fiber.props && fiber.props.__source;
2513
- if (src && error && typeof error === 'object') {
2514
- error.__ryunix_source = src;
2515
- }
2516
- let targetFiber = fiber;
2517
- while (!error.__ryunix_source && targetFiber) {
2518
- if (targetFiber.props && targetFiber.props.__source) {
2519
- error.__ryunix_source = targetFiber.props.__source;
2520
- }
2521
- targetFiber = targetFiber.parent;
2522
- }
2523
- }
2524
- catch (e) { }
1744
+ finalTitle = defaultTitle;
2525
1745
  }
2526
- // Traverse upwards to find nearest ErrorBoundary
2527
- let boundaryFiber = fiber.parent;
2528
- let foundBoundary = false;
2529
- while (boundaryFiber) {
2530
- if (boundaryFiber.type &&
2531
- /** @type {{ ryunix_type?: string }} */ boundaryFiber.type
2532
- .ryunix_type === 'RYUNIX_ERROR_BOUNDARY') {
2533
- foundBoundary = true;
2534
- break;
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);
2535
1753
  }
2536
- boundaryFiber = boundaryFiber.parent;
1754
+ link.setAttribute('href', tags.canonical);
2537
1755
  }
2538
- if (foundBoundary) {
2539
- if (process.env.NODE_ENV !== 'production') {
2540
- 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);
2541
1766
  }
2542
- // Assign the error state to the boundary so it can render the fallback
2543
- boundaryFiber.stateError = error;
2544
- // Discard the corrupted children of the crashing fiber to prevent undefined behavior
2545
- fiber.child = null;
2546
- // Rewind the rendering context to the ErrorBoundary fiber
2547
- // so the work loop immediately starts re-evaluating the boundary branch
2548
- return boundaryFiber;
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;
2549
1789
  }
2550
- else {
2551
- // Uncaught fatal error: stop the work loop entirely
2552
- console.error('[Ryunix] Fatal Uncaught Error. No ErrorBoundary was found in the tree to handle this exception:\n', error);
2553
- state.nextUnitOfWork = null;
2554
- return null;
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 };
2555
1807
  }
2556
1808
  }
2557
- if (fiber.child) {
2558
- 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];
2559
1815
  }
2560
- let nextFiber = fiber;
2561
- while (nextFiber) {
2562
- // If we just finished a Host node during hydration,
2563
- // the next fiber (sibling) should start at the next DOM sibling.
2564
- if (state.isHydrating && nextFiber.dom) {
2565
- state.hydrateCursor = nextValidSibling$1(nextFiber.dom.nextSibling);
2566
- }
2567
- if (nextFiber.sibling) {
2568
- return nextFiber.sibling;
2569
- }
2570
- nextFiber = nextFiber.parent;
2571
- // When ascending, we don't need to do anything else,
2572
- // the loop will handle the parent's sibling or end.
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 }));
2573
1830
  }
2574
- }
2575
- /**
2576
- * @param {{ timeRemaining: () => number, didTimeout?: boolean }} deadline
2577
- */
2578
- const workLoop = (deadline) => {
2579
- const state = getState();
2580
- let shouldYield = false;
2581
- while ((state.nextUnitOfWork || workQueue.length > 0) && !shouldYield) {
2582
- if (!state.nextUnitOfWork && workQueue.length > 0) {
2583
- const nextRoot = workQueue.shift();
2584
- state.wipRoot = nextRoot;
2585
- state.nextUnitOfWork = nextRoot;
2586
- state.deletions = [];
2587
- // Restore specific hydration state for this root
2588
- if (nextRoot.isHydrating !== undefined) {
2589
- state.isHydrating = nextRoot.isHydrating;
2590
- state.hydrateCursor = nextRoot.hydrateCursor;
2591
- }
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;
2592
1845
  }
2593
- if (state.nextUnitOfWork) {
2594
- 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' });
2595
1874
  }
2596
- shouldYield = deadline.timeRemaining() < 1;
2597
- }
2598
- if (!state.nextUnitOfWork && state.wipRoot) {
2599
- commitRoot();
2600
- runHydrationRecovery();
2601
- }
2602
- if (state.nextUnitOfWork || workQueue.length > 0) {
2603
- rIC(workLoop);
2604
- }
2605
- else {
2606
- isWorkLoopScheduled = false;
2607
- }
1875
+ }, [hash]);
1876
+ return createElement(route.component, {
1877
+ key: location,
1878
+ params,
1879
+ query,
1880
+ hash,
1881
+ location,
1882
+ });
2608
1883
  };
2609
- /**
2610
- * @param {RyunixRootFiber} root
2611
- * @param {number} [priority]
2612
- */
2613
- const scheduleWork = (root, priority = getCurrentPriority()) => {
2614
- const state = getState();
2615
- if (state.wipRoot) {
2616
- workQueue.push(root);
2617
- }
2618
- else {
2619
- state.nextUnitOfWork = root;
2620
- state.wipRoot = root;
2621
- state.deletions = [];
2622
- // Set immediate hydration state
2623
- if (root.isHydrating !== undefined) {
2624
- state.isHydrating = root.isHydrating;
2625
- state.hydrateCursor = root.hydrateCursor;
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;
2626
1897
  }
2627
- }
2628
- state.hookIndex = 0;
2629
- state.effects = [];
2630
- if (!isWorkLoopScheduled) {
2631
- isWorkLoopScheduled = true;
2632
- if (priority <= Priority.USER_BLOCKING) {
2633
- // High priority: run as soon as possible in a micro-task
2634
- // We provide a synthetic deadline that allows some work before yielding
2635
- Promise.resolve().then(() => {
2636
- workLoop({ timeRemaining: () => 10, didTimeout: true });
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);
2637
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;
2638
1975
  }
2639
- else {
2640
- // Low priority: wait for browser idleness
2641
- 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);
2642
1987
  }
2643
- }
2644
- };
2645
- setScheduleWork(scheduleWork);
2646
-
2647
- /**
2648
- * @typedef {import('./createElement.js').RyunixNode} RyunixNode
2649
- * @typedef {import('../types/internal.js').RyunixRootFiber} RyunixRootFiber
2650
- * @typedef {import('../types/internal.js').RyunixComponent} RyunixComponent
2651
- */
2652
- /**
2653
- * The `render` function in JavaScript updates the DOM with a new element and schedules work to be done
2654
- * on the element.
2655
- * @param {RyunixNode} element
2656
- * @param {Element | DocumentFragment} container
2657
- * @returns {RyunixRootFiber}
2658
- */
2659
- const render = (element, container) => {
2660
- const state = getState();
2661
- // Clear container before CSR render to avoid duplication
2662
- clearContainer(/** @type {HTMLElement} */ container);
2663
- /** @type {RyunixRootFiber} */
2664
- const root = {
2665
- dom: container,
2666
- props: {
2667
- children: [
2668
- /** @type {import('../types/internal.js').RyunixNode} */ element,
2669
- ],
2670
- },
2671
- alternate: state.currentRoot,
2672
- isHydrating: false,
2673
- hydrateCursor: /** @type {ChildNode | null} */ null,
2674
1988
  };
2675
- scheduleWork(root);
2676
- return root;
2677
- };
2678
- /**
2679
- * @param {ChildNode | null} node
2680
- * @returns {ChildNode | null}
2681
- */
2682
- const nextValidSibling = (node) => {
2683
- let next = node;
2684
- while (next &&
2685
- ((next.nodeType === 3 && !next.nodeValue.trim()) ||
2686
- next.nodeType === 8 ||
2687
- (next.nodeType === 1 &&
2688
- /** @type {Element} */ next.hasAttribute('data-ryunix-ssr')))) {
2689
- next = next.nextSibling;
2690
- }
2691
- return next;
1989
+ return [state, setValue];
2692
1990
  };
2693
- /**
2694
- * The `hydrate` function attaches Ryunix to an existing server-rendered DOM tree.
2695
- * Instead of clearing and re-rendering, it walks the existing DOM nodes and
2696
- * attaches event listeners and reconciles state, preserving SSR HTML.
2697
- * @param {RyunixNode} element
2698
- * @param {Element | DocumentFragment} container
2699
- * @returns {RyunixRootFiber}
2700
- */
2701
- const hydrate = (element, container) => {
2702
- const state = getState();
2703
- state.containerRoot = container;
2704
- /** @type {RyunixRootFiber} */
2705
- const root = {
2706
- dom: container,
2707
- props: {
2708
- children: [
2709
- /** @type {import('../types/internal.js').RyunixNode} */ element,
2710
- ],
2711
- },
2712
- alternate: state.currentRoot,
2713
- isHydrating: true,
2714
- hydrateCursor: nextValidSibling(container.firstChild),
1991
+ const useSwitch = (initialState = false) => {
1992
+ const [state, dispatch] = useStore(initialState);
1993
+ const toggle = () => {
1994
+ dispatch((prev) => !prev);
2715
1995
  };
2716
- scheduleWork(root);
2717
- return root;
1996
+ return [state, toggle];
2718
1997
  };
2719
- /**
2720
- * @param {RyunixNode} MainElement
2721
- * @param {string} [root]
2722
- * @param {Record<string, unknown>} [components]
2723
- * @returns {RyunixRootFiber | undefined}
2724
- */
2725
- const init = (MainElement, root = '__ryunix', components = {}) => {
1998
+ const useLayoutEffect = (callback, deps) => {
1999
+ if (typeof window === 'undefined') {
2000
+ return;
2001
+ }
2726
2002
  const state = getState();
2727
- state.containerRoot = document.getElementById(root);
2728
- resetHydrationLogFlags();
2729
- state.hydrationPolicy = getHydrationPolicy();
2730
- state.scopedRecoveryQueue = [];
2731
- state.hydrationRecover = false;
2732
- // Reset any stale hydration flags
2733
- state.isHydrating = false;
2734
- state.hydrationFailed = false;
2735
- // Auto-detect SSR based on child nodes - no need to manually set process.env.RYUNIX_SSR
2736
- const hasChildNodes = state.containerRoot && state.containerRoot.hasChildNodes();
2737
- if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
2738
- console.log(`[Ryunix Debug] init: hasChildNodes=${hasChildNodes}, has SSR content detected.`);
2003
+ if (state.isServerRendering) {
2004
+ return;
2739
2005
  }
2740
- // Auto-detect: if there's existing content, try to hydrate (SSR)
2741
- // If explicitly disabled via RYUNIX_SSR=false, skip hydration
2742
- const ssrEnabled = process.env.RYUNIX_SSR !== 'false';
2743
- if (hasChildNodes && ssrEnabled) {
2744
- if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
2745
- console.log(`[Ryunix Debug] init: SSR content detected. Starting hydration on #${root}`);
2746
- }
2747
- const res = hydrate(MainElement, state.containerRoot);
2748
- return res;
2006
+ validateHookContext();
2007
+ if (!is.function(callback)) {
2008
+ throw new Error('useLayoutEffect callback must be a function');
2749
2009
  }
2750
- if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
2751
- console.log(`[Ryunix Debug] init: No SSR content or SSR disabled. Starting normal render on #${root}`);
2010
+ if (deps !== undefined && !Array.isArray(deps)) {
2011
+ throw new Error('useLayoutEffect dependencies must be an array or undefined');
2752
2012
  }
2753
- const res = render(MainElement, state.containerRoot);
2754
- 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++;
2755
2028
  };
2756
- /**
2757
- * @param {RyunixComponent} component
2758
- * @param {Record<string, unknown>} props
2759
- * @param {(error: unknown) => void} [onError]
2760
- * @returns {RyunixNode}
2761
- */
2762
- const safeRender = (component, props, onError) => {
2763
- try {
2764
- return /** @type {RyunixNode} */ /** @type {(props: Record<string, unknown>) => RyunixNode} */ component(props);
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++}:`;
2765
2037
  }
2766
- catch (error) {
2767
- if (process.env.NODE_ENV !== 'production') {
2768
- 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);
2769
2073
  }
2770
- if (onError)
2771
- onError(error);
2772
- return null;
2773
- }
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;
2774
2083
  };
2775
2084
 
2776
- /**
2777
- * @typedef {import('./createElement.js').RyunixNode} RyunixNode
2778
- * @typedef {import('./createElement.js').RyunixElement} RyunixElement
2779
- * @typedef {import('../types/internal.js').RyunixRenderToStringOptions} RyunixRenderToStringOptions
2780
- * @typedef {Promise<{ success: boolean, id: string, content: string, error?: unknown }>} RyunixSuspenseTask
2781
- */
2782
- /**
2783
- * @param {unknown} unsafe
2784
- * @returns {string}
2785
- */
2786
2085
  const escapeHtml = (unsafe) => {
2787
2086
  if (typeof unsafe !== 'string')
2788
2087
  return String(unsafe);
@@ -2793,10 +2092,6 @@ const escapeHtml = (unsafe) => {
2793
2092
  .replace(/"/g, '&quot;')
2794
2093
  .replace(/'/g, '&#039;');
2795
2094
  };
2796
- /**
2797
- * @param {Record<string, unknown>} styleObj
2798
- * @returns {string}
2799
- */
2800
2095
  const renderStyle = (styleObj) => {
2801
2096
  if (!is.object(styleObj) || is.null(styleObj))
2802
2097
  return '';
@@ -2821,10 +2116,6 @@ const VOID_ELEMENTS = new Set([
2821
2116
  'track',
2822
2117
  'wbr',
2823
2118
  ]);
2824
- /**
2825
- * @param {RyunixNode | RyunixNode[]} element
2826
- * @returns {string}
2827
- */
2828
2119
  const renderToStringImpl = (element) => {
2829
2120
  if (element == null || typeof element === 'boolean') {
2830
2121
  return '';
@@ -2835,11 +2126,9 @@ const renderToStringImpl = (element) => {
2835
2126
  if (Array.isArray(element)) {
2836
2127
  return element.map((child) => renderToStringImpl(child)).join('');
2837
2128
  }
2838
- /** @type {RyunixElement} */
2839
2129
  const vnode = element;
2840
2130
  if (vnode.type === RYUNIX_TYPES.TEXT_ELEMENT) {
2841
- return escapeHtml(
2842
- /** @type {import('./createElement.js').RyunixTextElement} */ vnode.props
2131
+ return escapeHtml(vnode.props
2843
2132
  .nodeValue);
2844
2133
  }
2845
2134
  if (vnode.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
@@ -2847,11 +2136,9 @@ const renderToStringImpl = (element) => {
2847
2136
  return children.map((child) => renderToStringImpl(child)).join('');
2848
2137
  }
2849
2138
  if (vnode.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2850
- // Context Providers just render their children transparently on the server
2851
2139
  const state = getState();
2852
2140
  state.ssrContexts = state.ssrContexts || {};
2853
- const ctxProps =
2854
- /** @type {{ _contextId?: string, value?: unknown, children?: RyunixNode | RyunixNode[] }} */ vnode.props ||
2141
+ const ctxProps = vnode.props ||
2855
2142
  {};
2856
2143
  const ctxId = ctxProps._contextId;
2857
2144
  const prevCtx = state.ssrContexts[ctxId];
@@ -2874,11 +2161,9 @@ const renderToStringImpl = (element) => {
2874
2161
  if (typeof vnode.type === 'function') {
2875
2162
  const type = vnode.type;
2876
2163
  const props = vnode.props || {};
2877
- const renderedElement =
2878
- /** @type {(props: Record<string, unknown>) => RyunixNode} */ type(props);
2164
+ const renderedElement = type(props);
2879
2165
  return renderToStringImpl(renderedElement);
2880
2166
  }
2881
- // It's a standard host element
2882
2167
  const type = String(vnode.type);
2883
2168
  const props = vnode.props || {};
2884
2169
  let attributes = '';
@@ -2890,7 +2175,7 @@ const renderToStringImpl = (element) => {
2890
2175
  htmlChildren = value.map((child) => renderToStringImpl(child)).join('');
2891
2176
  }
2892
2177
  else {
2893
- htmlChildren = renderToStringImpl(/** @type {RyunixNode} */ value);
2178
+ htmlChildren = renderToStringImpl(value);
2894
2179
  }
2895
2180
  }
2896
2181
  else if (key === 'dangerouslySetInnerHTML') {
@@ -2900,8 +2185,7 @@ const renderToStringImpl = (element) => {
2900
2185
  }
2901
2186
  }
2902
2187
  else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
2903
- const styleString = renderStyle(
2904
- /** @type {Record<string, unknown>} */ value);
2188
+ const styleString = renderStyle(value);
2905
2189
  if (styleString) {
2906
2190
  attributes += ` style="${escapeHtml(styleString)}"`;
2907
2191
  }
@@ -2912,6 +2196,8 @@ const renderToStringImpl = (element) => {
2912
2196
  }
2913
2197
  }
2914
2198
  else if (!key.startsWith('on') &&
2199
+ key !== 'key' &&
2200
+ key !== 'ref' &&
2915
2201
  key !== '__source' &&
2916
2202
  key !== '__self') {
2917
2203
  if (typeof value === 'boolean') {
@@ -2943,17 +2229,10 @@ function $RC(id, templateId) {
2943
2229
  `
2944
2230
  .replace(/\s+/g, ' ')
2945
2231
  .trim();
2946
- /**
2947
- * @param {RyunixNode | RyunixNode[]} element
2948
- * @param {(chunk: string) => void} push
2949
- * @param {RyunixSuspenseTask[]} [suspenseTasks]
2950
- * @returns {Promise<void>}
2951
- */
2952
2232
  const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2953
2233
  if (element == null || typeof element === 'boolean') {
2954
2234
  return;
2955
2235
  }
2956
- // Await the element if it's a promise (e.g. from an async Server Component directly rendered)
2957
2236
  if (element instanceof Promise) {
2958
2237
  element = await element;
2959
2238
  if (element == null || typeof element === 'boolean')
@@ -2969,11 +2248,9 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2969
2248
  }
2970
2249
  return;
2971
2250
  }
2972
- /** @type {RyunixElement} */
2973
2251
  const vnode = element;
2974
2252
  if (vnode.type === RYUNIX_TYPES.TEXT_ELEMENT) {
2975
- push(escapeHtml(
2976
- /** @type {import('./createElement.js').RyunixTextElement} */ vnode
2253
+ push(escapeHtml(vnode
2977
2254
  .props.nodeValue));
2978
2255
  return;
2979
2256
  }
@@ -2987,8 +2264,7 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2987
2264
  if (vnode.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2988
2265
  const state = getState();
2989
2266
  state.ssrContexts = state.ssrContexts || {};
2990
- const ctxProps =
2991
- /** @type {{ _contextId?: string, value?: unknown, children?: RyunixNode | RyunixNode[] }} */ vnode.props ||
2267
+ const ctxProps = vnode.props ||
2992
2268
  {};
2993
2269
  const ctxId = ctxProps._contextId;
2994
2270
  const prevCtx = state.ssrContexts[ctxId];
@@ -3009,29 +2285,23 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
3009
2285
  }
3010
2286
  return;
3011
2287
  }
3012
- // Handle Suspense specifically
3013
2288
  const suspenseType = vnode.type;
3014
2289
  const isSuspenseBoundary = vnode.type === RYUNIX_TYPES.RYUNIX_SUSPENSE ||
3015
2290
  (typeof suspenseType === 'object' &&
3016
2291
  suspenseType != null &&
3017
- /** @type {{ type?: symbol }} */ suspenseType.type ===
2292
+ suspenseType.type ===
3018
2293
  RYUNIX_TYPES.RYUNIX_SUSPENSE);
3019
2294
  if (isSuspenseBoundary) {
3020
- const suspenseProps =
3021
- /** @type {{ fallback?: RyunixNode, children?: RyunixNode | RyunixNode[] }} */ vnode.props ||
2295
+ const suspenseProps = vnode.props ||
3022
2296
  {};
3023
2297
  const { fallback, children } = suspenseProps;
3024
2298
  const id = `s-${Math.random().toString(36).slice(2, 9)}`;
3025
- // In universal mode, Suspense renders children if ready, or fallback if pending.
3026
- // BUT we want to force a background task for the REAL children if we hit a lazy component.
3027
2299
  push(`<!--$?--><template id="B:${id}"></template><div id="S:${id}">`);
3028
- // 1. Start rendering the actual content in the background
3029
2300
  const task = (async () => {
3030
2301
  const state = getState();
3031
2302
  const wasBackground = state.isSuspenseBackground;
3032
2303
  state.isSuspenseBackground = true;
3033
2304
  let content = '';
3034
- /** @param {string} chunk */
3035
2305
  const subPush = (chunk) => {
3036
2306
  content += chunk;
3037
2307
  };
@@ -3047,7 +2317,6 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
3047
2317
  }
3048
2318
  })();
3049
2319
  suspenseTasks.push(task);
3050
- // 2. Render fallback immediately for the main stream
3051
2320
  await renderToStreamImpl(fallback, push, suspenseTasks);
3052
2321
  push(`</div><!--$/-->`);
3053
2322
  return;
@@ -3058,11 +2327,10 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
3058
2327
  if (process.env.RYUNIX_DEBUG) {
3059
2328
  console.log('[SSR Debug] Rendering function:', type.name || 'anonymous');
3060
2329
  }
3061
- const renderedElement = await /** @type {(props: Record<string, unknown>) => RyunixNode | Promise<RyunixNode>} */ type(props);
2330
+ const renderedElement = await type(props);
3062
2331
  await renderToStreamImpl(renderedElement, push, suspenseTasks);
3063
2332
  return;
3064
2333
  }
3065
- // It's a standard host element
3066
2334
  const hostTag = String(type);
3067
2335
  let attributes = '';
3068
2336
  let innerHTML = null;
@@ -3076,8 +2344,7 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
3076
2344
  }
3077
2345
  }
3078
2346
  else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
3079
- const styleString = renderStyle(
3080
- /** @type {Record<string, unknown>} */ value);
2347
+ const styleString = renderStyle(value);
3081
2348
  if (styleString) {
3082
2349
  attributes += ` style="${escapeHtml(styleString)}"`;
3083
2350
  }
@@ -3088,6 +2355,8 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
3088
2355
  }
3089
2356
  }
3090
2357
  else if (!key.startsWith('on') &&
2358
+ key !== 'key' &&
2359
+ key !== 'ref' &&
3091
2360
  key !== '__source' &&
3092
2361
  key !== '__self') {
3093
2362
  if (typeof value === 'boolean') {
@@ -3119,34 +2388,21 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
3119
2388
  push(`</${hostTag}>`);
3120
2389
  }
3121
2390
  };
3122
- /**
3123
- * @param {RyunixNode} element
3124
- * @param {RyunixRenderToStringOptions} [options]
3125
- * @returns {ReadableStream<Uint8Array>}
3126
- */
3127
2391
  const renderToReadableStream = (element, options = {}) => {
3128
2392
  const state = getState();
3129
2393
  const encoder = new TextEncoder();
3130
- // Reset idCounter for deterministic useId values
3131
2394
  resetIdCounter();
3132
2395
  return new ReadableStream({
3133
2396
  async start(controller) {
3134
2397
  const wasServerRendering = state.isServerRendering;
3135
2398
  state.isServerRendering = true;
3136
2399
  state.ssrMetadata = {};
3137
- /** @param {string} text */
3138
2400
  const push = (text) => controller.enqueue(encoder.encode(text));
3139
- /** @type {RyunixSuspenseTask[]} */
3140
2401
  const suspenseTasks = [];
3141
2402
  try {
3142
- // 0. Inject RC helper script first
3143
2403
  const nonceAttr = options.nonce ? ` nonce="${options.nonce}"` : '';
3144
2404
  push(`<script${nonceAttr} data-ryunix-ssr>${RC_SCRIPT}</script>`);
3145
- // 1. Render initial tree (with fallbacks)
3146
2405
  await renderToStreamImpl(element, push, suspenseTasks);
3147
- // 2. Process suspense tasks as they complete
3148
- // For now, we wait for all, but in a real streaming scenario,
3149
- // we could push them as they resolve.
3150
2406
  while (suspenseTasks.length > 0) {
3151
2407
  const task = suspenseTasks.shift();
3152
2408
  const res = await task;
@@ -3166,17 +2422,11 @@ const renderToReadableStream = (element, options = {}) => {
3166
2422
  },
3167
2423
  });
3168
2424
  };
3169
- /**
3170
- * @param {RyunixNode} element
3171
- * @param {RyunixRenderToStringOptions} [options]
3172
- * @returns {string}
3173
- */
3174
2425
  const renderToString = (element, options = {}) => {
3175
2426
  const state = getState();
3176
2427
  const wasServerRendering = state.isServerRendering;
3177
2428
  state.isServerRendering = true;
3178
2429
  state.ssrMetadata = {};
3179
- // Reset idCounter for deterministic useId values
3180
2430
  resetIdCounter();
3181
2431
  try {
3182
2432
  return renderToStringImpl(element);
@@ -3185,11 +2435,6 @@ const renderToString = (element, options = {}) => {
3185
2435
  state.isServerRendering = wasServerRendering;
3186
2436
  }
3187
2437
  };
3188
- /**
3189
- * @param {RyunixNode} element
3190
- * @param {RyunixRenderToStringOptions} [options]
3191
- * @returns {Promise<string>}
3192
- */
3193
2438
  const renderToStringAsync = async (element, options = {}) => {
3194
2439
  const stream = renderToReadableStream(element, options);
3195
2440
  const reader = stream.getReader();
@@ -3236,6 +2481,21 @@ function deepEqual(a, b) {
3236
2481
  return keysA.every((key) => deepEqual(a[key], b[key]));
3237
2482
  }
3238
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
+
3239
2499
  const SUSPENSE_STATUS = {
3240
2500
  PENDING: 'pending',
3241
2501
  RESOLVED: 'resolved',
@@ -3328,6 +2588,45 @@ function preload(importFn) {
3328
2588
  return importFn();
3329
2589
  }
3330
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
+
3331
2630
  const perfNow = () => typeof performance !== 'undefined' ? performance.now() : Date.now();
3332
2631
  class Profiler {
3333
2632
  enabled;
@@ -3449,43 +2748,10 @@ function withProfiler(Component, name) {
3449
2748
  return Profiled;
3450
2749
  }
3451
2750
 
3452
- function forwardRef(render) {
3453
- if (typeof render !== 'function') {
3454
- throw new Error('forwardRef requires a render function');
3455
- }
3456
- const ForwardRefComponent = ((props) => {
3457
- const { ref, ...restProps } = props || {};
3458
- return render(restProps, ref ?? null);
3459
- });
3460
- const named = render;
3461
- ForwardRefComponent.displayName = `ForwardRef(${named.displayName || named.name || 'Component'})`;
3462
- ForwardRefComponent._isForwardRef = true;
3463
- ForwardRefComponent._render = render;
3464
- return ForwardRefComponent;
3465
- }
3466
-
3467
- /**
3468
- * Wraps content rendered exclusively on the server. During hydration Ryunix
3469
- * preserves the server HTML inside this boundary.
3470
- *
3471
- * @param {object} props
3472
- * @param {import('./createElement.js').RyunixNode} [props.children]
3473
- * @param {string} [props.id]
3474
- * @returns {import('./createElement.js').RyunixElement}
3475
- */
3476
2751
  function ServerBoundary({ children, id }) {
3477
2752
  return createElement('div', { 'data-ryunix-server': id, style: { display: 'contents' } }, children);
3478
2753
  }
3479
2754
  ServerBoundary.ryunix_type = 'RYUNIX_SERVER_BOUNDARY';
3480
- /**
3481
- * Marks a DOM subtree for scoped hydration recovery. Mismatches inside this
3482
- * boundary can be recovered locally without remounting the full app root.
3483
- *
3484
- * @param {object} props
3485
- * @param {import('./createElement.js').RyunixNode} [props.children]
3486
- * @param {string} [props.id]
3487
- * @returns {import('./createElement.js').RyunixElement}
3488
- */
3489
2755
  function HydrationBoundary({ children, id }) {
3490
2756
  return createElement('div', {
3491
2757
  'data-ryunix-hydrate-boundary': id ?? '',
@@ -3510,11 +2776,6 @@ function ErrorBoundary({ children, fallback, }) {
3510
2776
  ErrorBoundary.ryunix_type =
3511
2777
  'RYUNIX_ERROR_BOUNDARY';
3512
2778
 
3513
- /**
3514
- * Client proxy for a compiled server action.
3515
- * @param {string} actionId - Build-time action identifier
3516
- * @returns {(...args: unknown[]) => Promise<unknown>}
3517
- */
3518
2779
  function createActionProxy(actionId) {
3519
2780
  return async function (...args) {
3520
2781
  const response = await fetch('/_ryunix/action', {
@@ -3533,80 +2794,66 @@ function createActionProxy(actionId) {
3533
2794
  };
3534
2795
  }
3535
2796
 
3536
- /**
3537
- * @typedef {object} OverlayError
3538
- * @property {string} [name]
3539
- * @property {string} [message]
3540
- * @property {string | string[]} [stack]
3541
- * @property {{ fileName?: string, lineNumber?: number }} [__ryunix_source]
3542
- */
3543
- /**
3544
- * @param {unknown} propsOrError
3545
- */
3546
2797
  function RyunixDevOverlay(propsOrError) {
3547
- /** @type {Record<string, unknown> | Error | null | undefined} */
3548
- const propsInput =
3549
- /** @type {Record<string, unknown> | Error | null | undefined} */ propsOrError;
3550
- // If propsOrError is an event or wrapped object, try to extract error
2798
+ const propsInput = propsOrError;
3551
2799
  const rawError = propsInput &&
3552
2800
  typeof propsInput === 'object' &&
3553
2801
  !(propsInput instanceof Error) &&
3554
2802
  propsInput.nativeEvent
3555
2803
  ? propsInput.error
3556
2804
  : propsInput;
3557
- /** @type {OverlayError | null} */
3558
2805
  let error = null;
3559
2806
  if (rawError instanceof Error) {
3560
2807
  error = rawError;
3561
2808
  }
3562
2809
  else if (rawError && typeof rawError === 'object') {
3563
2810
  if ('message' in rawError) {
3564
- error = /** @type {OverlayError} */ rawError;
2811
+ error = rawError;
3565
2812
  }
3566
2813
  else if ('error' in rawError) {
3567
- const nested = /** @type {Record<string, unknown>} */ rawError.error;
2814
+ const nested = rawError.error;
3568
2815
  error =
3569
2816
  nested && typeof nested === 'object'
3570
- ? /** @type {OverlayError} */ nested
2817
+ ? nested
3571
2818
  : null;
3572
2819
  }
3573
2820
  else {
3574
- error = /** @type {OverlayError} */ rawError;
2821
+ error = rawError;
3575
2822
  }
3576
2823
  }
3577
- // Debug string if error is broken
3578
2824
  const debugObjectStr = JSON.stringify(propsInput, Object.getOwnPropertyNames(propsInput || {}));
3579
2825
  const [snippetState, setSnippet] = useStore(null);
3580
2826
  const [startLineState, setStartLine] = useStore(1);
3581
2827
  const [errorFileState, setErrorFile] = useStore('');
3582
2828
  const [errorLineState, setErrorLine] = useStore(0);
3583
- const snippet = /** @type {string | null} */ snippetState;
3584
- const startLine = /** @type {number} */ startLineState;
3585
- const errorFile = /** @type {string} */ errorFileState;
3586
- const errorLine = /** @type {number} */ errorLineState;
3587
- // Normalize stack ensuring we have lines
3588
- /** @type {string[]} */
2829
+ const snippet = snippetState;
2830
+ const startLine = startLineState;
2831
+ const errorFile = errorFileState;
2832
+ const errorLine = errorLineState;
3589
2833
  let stackLines = [];
3590
2834
  if (error && error.stack) {
3591
2835
  stackLines =
3592
2836
  typeof error.stack === 'string'
3593
- ? error.stack.split('\n').filter((/** @type {string} */ line) => {
2837
+ ? error.stack.split('\n').filter((line) => {
3594
2838
  const trimmed = line.trim();
3595
2839
  if (!trimmed)
3596
2840
  return false;
3597
2841
  if (trimmed.includes('node_modules'))
3598
2842
  return false;
3599
- // Filter out internal Ryunix core framework files to isolate user code
3600
2843
  const isInternal = [
3601
- 'components.js',
2844
+ 'fiber-update.js',
3602
2845
  'workers.js',
3603
2846
  'reconciler.js',
3604
2847
  'commits.js',
2848
+ 'hooks/hooks.js',
3605
2849
  'hooks.js',
2850
+ 'error-boundary.js',
3606
2851
  'errorBoundary.js',
2852
+ 'boundaries.js',
3607
2853
  'serverBoundary.js',
3608
2854
  'app-router.js',
3609
2855
  'app-router-server.js',
2856
+ 'render/render.js',
3610
2857
  'render.js',
3611
2858
  'createElement.js',
3612
2859
  'index.js',
@@ -3614,7 +2861,7 @@ function RyunixDevOverlay(propsOrError) {
3614
2861
  return !isInternal;
3615
2862
  })
3616
2863
  : Array.isArray(error.stack)
3617
- ? /** @type {string[]} */ error.stack
2864
+ ? error.stack
3618
2865
  : [];
3619
2866
  }
3620
2867
  const errorName = error && typeof error.name === 'string' ? error.name : 'Unknown Error Type';
@@ -3624,22 +2871,18 @@ function RyunixDevOverlay(propsOrError) {
3624
2871
  useEffect(() => {
3625
2872
  let targetPath = null;
3626
2873
  let targetLine = null;
3627
- // 1. Direct JSX __source mapping (injected by Webpack/SWC)
3628
2874
  const ryunixSource = error?.__ryunix_source;
3629
2875
  if (ryunixSource?.fileName) {
3630
2876
  targetPath = ryunixSource.fileName;
3631
2877
  targetLine = ryunixSource.lineNumber ?? null;
3632
2878
  }
3633
- // 2. Fallback to Regex Stack Parsing
3634
2879
  if (!targetPath || !targetLine) {
3635
2880
  for (let i = 0; i < stackLines.length; i++) {
3636
2881
  const line = stackLines[i];
3637
2882
  if (!line.includes(':'))
3638
2883
  continue;
3639
- // Deterministic string-based parsing (no regex on uncontrolled data)
3640
2884
  let matchedPath = null;
3641
2885
  let matchedLine = null;
3642
- // V8 format: "at fn (file:line:col)" — extract content between parens
3643
2886
  const parenOpen = line.indexOf('(');
3644
2887
  const parenClose = line.lastIndexOf(')');
3645
2888
  if (parenOpen !== -1 && parenClose > parenOpen) {
@@ -3655,7 +2898,6 @@ function RyunixDevOverlay(propsOrError) {
3655
2898
  }
3656
2899
  }
3657
2900
  }
3658
- // V8 format without parens: "at file:line:col"
3659
2901
  if (!matchedPath) {
3660
2902
  const trimmed = line.trim();
3661
2903
  if (trimmed.startsWith('at ')) {
@@ -3672,7 +2914,6 @@ function RyunixDevOverlay(propsOrError) {
3672
2914
  }
3673
2915
  }
3674
2916
  }
3675
- // Ryunix format: "file.ryx:line" or "file.jsx:line"
3676
2917
  if (!matchedPath) {
3677
2918
  const exts = ['.ryx', '.jsx', '.js', '.ts', '.tsx'];
3678
2919
  const c1 = line.lastIndexOf(':');
@@ -3780,7 +3021,6 @@ function RyunixDevOverlay(propsOrError) {
3780
3021
  maxHeight: '150px',
3781
3022
  height: 'auto',
3782
3023
  };
3783
- /** @param {boolean} isErrorLine */
3784
3024
  const lineStyle = (isErrorLine) => ({
3785
3025
  display: 'flex',
3786
3026
  backgroundColor: isErrorLine ? 'rgba(239, 68, 68, 0.15)' : 'transparent',
@@ -3838,9 +3078,7 @@ function RyunixDevOverlay(propsOrError) {
3838
3078
  }, createElement('path', {
3839
3079
  d: 'M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z',
3840
3080
  }), createElement('polyline', { points: '13 2 13 9 20 9' })), errorFile, ':', errorLine), snippet &&
3841
- createElement('div', { style: snippetContainerStyle }, createElement('div', { style: { display: 'flex', flexDirection: 'column' } }, ...snippetLines.map((
3842
- /** @type {string} */ lineText,
3843
- /** @type {number} */ index) => {
3081
+ createElement('div', { style: snippetContainerStyle }, createElement('div', { style: { display: 'flex', flexDirection: 'column' } }, ...snippetLines.map((lineText, index) => {
3844
3082
  const currentLineNumber = startLine + index;
3845
3083
  const isErrorLine = currentLineNumber === errorLine;
3846
3084
  return createElement('div', { key: index, style: lineStyle(isErrorLine) }, createElement('span', {
@@ -3877,16 +3115,14 @@ function RyunixDevOverlay(propsOrError) {
3877
3115
  flexDirection: 'column',
3878
3116
  gap: '12px',
3879
3117
  },
3880
- }, ...stackLines.map((/** @type {string} */ line, /** @type {number} */ i) => {
3118
+ }, ...stackLines.map((line, i) => {
3881
3119
  if (i === 0 &&
3882
3120
  (line.startsWith('Error:') ||
3883
3121
  line.startsWith('TypeError:')))
3884
3122
  return null;
3885
- // Deterministic string-based stack frame parsing (no polynomial regex)
3886
3123
  const trimmed = line.trim();
3887
3124
  let fnName = '<anonymous>';
3888
3125
  let filePath = line;
3889
- // V8 format: "at fnName (file:line:col)" or "at file:line:col"
3890
3126
  if (trimmed.startsWith('at ')) {
3891
3127
  const rest = trimmed.slice(3);
3892
3128
  const parenOpen = rest.indexOf('(');
@@ -3897,7 +3133,6 @@ function RyunixDevOverlay(propsOrError) {
3897
3133
  filePath = rest.slice(parenOpen + 1, parenClose);
3898
3134
  }
3899
3135
  else {
3900
- // "at file:line:col" — no function name
3901
3136
  if (rest.includes(':')) {
3902
3137
  fnName = '<anonymous>';
3903
3138
  }
@@ -3907,13 +3142,11 @@ function RyunixDevOverlay(propsOrError) {
3907
3142
  filePath = rest;
3908
3143
  }
3909
3144
  }
3910
- // Firefox format: "fnName@file:line:col"
3911
3145
  else if (trimmed.includes('@')) {
3912
3146
  const atIdx = trimmed.indexOf('@');
3913
3147
  fnName = trimmed.slice(0, atIdx) || '<anonymous>';
3914
3148
  filePath = trimmed.slice(atIdx + 1);
3915
3149
  }
3916
- // Ryunix format: "fnName file.ext:line"
3917
3150
  else {
3918
3151
  const exts = ['.ryx', '.jsx', '.js', '.ts', '.tsx'];
3919
3152
  const parts = trimmed.split(/\s+/);
@@ -3960,37 +3193,290 @@ function RyunixDevOverlay(propsOrError) {
3960
3193
  : createElement('div', { style: { color: '#6b7280', fontStyle: 'italic' } }, 'No stack trace available.'))))));
3961
3194
  }
3962
3195
 
3963
- /**
3964
- * Public API surface of `@unsetsoft/ryunixjs`.
3965
- * Re-exports createElement, hooks, render, SSR helpers, and related utilities.
3966
- *
3967
- * @module lib/index
3968
- */
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
+ }
3969
3444
 
3970
3445
  var Ryunix = /*#__PURE__*/Object.freeze({
3971
3446
  __proto__: null,
3972
3447
  Children: Children,
3448
+ DEFAULT_THEME_COOKIE_NAME: DEFAULT_THEME_COOKIE_NAME,
3973
3449
  ErrorBoundary: ErrorBoundary,
3450
+ Footer: Footer,
3974
3451
  Fragment: Fragment,
3975
- Hooks: hooks,
3452
+ Header: Header,
3453
+ Hooks: index,
3976
3454
  HydrationBoundary: HydrationBoundary,
3977
3455
  Link: Link,
3456
+ Main: Main,
3978
3457
  NavLink: NavLink,
3979
3458
  Priority: Priority,
3980
3459
  RouterProvider: RouterProvider,
3981
3460
  RyunixDevOverlay: RyunixDevOverlay,
3982
3461
  ServerBoundary: ServerBoundary,
3983
3462
  Suspense: Suspense,
3463
+ THEME_PREFERENCES: THEME_PREFERENCES,
3464
+ ThemeInitScript: ThemeInitScript,
3465
+ ThemeToggle: ThemeToggle,
3466
+ applyTheme: applyTheme,
3984
3467
  batchUpdates: batchUpdates,
3985
3468
  cloneElement: cloneElement,
3986
3469
  createActionProxy: createActionProxy,
3987
3470
  createContext: createContext,
3988
3471
  createElement: createElement,
3989
3472
  createPortal: createPortal,
3473
+ createThemeController: createThemeController,
3990
3474
  deepEqual: deepEqual,
3991
3475
  escapeHtml: escapeHtml,
3992
3476
  forwardRef: forwardRef,
3993
3477
  getState: getState,
3478
+ getSystemColorScheme: getSystemColorScheme,
3479
+ getThemeCookie: getThemeCookie,
3994
3480
  hydrate: hydrate,
3995
3481
  init: init,
3996
3482
  isValidElement: isValidElement,
@@ -4008,8 +3494,13 @@ var Ryunix = /*#__PURE__*/Object.freeze({
4008
3494
  renderToString: renderToString,
4009
3495
  renderToStringAsync: renderToStringAsync,
4010
3496
  resetIdCounter: resetIdCounter,
3497
+ resolveEffectiveTheme: resolveEffectiveTheme,
3498
+ resolveThemeFromCookie: resolveThemeFromCookie,
4011
3499
  safeRender: safeRender,
3500
+ setThemeCookie: setThemeCookie,
4012
3501
  shallowEqual: shallowEqual,
3502
+ themeController: themeController,
3503
+ themeInitScript: themeInitScript,
4013
3504
  useCallback: useCallback,
4014
3505
  useDebounce: useDebounce,
4015
3506
  useDeferredValue: useDeferredValue,
@@ -4033,10 +3524,79 @@ var Ryunix = /*#__PURE__*/Object.freeze({
4033
3524
  useSwitch: useSwitch,
4034
3525
  useThrottle: useThrottle,
4035
3526
  useTransition: useTransition,
3527
+ watchSystemTheme: watchSystemTheme,
4036
3528
  withProfiler: withProfiler
4037
3529
  });
4038
3530
 
4039
- 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;
4040
3600
 
4041
- export { Children, ErrorBoundary, Fragment, hooks as Hooks, HydrationBoundary, Image, Link, MDXContent, MDXProvider, NavLink, Priority, RouterProvider, RyunixDevOverlay, ServerBoundary, Suspense, batchUpdates, cloneElement, createActionProxy, createContext, createElement, createPortal, deepEqual, Ryunix as default, defaultComponents, escapeHtml, forwardRef, getMDXComponents, getState, hydrate, init, isValidElement, lazy, logHydrationBoundaryMismatch, logHydrationBoundaryRecovery, logHydrationFatal, logHydrationInfo, logHydrationRecoverable, memo, preload, profiler, render, renderToReadableStream, renderToString, renderToStringAsync, resetIdCounter, safeRender, shallowEqual, useCallback, useDebounce, useDeferredValue, useEffect, useHash, useId, useLayoutEffect, useMDXComponents, useMemo, useMetadata, usePathname, usePersistentStore, usePersistentStore as usePersitentStore, useProfiler, useQuery, useReducer, useRef, useRouter, useSearchParams, useStore, useStorePriority, useSwitch, useThrottle, useTransition, withProfiler };
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 };
4042
3602
  //# sourceMappingURL=Ryunix.esm.js.map