@jsenv/navi 0.16.6 → 0.16.8

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.
@@ -2459,8 +2459,9 @@ const stateSignal = (defaultValue, options = {}) => {
2459
2459
  // Convert numeric IDs to strings for consistency
2460
2460
  const signalIdString = String(signalId);
2461
2461
  if (globalSignalRegistry.has(signalIdString)) {
2462
+ const conflictInfo = globalSignalRegistry.get(signalIdString);
2462
2463
  throw new Error(
2463
- `Signal ID conflict: A signal with ID "${signalIdString}" already exists`,
2464
+ `Signal ID conflict: A signal with ID "${signalIdString}" already exists (existing default: ${conflictInfo.options.defaultValue})`,
2464
2465
  );
2465
2466
  }
2466
2467
 
@@ -2475,7 +2476,7 @@ const stateSignal = (defaultValue, options = {}) => {
2475
2476
  if (valueFromLocalStorage !== undefined) {
2476
2477
  if (debug) {
2477
2478
  console.debug(
2478
- `[stateSignal] using value from localStorage "${localStorageKey}"=${valueFromLocalStorage}`,
2479
+ `[stateSignal:${signalIdString}] using value from localStorage "${localStorageKey}"=${valueFromLocalStorage}`,
2479
2480
  );
2480
2481
  }
2481
2482
  return valueFromLocalStorage;
@@ -2485,20 +2486,34 @@ const stateSignal = (defaultValue, options = {}) => {
2485
2486
  if (sourceValue !== undefined) {
2486
2487
  if (debug) {
2487
2488
  console.debug(
2488
- `[stateSignal] using value from source signal=${sourceValue}`,
2489
+ `[stateSignal:${signalIdString}] using value from source signal=${sourceValue}`,
2489
2490
  );
2490
2491
  }
2491
2492
  return sourceValue;
2492
2493
  }
2493
2494
  }
2494
2495
  if (debug) {
2495
- console.debug(`[stateSignal] using default value=${defaultValue}`);
2496
+ console.debug(
2497
+ `[stateSignal:${signalIdString}] using default value=${defaultValue}`,
2498
+ );
2496
2499
  }
2497
2500
  return defaultValue;
2498
2501
  };
2499
2502
 
2500
2503
  const advancedSignal = signal(getFallbackValue());
2501
2504
 
2505
+ if (debug) {
2506
+ console.debug(
2507
+ `[stateSignal:${signalIdString}] created with initial value=${advancedSignal.peek()}`,
2508
+ {
2509
+ defaultValue,
2510
+ hasSourceSignal: Boolean(sourceSignal),
2511
+ persists,
2512
+ localStorageKey: persists ? localStorageKey : undefined,
2513
+ },
2514
+ );
2515
+ }
2516
+
2502
2517
  // Set signal ID and create meaningful string representation
2503
2518
  advancedSignal.__signalId = signalIdString;
2504
2519
  advancedSignal.toString = () => `{navi_state_signal:${signalIdString}}`;
@@ -2562,10 +2577,11 @@ const stateSignal = (defaultValue, options = {}) => {
2562
2577
  // we don't have anything in the source signal, keep current value
2563
2578
  if (debug) {
2564
2579
  console.debug(
2565
- `[stateSignal] source signal is undefined, keeping current value`,
2580
+ `[stateSignal:${signalIdString}] source signal is undefined, keeping current value=${advancedSignal.peek()}`,
2566
2581
  {
2567
2582
  sourcePreviousValue,
2568
2583
  sourceValue,
2584
+ currentValue: advancedSignal.peek(),
2569
2585
  },
2570
2586
  );
2571
2587
  }
@@ -2574,10 +2590,14 @@ const stateSignal = (defaultValue, options = {}) => {
2574
2590
  }
2575
2591
  // the case we want to support: source signal value changes -> override current value
2576
2592
  if (debug) {
2577
- console.debug(`[stateSignal] source signal updated`, {
2578
- sourcePreviousValue,
2579
- sourceValue,
2580
- });
2593
+ console.debug(
2594
+ `[stateSignal:${signalIdString}] source signal updated, overriding current value`,
2595
+ {
2596
+ sourcePreviousValue,
2597
+ sourceValue,
2598
+ previousValue: advancedSignal.peek(),
2599
+ },
2600
+ );
2581
2601
  }
2582
2602
  advancedSignal.value = sourceValue;
2583
2603
  sourcePreviousValue = sourceValue;
@@ -2593,14 +2613,14 @@ const stateSignal = (defaultValue, options = {}) => {
2593
2613
  if (value === undefined || value === null || value === defaultValue) {
2594
2614
  if (debug) {
2595
2615
  console.debug(
2596
- `[stateSignal] removing "${localStorageKey}" from localStorage`,
2616
+ `[stateSignal:${signalIdString}] removing "${localStorageKey}" from localStorage (value=${value}, default=${defaultValue})`,
2597
2617
  );
2598
2618
  }
2599
2619
  removeFromLocalStorage();
2600
2620
  } else {
2601
2621
  if (debug) {
2602
2622
  console.debug(
2603
- `[stateSignal] writing into localStorage "${localStorageKey}"=${value}`,
2623
+ `[stateSignal:${signalIdString}] writing into localStorage "${localStorageKey}"=${value}`,
2604
2624
  );
2605
2625
  }
2606
2626
  writeIntoLocalStorage(value);
@@ -2611,10 +2631,39 @@ const stateSignal = (defaultValue, options = {}) => {
2611
2631
  {
2612
2632
  effect(() => {
2613
2633
  const value = advancedSignal.value;
2634
+ const wasValid = validity.valid;
2614
2635
  updateValidity({ oneOf }, validity, value);
2615
- if (!validity.valid && autoFix) {
2616
- advancedSignal.value = autoFix();
2617
- return;
2636
+ if (!validity.valid) {
2637
+ if (debug) {
2638
+ console.debug(`[stateSignal:${signalIdString}] validation failed`, {
2639
+ value,
2640
+ oneOf,
2641
+ hasAutoFix: Boolean(autoFix),
2642
+ });
2643
+ }
2644
+ if (autoFix) {
2645
+ const fixedValue = autoFix();
2646
+ if (debug) {
2647
+ console.debug(
2648
+ `[stateSignal:${signalIdString}] auto-fixing invalid value`,
2649
+ {
2650
+ invalidValue: value,
2651
+ fixedValue,
2652
+ },
2653
+ );
2654
+ }
2655
+ advancedSignal.value = fixedValue;
2656
+ return;
2657
+ }
2658
+ } else if (!wasValid && validity.valid) {
2659
+ if (debug) {
2660
+ console.debug(
2661
+ `[stateSignal:${signalIdString}] validation now passes`,
2662
+ {
2663
+ value,
2664
+ },
2665
+ );
2666
+ }
2618
2667
  }
2619
2668
  });
2620
2669
  }
@@ -7503,6 +7552,62 @@ const useUITransitionContentId = value => {
7503
7552
  */
7504
7553
 
7505
7554
 
7555
+ // Raw URL part functionality for bypassing encoding
7556
+ const rawUrlPartSymbol = Symbol("raw_url_part");
7557
+ const rawUrlPart = (value) => {
7558
+ return {
7559
+ [rawUrlPartSymbol]: true,
7560
+ value,
7561
+ };
7562
+ };
7563
+
7564
+ /**
7565
+ * Encode parameter values for URL usage, with special handling for raw URL parts.
7566
+ * When a parameter is wrapped with rawUrlPart(), it bypasses encoding and is
7567
+ * inserted as-is into the URL.
7568
+ */
7569
+ const encodeParamValue = (value, isWildcard = false) => {
7570
+ if (value && value[rawUrlPartSymbol]) {
7571
+ return value.value;
7572
+ }
7573
+
7574
+ if (isWildcard) {
7575
+ // For wildcards, only encode characters that are invalid in URL paths,
7576
+ // but preserve slashes as they are path separators
7577
+ return value
7578
+ ? value.replace(/[^a-zA-Z0-9\-._~!$&'()*+,;=:@/]/g, (char) => {
7579
+ return encodeURIComponent(char);
7580
+ })
7581
+ : value;
7582
+ }
7583
+
7584
+ // For named parameters and search params, encode everything including slashes
7585
+ return encodeURIComponent(value);
7586
+ };
7587
+
7588
+ /**
7589
+ * Build query string from parameters, respecting rawUrlPart values
7590
+ */
7591
+ const buildQueryString = (params) => {
7592
+ const searchParamPairs = [];
7593
+
7594
+ for (const [key, value] of Object.entries(params)) {
7595
+ if (value !== undefined && value !== null) {
7596
+ const encodedKey = encodeURIComponent(key);
7597
+
7598
+ // Handle boolean values - if true, just add the key without value
7599
+ if (value === true || value === "") {
7600
+ searchParamPairs.push(encodedKey);
7601
+ } else {
7602
+ const encodedValue = encodeParamValue(value, false); // Search params encode slashes
7603
+ searchParamPairs.push(`${encodedKey}=${encodedValue}`);
7604
+ }
7605
+ }
7606
+ }
7607
+
7608
+ return searchParamPairs.join("&");
7609
+ };
7610
+
7506
7611
  // Base URL management
7507
7612
  let baseFileUrl;
7508
7613
  let baseUrl;
@@ -7619,7 +7724,7 @@ const createRoutePattern = (pattern) => {
7619
7724
  };
7620
7725
 
7621
7726
  const buildUrl = (params = {}) => {
7622
- return buildUrlFromPattern(parsedPattern, params);
7727
+ return buildUrlFromPattern(parsedPattern, params, pattern);
7623
7728
  };
7624
7729
 
7625
7730
  const resolveParams = (providedParams = {}) => {
@@ -7642,213 +7747,411 @@ const createRoutePattern = (pattern) => {
7642
7747
  * Build the most precise URL by using route relationships from pattern registry.
7643
7748
  * Each route is responsible for its own URL generation using its own signals.
7644
7749
  */
7645
- const buildMostPreciseUrl = (params = {}) => {
7646
- // Handle parameter resolution internally to preserve user intent detection
7647
- const resolvedParams = resolveParams(params);
7648
7750
 
7649
- // Start with resolved parameters
7650
- let finalParams = { ...resolvedParams };
7751
+ /**
7752
+ * Helper: Filter out default values from parameters for cleaner URLs
7753
+ */
7754
+ const removeDefaultValues = (params) => {
7755
+ const filtered = { ...params };
7651
7756
 
7652
7757
  for (const connection of connections) {
7653
7758
  const { paramName, signal, options } = connection;
7654
7759
  const defaultValue = options.defaultValue;
7655
7760
 
7656
- if (paramName in finalParams) {
7657
- // Parameter was explicitly provided - ALWAYS respect explicit values
7658
- // If it equals the default value, remove it for shorter URLs
7659
- if (finalParams[paramName] === defaultValue) {
7660
- delete finalParams[paramName];
7661
- }
7662
- // Note: Don't fall through to signal logic - explicit params take precedence
7663
- }
7664
- // Parameter was NOT provided, check signal value
7665
- else if (signal?.value !== undefined && signal.value !== defaultValue) {
7666
- // Only include signal value if it's not the default
7667
- finalParams[paramName] = signal.value;
7668
- // If signal.value === defaultValue, omit the parameter for shorter URL
7761
+ if (paramName in filtered && filtered[paramName] === defaultValue) {
7762
+ delete filtered[paramName];
7763
+ } else if (
7764
+ !(paramName in filtered) &&
7765
+ signal?.value !== undefined &&
7766
+ signal.value !== defaultValue
7767
+ ) {
7768
+ filtered[paramName] = signal.value;
7669
7769
  }
7670
7770
  }
7671
7771
 
7672
- // DEEPEST URL GENERATION: Check if we should use a child route instead
7673
- // This happens when:
7674
- // 1. This route's parameters are all defaults (would be omitted)
7675
- // 2. A child route has non-default parameters that should be included
7772
+ return filtered;
7773
+ };
7676
7774
 
7677
- // DEEPEST URL GENERATION: Only activate when NO explicit parameters provided
7678
- // This prevents overriding explicit user intentions with signal-based "smart" routing
7679
- // We need to distinguish between user-provided params and signal-derived params
7680
- let hasUserProvidedParams = false;
7775
+ /**
7776
+ * Helper: Find the best child route that matches current parameters and signals
7777
+ */
7778
+ const findBestChildRoute = (params, relationships) => {
7779
+ const childPatternObjs = relationships?.childPatterns || [];
7780
+ if (pattern === "/" || !childPatternObjs.length) {
7781
+ return null;
7782
+ }
7681
7783
 
7682
- // Check if provided params contain anything beyond what signals would provide
7683
- const signalDerivedParams = {};
7684
- for (const { paramName, signal, options } of connections) {
7685
- if (signal?.value !== undefined) {
7686
- const defaultValue = options.defaultValue;
7687
- // Only include signal value if it's not the default (same logic as above)
7688
- if (signal.value !== defaultValue) {
7689
- signalDerivedParams[paramName] = signal.value;
7690
- }
7784
+ // Try each child pattern object to find the most specific match
7785
+ for (const childPatternObj of childPatternObjs) {
7786
+ const childRouteCandidate = evaluateChildRoute(childPatternObj, params);
7787
+
7788
+ if (childRouteCandidate) {
7789
+ return childRouteCandidate;
7691
7790
  }
7692
7791
  }
7792
+ return null;
7793
+ };
7693
7794
 
7694
- // Check if original params (before resolution) contains anything that's not from signals
7695
- // This preserves user intent detection for explicit parameters
7696
- for (const [key, value] of Object.entries(params)) {
7697
- if (signalDerivedParams[key] !== value) {
7698
- hasUserProvidedParams = true;
7795
+ /**
7796
+ * Helper: Evaluate if a specific child route is suitable for current params/signals
7797
+ */
7798
+ const evaluateChildRoute = (childPatternObj, params) => {
7799
+ // Step 1: Check parameter compatibility
7800
+ const compatibility = checkChildRouteCompatibility(childPatternObj, params);
7801
+ if (!compatibility.isCompatible) {
7802
+ return null;
7803
+ }
7804
+
7805
+ // Step 2: Determine if child route should be used
7806
+ const shouldUseChild = shouldUseChildRoute(
7807
+ childPatternObj,
7808
+ params,
7809
+ compatibility,
7810
+ );
7811
+ if (!shouldUseChild) {
7812
+ return null;
7813
+ }
7814
+
7815
+ // Step 3: Build child route URL with proper parameter filtering
7816
+ return buildChildRouteUrl(childPatternObj, params);
7817
+ };
7818
+
7819
+ /**
7820
+ * Helper: Check if parameters are compatible with child route
7821
+ */
7822
+ const checkChildRouteCompatibility = (childPatternObj, params) => {
7823
+ const childParams = {};
7824
+ let isCompatible = true;
7825
+
7826
+ // Check both parent signals AND user-provided params for child route matching
7827
+ const paramsToCheck = [
7828
+ ...connections,
7829
+ ...Object.entries(params).map(([key, value]) => ({
7830
+ paramName: key,
7831
+ userValue: value,
7832
+ isUserProvided: true,
7833
+ })),
7834
+ ];
7835
+
7836
+ for (const item of paramsToCheck) {
7837
+ const result = processParameterForChildRoute(
7838
+ item,
7839
+ childPatternObj.pattern,
7840
+ );
7841
+
7842
+ if (!result.isCompatible) {
7843
+ isCompatible = false;
7699
7844
  break;
7700
7845
  }
7846
+
7847
+ if (result.shouldInclude) {
7848
+ childParams[result.paramName] = result.paramValue;
7849
+ }
7701
7850
  }
7702
7851
 
7703
- // Also check if original params has extra keys beyond what signals provide
7704
- const providedKeys = new Set(Object.keys(params));
7705
- const signalKeys = new Set(Object.keys(signalDerivedParams));
7706
- for (const key of providedKeys) {
7707
- if (!signalKeys.has(key)) {
7708
- hasUserProvidedParams = true;
7709
- break;
7852
+ return { isCompatible, childParams };
7853
+ };
7854
+
7855
+ /**
7856
+ * Helper: Process a single parameter for child route compatibility
7857
+ */
7858
+ const processParameterForChildRoute = (item, childParsedPattern) => {
7859
+ let paramName;
7860
+ let paramValue;
7861
+
7862
+ if (item.isUserProvided) {
7863
+ paramName = item.paramName;
7864
+ paramValue = item.userValue;
7865
+ } else {
7866
+ const { paramName: name, signal, options } = item;
7867
+ paramName = name;
7868
+ // Only include non-default parent signal values
7869
+ if (
7870
+ signal?.value === undefined ||
7871
+ signal.value === options.defaultValue
7872
+ ) {
7873
+ return { isCompatible: true, shouldInclude: false };
7710
7874
  }
7875
+ paramValue = signal.value;
7711
7876
  }
7712
7877
 
7713
- // ROOT ROUTE PROTECTION: Never apply deepest URL generation to root route "/"
7714
- // Users must always be able to navigate to home page regardless of app state
7715
- const isRootRoute = pattern === "/";
7878
+ // Check if parameter value matches a literal segment in child pattern
7879
+ const matchesChildLiteral = paramMatchesChildLiteral(
7880
+ paramValue,
7881
+ childParsedPattern,
7882
+ );
7716
7883
 
7717
- const relationships = patternRelationships.get(pattern);
7718
- const childPatterns = relationships?.childPatterns || [];
7719
- if (!hasUserProvidedParams && !isRootRoute && childPatterns.length) {
7720
- // Try to find the most specific child pattern that has active signals
7721
- for (const childPattern of childPatterns) {
7722
- const childPatternData = getPatternData(childPattern);
7723
- if (!childPatternData) continue;
7724
-
7725
- // Check if any of this child's parameters have non-default signal values
7726
- let hasActiveParams = false;
7727
- const childParams = {};
7728
-
7729
- // Include parent signal values for child pattern matching
7730
- // But first check if they're compatible with the child pattern
7731
- let parentSignalsCompatibleWithChild = true;
7732
- for (const parentConnection of connections) {
7733
- const { paramName, signal, options } = parentConnection;
7734
- // Only include non-default parent signal values
7735
- if (
7736
- signal?.value !== undefined &&
7737
- signal.value !== options.defaultValue
7738
- ) {
7739
- // Check if child pattern has conflicting literal segments for this parameter
7740
- const childParsedPattern = childPatternData.parsedPattern;
7884
+ if (matchesChildLiteral) {
7885
+ // Compatible - parameter value matches child literal
7886
+ return {
7887
+ isCompatible: true,
7888
+ shouldInclude: !item.isUserProvided,
7889
+ paramName,
7890
+ paramValue,
7891
+ };
7892
+ }
7741
7893
 
7742
- // Check if parent signal value matches a literal segment in child pattern
7743
- const matchesChildLiteral = childParsedPattern.segments.some(
7744
- (segment) =>
7745
- segment.type === "literal" && segment.value === signal.value,
7746
- );
7894
+ // Check for incompatible cases
7895
+ if (item.isUserProvided && !matchesChildLiteral) {
7896
+ // Check if this is a path parameter from parent pattern
7897
+ const isParentPathParam = connections.some(
7898
+ (conn) => conn.paramName === paramName,
7899
+ );
7747
7900
 
7748
- // If parent signal matches a literal in child, don't add as parameter
7749
- // (it's already represented in the child URL path)
7750
- if (matchesChildLiteral) {
7751
- // Compatible - signal value matches child literal, no need to add param
7752
- continue;
7753
- }
7901
+ if (isParentPathParam) {
7902
+ // User provided a path param value that doesn't match this child's literals
7903
+ return { isCompatible: false };
7904
+ }
7905
+ }
7754
7906
 
7755
- // For section parameter specifically, check if child has literal "settings"
7756
- // but parent signal has different value (incompatible case)
7757
- if (paramName === "section" && signal.value !== "settings") {
7758
- const hasSettingsLiteral = childParsedPattern.segments.some(
7759
- (segment) =>
7760
- segment.type === "literal" && segment.value === "settings",
7761
- );
7762
- if (hasSettingsLiteral) {
7763
- parentSignalsCompatibleWithChild = false;
7764
- break;
7765
- }
7766
- }
7907
+ // Special case: section parameter with settings literal
7908
+ if (paramName === "section" && paramValue !== "settings") {
7909
+ const hasSettingsLiteral = childParsedPattern.segments.some(
7910
+ (segment) => segment.type === "literal" && segment.value === "settings",
7911
+ );
7912
+ if (hasSettingsLiteral) {
7913
+ return { isCompatible: false };
7914
+ }
7915
+ }
7767
7916
 
7768
- // Only add parent signal as parameter if it doesn't match child literals
7769
- childParams[paramName] = signal.value;
7770
- }
7917
+ // Compatible but should only include if from signal (not user-provided)
7918
+ return {
7919
+ isCompatible: true,
7920
+ shouldInclude: !item.isUserProvided && !matchesChildLiteral,
7921
+ paramName,
7922
+ paramValue,
7923
+ };
7924
+ };
7925
+
7926
+ /**
7927
+ * Helper: Determine if child route should be used based on active parameters
7928
+ */
7929
+ const shouldUseChildRoute = (childPatternObj, params, compatibility) => {
7930
+ // Check if child has active non-default signal values
7931
+ let hasActiveParams = false;
7932
+ const childParams = { ...compatibility.childParams };
7933
+
7934
+ for (const connection of childPatternObj.connections) {
7935
+ const { paramName, signal, options } = connection;
7936
+ const defaultValue = options.defaultValue;
7937
+
7938
+ if (signal?.value !== undefined) {
7939
+ childParams[paramName] = signal.value;
7940
+ if (signal.value !== defaultValue) {
7941
+ hasActiveParams = true;
7771
7942
  }
7943
+ }
7944
+ }
7772
7945
 
7773
- // Skip this child if parent signals are incompatible
7774
- if (!parentSignalsCompatibleWithChild) {
7775
- continue;
7946
+ // Check if child pattern can be fully satisfied
7947
+ const initialMergedParams = { ...childParams, ...params };
7948
+ const canBuildChildCompletely = childPatternObj.pattern.segments.every(
7949
+ (segment) => {
7950
+ if (segment.type === "literal") return true;
7951
+ if (segment.type === "param") {
7952
+ return (
7953
+ segment.optional || initialMergedParams[segment.name] !== undefined
7954
+ );
7776
7955
  }
7956
+ return true;
7957
+ },
7958
+ );
7777
7959
 
7778
- // Check child connections and see if any have non-default values
7779
- for (const connection of childPatternData.connections) {
7780
- const { paramName, signal, options } = connection;
7781
- const defaultValue = options.defaultValue;
7960
+ const hasProvidedParams = Object.keys(params).length > 0;
7782
7961
 
7783
- if (signal?.value !== undefined) {
7784
- childParams[paramName] = signal.value;
7785
- if (signal.value !== defaultValue) {
7786
- hasActiveParams = true;
7787
- }
7788
- }
7962
+ // Use child route if:
7963
+ // 1. Child has active non-default parameters, OR
7964
+ // 2. User provided params AND child can be built completely
7965
+ return hasActiveParams || (hasProvidedParams && canBuildChildCompletely);
7966
+ };
7967
+
7968
+ /**
7969
+ * Helper: Build URL for selected child route with proper parameter filtering
7970
+ */
7971
+ const buildChildRouteUrl = (childPatternObj, params) => {
7972
+ // Start with child signal values
7973
+ const baseParams = {};
7974
+ for (const connection of childPatternObj.connections) {
7975
+ const { paramName, signal, options } = connection;
7976
+ if (
7977
+ signal?.value !== undefined &&
7978
+ signal.value !== options.defaultValue
7979
+ ) {
7980
+ baseParams[paramName] = signal.value;
7981
+ }
7982
+ }
7983
+
7984
+ // Apply user params with filtering logic
7985
+ for (const [paramName, userValue] of Object.entries(params)) {
7986
+ const childConnection = childPatternObj.connections.find(
7987
+ (conn) => conn.paramName === paramName,
7988
+ );
7989
+
7990
+ if (childConnection) {
7991
+ const { options } = childConnection;
7992
+ const defaultValue = options.defaultValue;
7993
+
7994
+ // Only include if it's NOT the signal's default value
7995
+ if (userValue !== defaultValue) {
7996
+ baseParams[paramName] = userValue;
7997
+ } else {
7998
+ // User provided the default value - complete omission
7999
+ delete baseParams[paramName];
8000
+ }
8001
+ } else {
8002
+ // Check if param corresponds to a literal segment in child pattern
8003
+ const isConsumedByChildPath = childPatternObj.pattern.segments.some(
8004
+ (segment) =>
8005
+ segment.type === "literal" && segment.value === userValue,
8006
+ );
8007
+
8008
+ if (!isConsumedByChildPath) {
8009
+ // Not consumed by child path, keep it as query param
8010
+ baseParams[paramName] = userValue;
7789
8011
  }
8012
+ }
8013
+ }
8014
+
8015
+ // Build child URL
8016
+ const childUrl = childPatternObj.buildUrl(baseParams);
8017
+
8018
+ if (childUrl && !childUrl.includes(":")) {
8019
+ // Check for parent optimization before returning
8020
+ const optimizedUrl = checkChildParentOptimization(
8021
+ childPatternObj.originalPattern,
8022
+ childUrl,
8023
+ baseParams,
8024
+ );
8025
+ return optimizedUrl || childUrl;
8026
+ }
8027
+
8028
+ return null;
8029
+ };
8030
+
8031
+ /**
8032
+ * Helper: Check if parent route optimization applies to child route
8033
+ */
8034
+ const checkChildParentOptimization = (childPattern, childUrl, baseParams) => {
8035
+ if (Object.keys(baseParams).length > 0) {
8036
+ return null; // No optimization if parameters exist
8037
+ }
8038
+
8039
+ const childRelationships = patternRelationships.get(childPattern);
8040
+ const childParentObjs = childRelationships?.parentPatterns || [];
8041
+
8042
+ for (const childParentObj of childParentObjs) {
8043
+ if (childParentObj.originalPattern === pattern) {
8044
+ // Get the child pattern object from relationships instead of recreating
8045
+ const childPatternObj = childRelationships;
7790
8046
 
7791
- // If child has non-default parameters, use the child route
7792
- if (hasActiveParams) {
7793
- const childPatternObj = createRoutePattern(childPattern);
7794
- // Use buildUrl (not buildMostPreciseUrl) to avoid infinite recursion
7795
- const childUrl = childPatternObj.buildUrl(childParams);
7796
- if (childUrl) {
7797
- return childUrl;
8047
+ const allChildParamsAreDefaults = (
8048
+ childPatternObj.connections || []
8049
+ ).every((childConnection) => {
8050
+ const { signal, options } = childConnection;
8051
+ return signal?.value === options.defaultValue;
8052
+ });
8053
+
8054
+ if (allChildParamsAreDefaults) {
8055
+ // Build current route URL for comparison
8056
+ const resolvedParams = resolveParams({});
8057
+ const finalParams = removeDefaultValues(resolvedParams);
8058
+ const currentUrl = buildUrlFromPattern(
8059
+ parsedPattern,
8060
+ finalParams,
8061
+ pattern,
8062
+ );
8063
+ if (currentUrl.length < childUrl.length) {
8064
+ return currentUrl;
7798
8065
  }
7799
8066
  }
7800
8067
  }
7801
8068
  }
8069
+ return null;
8070
+ };
8071
+
8072
+ const buildMostPreciseUrl = (params = {}) => {
8073
+ // Step 1: Resolve and clean parameters
8074
+ const resolvedParams = resolveParams(params);
8075
+
8076
+ // Step 2: Check for parent route optimization BEFORE removing defaults
8077
+ // This allows optimization when final effective values match defaults
8078
+ const relationships = patternRelationships.get(pattern);
8079
+ const optimizedUrl = checkParentRouteOptimization(
8080
+ resolvedParams,
8081
+ relationships,
8082
+ );
8083
+ if (optimizedUrl) {
8084
+ return optimizedUrl;
8085
+ }
8086
+
8087
+ // Step 3: Remove default values for normal URL building
8088
+ let finalParams = removeDefaultValues(resolvedParams);
8089
+
8090
+ // Step 4: Try to find a more specific child route
8091
+ const childRouteUrl = findBestChildRoute(params, relationships);
8092
+ if (childRouteUrl) {
8093
+ return childRouteUrl;
8094
+ }
7802
8095
 
7803
- // PARENT PARAMETER INHERITANCE: Inherit query parameters from parent patterns
7804
- // This allows child routes like "/map/isochrone" to inherit "zoom=15" from parent "/map/?zoom=..."
7805
- const parentPatterns = relationships?.parentPatterns || [];
7806
- for (const parentPattern of parentPatterns) {
7807
- const parentPatternData = getPatternData(parentPattern);
7808
- if (!parentPatternData) continue;
8096
+ // Step 5: Inherit parameters from parent routes
8097
+ inheritParentParameters(finalParams, relationships);
8098
+
8099
+ // Step 6: Build the current route URL
8100
+ const generatedUrl = buildCurrentRouteUrl(finalParams);
8101
+
8102
+ return generatedUrl;
8103
+ };
8104
+
8105
+ /**
8106
+ * Helper: Inherit query parameters from parent patterns
8107
+ */
8108
+ const inheritParentParameters = (finalParams, relationships) => {
8109
+ const parentPatternObjs = relationships?.parentPatterns || [];
7809
8110
 
8111
+ for (const parentPatternObj of parentPatternObjs) {
7810
8112
  // Check parent's signal connections for non-default values to inherit
7811
- for (const parentConnection of parentPatternData.connections) {
8113
+ for (const parentConnection of parentPatternObj.connections) {
7812
8114
  const { paramName, signal, options } = parentConnection;
7813
8115
  const defaultValue = options.defaultValue;
7814
8116
 
7815
- // If we don't already have this parameter and parent signal has non-default value
8117
+ // Only inherit if we don't have this param and parent has non-default value
7816
8118
  if (
7817
8119
  !(paramName in finalParams) &&
7818
8120
  signal?.value !== undefined &&
7819
8121
  signal.value !== defaultValue
7820
8122
  ) {
7821
- // Check if this parameter corresponds to a literal segment in our path
7822
- // E.g., don't inherit "section=analytics" if our path is "/admin/analytics"
8123
+ // Don't inherit if parameter corresponds to a literal in our path
7823
8124
  const shouldInherit = !isParameterRedundantWithLiteralSegments(
7824
8125
  parsedPattern,
7825
- parentPatternData.parsedPattern,
8126
+ parentPatternObj.pattern,
7826
8127
  paramName,
7827
8128
  signal.value,
7828
8129
  );
7829
8130
 
7830
8131
  if (shouldInherit) {
7831
- // Inherit the parent's signal value
7832
8132
  finalParams[paramName] = signal.value;
7833
8133
  }
7834
8134
  }
7835
8135
  }
7836
8136
  }
8137
+ };
7837
8138
 
8139
+ /**
8140
+ * Helper: Build URL for current route with filtered pattern
8141
+ */
8142
+ const buildCurrentRouteUrl = (finalParams) => {
7838
8143
  if (!parsedPattern.segments) {
7839
8144
  return "/";
7840
8145
  }
7841
8146
 
7842
- // Filter out segments for parameters that are not provided (omitted defaults)
8147
+ // Filter out parameter segments that don't have values
7843
8148
  const filteredPattern = {
7844
8149
  ...parsedPattern,
7845
8150
  segments: parsedPattern.segments.filter((segment) => {
7846
8151
  if (segment.type === "param") {
7847
- // Only keep parameter segments if we have a value for them
7848
8152
  return segment.name in finalParams;
7849
8153
  }
7850
- // Always keep literal segments
7851
- return true;
8154
+ return true; // Keep literal segments
7852
8155
  }),
7853
8156
  };
7854
8157
 
@@ -7860,7 +8163,178 @@ const createRoutePattern = (pattern) => {
7860
8163
  filteredPattern.trailingSlash = false;
7861
8164
  }
7862
8165
 
7863
- return buildUrlFromPattern(filteredPattern, finalParams);
8166
+ return buildUrlFromPattern(filteredPattern, finalParams, pattern);
8167
+ };
8168
+
8169
+ /**
8170
+ * Helper: Check if parent route can provide a shorter equivalent URL
8171
+ */
8172
+ const checkParentRouteOptimization = (resolvedParams, relationships) => {
8173
+ // Only consider parent optimization for patterns with signal connections
8174
+ if (connections.length === 0) {
8175
+ return null;
8176
+ }
8177
+
8178
+ // Check if all final effective values equal their defaults
8179
+ const allEffectiveValuesAreDefaults = connections.every((conn) => {
8180
+ // Final effective value is what's in resolvedParams (signals + provided params)
8181
+ const effectiveValue =
8182
+ resolvedParams[conn.paramName] ?? conn.options.defaultValue;
8183
+ return effectiveValue === conn.options.defaultValue;
8184
+ });
8185
+
8186
+ // Only optimize if all effective values equal their defaults
8187
+ if (!allEffectiveValuesAreDefaults) {
8188
+ return null;
8189
+ }
8190
+
8191
+ // Check if there are extra parameters not handled by current route's connections
8192
+ const connectionParamNames = new Set(
8193
+ connections.map((conn) => conn.paramName),
8194
+ );
8195
+ const hasExtraParams = Object.keys(resolvedParams).some(
8196
+ (paramName) => !connectionParamNames.has(paramName),
8197
+ );
8198
+
8199
+ // Don't optimize if there are extra parameters that would be lost
8200
+ if (hasExtraParams) {
8201
+ return null;
8202
+ }
8203
+
8204
+ const possibleParentObjs = relationships?.parentPatterns || [];
8205
+
8206
+ for (const parentPatternObj of possibleParentObjs) {
8207
+ // Skip root route and routes without parameters
8208
+ if (
8209
+ parentPatternObj.originalPattern === "/" ||
8210
+ !parentPatternObj.originalPattern.includes(":")
8211
+ ) {
8212
+ continue;
8213
+ }
8214
+
8215
+ const optimizedParentUrl = evaluateParentOptimization(
8216
+ parentPatternObj,
8217
+ resolvedParams,
8218
+ );
8219
+
8220
+ if (optimizedParentUrl) {
8221
+ // Before returning optimized parent URL, check if we need to inherit parameters
8222
+ // from our ancestors that the parent route might not inherit on its own
8223
+ const parentFinalParams = { ...resolvedParams };
8224
+
8225
+ // Remove params that belong to current route (they're at defaults anyway)
8226
+ for (const conn of connections) {
8227
+ delete parentFinalParams[conn.paramName];
8228
+ }
8229
+
8230
+ // Inherit from all ancestor routes, not just immediate parent
8231
+ inheritParentParameters(parentFinalParams, relationships);
8232
+
8233
+ // If we inherited any parameters, add them to the parent URL
8234
+ const extraParamEntries = Object.entries(parentFinalParams).filter(
8235
+ ([key, value]) => {
8236
+ // Only include params not handled by parent route
8237
+ const isParentParam = parentPatternObj.connections.some(
8238
+ (conn) => conn.paramName === key,
8239
+ );
8240
+ return !isParentParam && value !== undefined;
8241
+ },
8242
+ );
8243
+
8244
+ if (extraParamEntries.length > 0) {
8245
+ const queryString = buildQueryString(
8246
+ Object.fromEntries(extraParamEntries),
8247
+ );
8248
+ return (
8249
+ optimizedParentUrl +
8250
+ (optimizedParentUrl.includes("?") ? "&" : "?") +
8251
+ queryString
8252
+ );
8253
+ }
8254
+
8255
+ return optimizedParentUrl;
8256
+ }
8257
+ }
8258
+
8259
+ return null;
8260
+ };
8261
+
8262
+ /**
8263
+ * Helper: Evaluate a specific parent pattern for URL optimization
8264
+ */
8265
+ const evaluateParentOptimization = (parentPatternObj, resolvedParams) => {
8266
+ // Get literal segments from child pattern to map to parent parameters
8267
+ const childLiterals = getPatternLiterals(parsedPattern);
8268
+
8269
+ // Check if parent would also have all default values
8270
+ // For parent optimization, we consider both explicitly provided params and literal segments
8271
+ const allParentParamsAreDefaults = parentPatternObj.connections.every(
8272
+ (parentConnection) => {
8273
+ const paramName = parentConnection.paramName;
8274
+
8275
+ // If explicitly provided in resolved params, use that
8276
+ if (resolvedParams[paramName] !== undefined) {
8277
+ return (
8278
+ resolvedParams[paramName] === parentConnection.options.defaultValue
8279
+ );
8280
+ }
8281
+
8282
+ // Check if this parent parameter corresponds to a literal segment in child
8283
+ // If parent default matches a child literal, consider it as using the default
8284
+ const defaultValue = parentConnection.options.defaultValue;
8285
+ if (childLiterals.includes(defaultValue)) {
8286
+ return true; // Literal segment effectively provides the default value
8287
+ }
8288
+
8289
+ // Otherwise assume parent would use its default for optimization purposes
8290
+ return true;
8291
+ },
8292
+ );
8293
+
8294
+ if (!allParentParamsAreDefaults) {
8295
+ return null; // Can't optimize if parent has non-default values
8296
+ }
8297
+
8298
+ // Check if parent's default values match our literals
8299
+ const parentPointsToCurrentRoute = parentPatternObj.connections.every(
8300
+ (parentConnection) => {
8301
+ const { options } = parentConnection;
8302
+ const defaultValue = options.defaultValue;
8303
+ return childLiterals.includes(defaultValue);
8304
+ },
8305
+ );
8306
+
8307
+ if (parentPointsToCurrentRoute) {
8308
+ // Build parent URL using defaults, not current signal values
8309
+ const parentDefaultParams = {};
8310
+ for (const parentConnection of parentPatternObj.connections) {
8311
+ parentDefaultParams[parentConnection.paramName] =
8312
+ parentConnection.options.defaultValue;
8313
+ }
8314
+ // Build parent URL and check if it can be optimized further
8315
+ let parentUrl = parentPatternObj.buildUrl(parentDefaultParams);
8316
+
8317
+ // Check if parent can optimize itself by removing default parameters
8318
+ if (parentUrl && parentUrl !== "/") {
8319
+ // Check if all parent's default params are actually defaults
8320
+ const parentAllDefaults = parentPatternObj.connections.every((conn) => {
8321
+ const paramValue = parentDefaultParams[conn.paramName];
8322
+ return paramValue === conn.options.defaultValue;
8323
+ });
8324
+
8325
+ if (parentAllDefaults) {
8326
+ // Try to build parent URL without any parameters to see if it's shorter
8327
+ const parentMinimalUrl = parentPatternObj.buildUrl({});
8328
+ if (parentMinimalUrl && parentMinimalUrl.length < parentUrl.length) {
8329
+ parentUrl = parentMinimalUrl;
8330
+ }
8331
+ }
8332
+
8333
+ return parentUrl;
8334
+ }
8335
+ }
8336
+
8337
+ return null;
7864
8338
  };
7865
8339
 
7866
8340
  return {
@@ -7868,6 +8342,7 @@ const createRoutePattern = (pattern) => {
7868
8342
  pattern: parsedPattern,
7869
8343
  cleanPattern, // Return the clean pattern string
7870
8344
  connections, // Return signal connections along with pattern
8345
+ specificity: calculatePatternSpecificity(parsedPattern), // Pre-calculate specificity
7871
8346
  applyOn,
7872
8347
  buildUrl,
7873
8348
  buildMostPreciseUrl,
@@ -7875,6 +8350,50 @@ const createRoutePattern = (pattern) => {
7875
8350
  };
7876
8351
  };
7877
8352
 
8353
+ /**
8354
+ * Helper: Extract literal values from pattern segments
8355
+ */
8356
+ const getPatternLiterals = (pattern) => {
8357
+ return pattern.segments
8358
+ .filter((seg) => seg.type === "literal")
8359
+ .map((seg) => seg.value);
8360
+ };
8361
+
8362
+ /**
8363
+ * Helper: Check if parameter matches any literal in child pattern
8364
+ */
8365
+ const paramMatchesChildLiteral = (paramValue, childParsedPattern) => {
8366
+ return childParsedPattern.segments.some(
8367
+ (segment) => segment.type === "literal" && segment.value === paramValue,
8368
+ );
8369
+ };
8370
+
8371
+ /**
8372
+ * Calculate pattern specificity score for route matching
8373
+ * Higher score = more specific route
8374
+ */
8375
+ const calculatePatternSpecificity = (parsedPattern) => {
8376
+ let specificity = 0;
8377
+
8378
+ // Count path segments (ignoring query params for specificity)
8379
+ const pathSegments = parsedPattern.segments || [];
8380
+
8381
+ for (const segment of pathSegments) {
8382
+ if (segment.type === "literal") {
8383
+ // Literal segments are more specific than parameters
8384
+ specificity += 100; // High score for literal segments
8385
+ } else if (segment.type === "param") {
8386
+ // Parameter segments are less specific
8387
+ specificity += 10; // Lower score for parameters
8388
+ }
8389
+ }
8390
+
8391
+ // Add base score for number of path segments (more segments = more specific)
8392
+ specificity += pathSegments.length;
8393
+
8394
+ return specificity;
8395
+ };
8396
+
7878
8397
  /**
7879
8398
  * Parse a route pattern string into structured segments
7880
8399
  */
@@ -8163,19 +8682,99 @@ const extractSearchParams = (urlObj, connections = []) => {
8163
8682
  return params;
8164
8683
  };
8165
8684
 
8685
+ /**
8686
+ * Build query parameters respecting hierarchical order from ancestor patterns
8687
+ */
8688
+ const buildHierarchicalQueryParams = (
8689
+ parsedPattern,
8690
+ params,
8691
+ originalPattern,
8692
+ ) => {
8693
+ const queryParams = {};
8694
+ const processedParams = new Set();
8695
+
8696
+ // Get relationships for this pattern
8697
+ const relationships = patternRelationships.get(originalPattern);
8698
+ const parentPatterns = relationships?.parentPatterns || [];
8699
+
8700
+ // Step 1: Add query parameters from ancestor patterns (oldest to newest)
8701
+ // This ensures ancestor parameters come first in their declaration order
8702
+ const ancestorPatterns = parentPatterns; // Process in order: root ancestor first, then immediate parent
8703
+
8704
+ for (const ancestorPatternObj of ancestorPatterns) {
8705
+ if (ancestorPatternObj.pattern?.queryParams) {
8706
+
8707
+ for (const queryParam of ancestorPatternObj.pattern.queryParams) {
8708
+ const paramName = queryParam.name;
8709
+ if (
8710
+ params[paramName] !== undefined &&
8711
+ !processedParams.has(paramName)
8712
+ ) {
8713
+ queryParams[paramName] = params[paramName];
8714
+ processedParams.add(paramName);
8715
+ }
8716
+ }
8717
+ }
8718
+ }
8719
+
8720
+ // Step 2: Add query parameters from current pattern
8721
+ if (parsedPattern.queryParams) {
8722
+
8723
+ for (const queryParam of parsedPattern.queryParams) {
8724
+ const paramName = queryParam.name;
8725
+ if (params[paramName] !== undefined && !processedParams.has(paramName)) {
8726
+ queryParams[paramName] = params[paramName];
8727
+ processedParams.add(paramName);
8728
+ }
8729
+ }
8730
+ }
8731
+
8732
+ // Step 3: Add remaining parameters (extra params) alphabetically
8733
+ const extraParams = [];
8734
+
8735
+ // Get all path parameter names to exclude them
8736
+ const pathParamNames = new Set(
8737
+ parsedPattern.segments.filter((s) => s.type === "param").map((s) => s.name),
8738
+ );
8739
+
8740
+ for (const [key, value] of Object.entries(params)) {
8741
+ if (
8742
+ !pathParamNames.has(key) &&
8743
+ !processedParams.has(key) &&
8744
+ value !== undefined
8745
+ ) {
8746
+ extraParams.push([key, value]);
8747
+ }
8748
+ }
8749
+
8750
+ // Sort extra params alphabetically for consistent order
8751
+ extraParams.sort(([a], [b]) => a.localeCompare(b));
8752
+
8753
+ // Add sorted extra params
8754
+ for (const [key, value] of extraParams) {
8755
+ queryParams[key] = value;
8756
+ }
8757
+
8758
+ return queryParams;
8759
+ };
8760
+
8166
8761
  /**
8167
8762
  * Build a URL from a pattern and parameters
8168
8763
  */
8169
- const buildUrlFromPattern = (parsedPattern, params = {}) => {
8764
+ const buildUrlFromPattern = (
8765
+ parsedPattern,
8766
+ params = {},
8767
+ originalPattern = null,
8768
+ ) => {
8170
8769
  if (parsedPattern.segments.length === 0) {
8171
8770
  // Root route
8172
- const searchParams = new URLSearchParams();
8771
+ const queryParams = {};
8173
8772
  for (const [key, value] of Object.entries(params)) {
8174
8773
  if (value !== undefined) {
8175
- searchParams.set(key, value);
8774
+ queryParams[key] = value;
8176
8775
  }
8177
8776
  }
8178
- const search = searchParams.toString();
8777
+ const search = buildQueryString(queryParams);
8179
8778
  return `/${search ? `?${search}` : ""}`;
8180
8779
  }
8181
8780
 
@@ -8189,7 +8788,7 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
8189
8788
 
8190
8789
  // If value is provided, include it
8191
8790
  if (value !== undefined) {
8192
- segments.push(encodeURIComponent(value));
8791
+ segments.push(encodeParamValue(value, false)); // Named parameters encode slashes
8193
8792
  } else if (!patternSeg.optional) {
8194
8793
  // For required parameters without values, keep the placeholder
8195
8794
  segments.push(`:${patternSeg.name}`);
@@ -8243,41 +8842,14 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
8243
8842
  path = path.slice(0, -1);
8244
8843
  }
8245
8844
 
8246
- // Add search parameters
8247
- const pathParamNames = new Set(
8248
- parsedPattern.segments.filter((s) => s.type === "param").map((s) => s.name),
8845
+ // Build query parameters respecting hierarchical order
8846
+ const queryParams = buildHierarchicalQueryParams(
8847
+ parsedPattern,
8848
+ params,
8849
+ originalPattern,
8249
8850
  );
8250
8851
 
8251
- // Add query parameters defined in the pattern first
8252
- const queryParamNames = new Set();
8253
- const searchParams = new URLSearchParams();
8254
-
8255
- // Handle pattern-defined query parameters (from ?tab, &lon, etc.)
8256
- if (parsedPattern.queryParams) {
8257
- for (const queryParam of parsedPattern.queryParams) {
8258
- const paramName = queryParam.name;
8259
- queryParamNames.add(paramName);
8260
-
8261
- const value = params[paramName];
8262
- if (value !== undefined) {
8263
- searchParams.set(paramName, value);
8264
- }
8265
- // If no value provided, don't add the parameter to keep URLs clean
8266
- }
8267
- }
8268
-
8269
- // Add remaining parameters as additional query parameters (excluding path and pattern query params)
8270
- for (const [key, value] of Object.entries(params)) {
8271
- if (
8272
- !pathParamNames.has(key) &&
8273
- !queryParamNames.has(key) &&
8274
- value !== undefined
8275
- ) {
8276
- searchParams.set(key, value);
8277
- }
8278
- }
8279
-
8280
- const search = searchParams.toString();
8852
+ const search = buildQueryString(queryParams);
8281
8853
 
8282
8854
  // No longer handle trailing slash inheritance here
8283
8855
 
@@ -8386,7 +8958,9 @@ const setupPatterns = (patternDefinitions) => {
8386
8958
  patternRegistry.clear();
8387
8959
  patternRelationships.clear();
8388
8960
 
8389
- // Phase 1: Register all patterns
8961
+ // Phase 1: Register all patterns and create pattern objects
8962
+ const patternObjects = new Map(); // pattern string -> pattern object
8963
+
8390
8964
  for (const [key, urlPatternRaw] of Object.entries(patternDefinitions)) {
8391
8965
  const [cleanPattern, connections] = detectSignals(urlPatternRaw);
8392
8966
  const parsedPattern = parsePattern(cleanPattern);
@@ -8402,6 +8976,10 @@ const setupPatterns = (patternDefinitions) => {
8402
8976
  };
8403
8977
 
8404
8978
  patternRegistry.set(urlPatternRaw, patternData);
8979
+
8980
+ // Create the full pattern object for this pattern
8981
+ const patternObj = createRoutePattern(urlPatternRaw);
8982
+ patternObjects.set(urlPatternRaw, patternObj);
8405
8983
  }
8406
8984
 
8407
8985
  // Phase 2: Build relationships between all patterns
@@ -8417,30 +8995,24 @@ const setupPatterns = (patternDefinitions) => {
8417
8995
 
8418
8996
  // Check if current pattern is a child of other pattern using clean patterns
8419
8997
  if (isChildPattern(currentData.cleanPattern, otherData.cleanPattern)) {
8420
- currentData.parentPatterns.push(otherPattern);
8421
- otherData.childPatterns.push(currentPattern);
8998
+ // Store pattern objects instead of pattern strings
8999
+ currentData.parentPatterns.push(patternObjects.get(otherPattern));
9000
+ otherData.childPatterns.push(patternObjects.get(currentPattern));
8422
9001
  }
8423
9002
  }
8424
9003
 
8425
- // Store relationships for easy access
9004
+ // Store relationships for easy access with pattern objects
8426
9005
  patternRelationships.set(currentPattern, {
8427
9006
  pattern: currentData.parsedPattern,
8428
9007
  parsedPattern: currentData.parsedPattern,
8429
9008
  connections: currentData.connections,
8430
- childPatterns: currentData.childPatterns, // Store child patterns
8431
- parentPatterns: currentData.parentPatterns, // Store parent patterns
9009
+ childPatterns: currentData.childPatterns, // Now contains pattern objects
9010
+ parentPatterns: currentData.parentPatterns, // Now contains pattern objects
8432
9011
  originalPattern: currentPattern,
8433
9012
  });
8434
9013
  }
8435
9014
  };
8436
9015
 
8437
- /**
8438
- * Get pattern data for a registered pattern
8439
- */
8440
- const getPatternData = (urlPatternRaw) => {
8441
- return patternRegistry.get(urlPatternRaw);
8442
- };
8443
-
8444
9016
  /**
8445
9017
  * Clear all registered patterns
8446
9018
  */
@@ -8687,8 +9259,7 @@ const getRoutePrivateProperties = (route) => {
8687
9259
 
8688
9260
  const registerRoute = (routePattern) => {
8689
9261
  const urlPatternRaw = routePattern.originalPattern;
8690
- const patternData = getPatternData(urlPatternRaw);
8691
- const { cleanPattern, connections } = patternData;
9262
+ const { cleanPattern, connections } = routePattern;
8692
9263
 
8693
9264
  const cleanupCallbackSet = new Set();
8694
9265
  const cleanup = () => {
@@ -8710,6 +9281,7 @@ const registerRoute = (routePattern) => {
8710
9281
  relativeUrl: null,
8711
9282
  url: null,
8712
9283
  action: null,
9284
+ specificity: routePattern.specificity, // Expose pattern specificity publicly
8713
9285
  cleanup,
8714
9286
  toString: () => {
8715
9287
  return `route "${cleanPattern}"`;
@@ -8872,16 +9444,22 @@ const registerRoute = (routePattern) => {
8872
9444
  }
8873
9445
  }
8874
9446
 
8875
- // Find the most specific route (the one with the longest pattern path)
9447
+ // Find the most specific route using pre-calculated specificity scores
8876
9448
  let mostSpecificRoute = route;
8877
- let maxSegments = route.pattern.split("/").filter((s) => s !== "").length;
9449
+ const routePrivateProperties = getRoutePrivateProperties(route);
9450
+ let maxSpecificity = routePrivateProperties?.routePattern?.specificity || 0;
8878
9451
 
8879
9452
  for (const matchingRoute of allMatchingRoutes) {
8880
- const segments = matchingRoute.pattern
8881
- .split("/")
8882
- .filter((s) => s !== "").length;
8883
- if (segments > maxSegments) {
8884
- maxSegments = segments;
9453
+ if (matchingRoute === route) {
9454
+ continue;
9455
+ }
9456
+ const matchingRoutePrivateProperties =
9457
+ getRoutePrivateProperties(matchingRoute);
9458
+ const specificity =
9459
+ matchingRoutePrivateProperties?.routePattern?.specificity || 0;
9460
+
9461
+ if (specificity > maxSpecificity) {
9462
+ maxSpecificity = specificity;
8885
9463
  mostSpecificRoute = matchingRoute;
8886
9464
  }
8887
9465
  }
@@ -18198,14 +18776,6 @@ const RouteLink = ({
18198
18776
  });
18199
18777
  };
18200
18778
 
18201
- const rawUrlPartSymbol = Symbol("raw_url_part");
18202
- const rawUrlPart = (value) => {
18203
- return {
18204
- [rawUrlPartSymbol]: true,
18205
- value,
18206
- };
18207
- };
18208
-
18209
18779
  installImportMetaCss(import.meta);Object.assign(PSEUDO_CLASSES, {
18210
18780
  ":-navi-tab-selected": {
18211
18781
  attribute: "data-tab-selected"
@@ -18541,12 +19111,12 @@ const TabRoute = ({
18541
19111
  expand: true,
18542
19112
  discrete: true,
18543
19113
  padding: padding,
19114
+ paddingX: paddingX,
19115
+ paddingY: paddingY,
18544
19116
  paddingLeft: paddingLeft,
18545
19117
  paddingRight: paddingRight,
18546
19118
  paddingTop: paddingTop,
18547
19119
  paddingBottom: paddingBottom,
18548
- paddingX: paddingX,
18549
- paddingY: paddingY,
18550
19120
  alignX: alignX,
18551
19121
  alignY: alignY,
18552
19122
  children: children