{"version":3,"file":"intercepting-routes.mjs","sources":["../../../../src/lib/routing/advanced/intercepting-routes.ts"],"sourcesContent":["/**\n * @file Intercepting Routes Support\n * @description Implements route interception patterns inspired by Next.js App Router.\n * Enables intercepting navigation to show content in a different context (e.g., modal).\n *\n * @module @/lib/routing/advanced/intercepting-routes\n *\n * This module provides:\n * - Route interception definitions\n * - Context-aware route rendering\n * - Modal/overlay pattern support\n * - Soft navigation handling\n * - Interception level configuration\n *\n * @example\n * ```typescript\n * import { createInterceptingRoute, InterceptionLevel } from '@/lib/routing/advanced/intercepting-routes';\n *\n * // Intercept /photo/[id] and show in modal when navigating from same route\n * const photoInterceptor = createInterceptingRoute({\n *   pattern: '/photo/:id',\n *   level: InterceptionLevel.SameLevel,\n *   interceptWith: PhotoModal,\n *   fallback: PhotoPage,\n * });\n * ```\n */\n\nimport type { ComponentType } from 'react';\nimport {\n  matchPathPattern as coreMatchPathPattern,\n  getPathDepth as coreGetPathDepth,\n} from '../core/path-utils';\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Interception level determines how far up the route tree to intercept\n *\n * - SameLevel (.) - Intercept from same route level\n * - OneUp (..) - Intercept from one level up\n * - TwoUp (...) - Intercept from two levels up\n * - Root (...) - Intercept from any level (root)\n */\nexport enum InterceptionLevel {\n  /** Intercept from same route level */\n  SameLevel = '.',\n  /** Intercept from one level up */\n  OneUp = '..',\n  /** Intercept from two levels up */\n  TwoUp = '...',\n  /** Intercept from root (any level) */\n  Root = '....',\n}\n\n/**\n * Intercepting route configuration\n */\nexport interface InterceptingRouteConfig<TProps = unknown> {\n  /** Route pattern to intercept */\n  readonly pattern: string;\n  /** Interception level */\n  readonly level: InterceptionLevel;\n  /** Component to render when intercepted */\n  readonly interceptWith: ComponentType<TProps & InterceptedRouteProps>;\n  /** Fallback component for direct navigation */\n  readonly fallback: ComponentType<TProps>;\n  /** Origins from which interception is allowed */\n  readonly allowedOrigins?: readonly string[];\n  /** Origins from which interception is denied */\n  readonly deniedOrigins?: readonly string[];\n  /** Custom condition for interception */\n  readonly shouldIntercept?: (context: InterceptionContext) => boolean;\n  /** Feature flag for this interception */\n  readonly featureFlag?: string;\n}\n\n/**\n * Props passed to intercepted route component\n */\nexport interface InterceptedRouteProps {\n  /** Whether route is currently intercepted */\n  readonly isIntercepted: boolean;\n  /** Original navigation context */\n  readonly interceptionContext: InterceptionContext;\n  /** Close the interception (return to origin) */\n  readonly closeInterception: () => void;\n  /** Navigate to the full page (break out of interception) */\n  readonly navigateToFull: () => void;\n}\n\n/**\n * Context for interception decision making\n */\nexport interface InterceptionContext {\n  /** Current URL path */\n  readonly currentPath: string;\n  /** Previous URL path (origin of navigation) */\n  readonly originPath: string;\n  /** Target URL path */\n  readonly targetPath: string;\n  /** Extracted route parameters */\n  readonly params: Record<string, string>;\n  /** Navigation trigger type */\n  readonly triggerType: NavigationTrigger;\n  /** Navigation state data */\n  readonly state?: unknown;\n  /** Whether this is a back/forward navigation */\n  readonly isPopState: boolean;\n}\n\n/**\n * Navigation trigger types\n */\nexport type NavigationTrigger =\n  | 'link'        // User clicked a link\n  | 'programmatic' // navigate() call\n  | 'popstate'    // Browser back/forward\n  | 'replace'     // replace() call\n  | 'external';   // External navigation\n\n/**\n * Interception resolution result\n */\nexport interface InterceptionResolution {\n  /** Whether route should be intercepted */\n  readonly shouldIntercept: boolean;\n  /** Component to render */\n  readonly component: ComponentType<unknown>;\n  /** Props for the component */\n  readonly props: InterceptedRouteProps | Record<string, unknown>;\n  /** Interception context if intercepted */\n  readonly context: InterceptionContext | null;\n  /** Reason if not intercepted */\n  readonly skipReason?: string;\n}\n\n/**\n * Registered intercepting route\n */\nexport interface RegisteredInterceptor {\n  /** Unique interceptor ID */\n  readonly id: string;\n  /** Route pattern */\n  readonly pattern: string;\n  /** Interception configuration */\n  readonly config: InterceptingRouteConfig;\n  /** Registration timestamp */\n  readonly registeredAt: number;\n}\n\n/**\n * Interception manager state\n */\nexport interface InterceptionManagerState {\n  /** Currently active interception */\n  readonly activeInterception: InterceptionContext | null;\n  /** Navigation history for interception tracking */\n  readonly history: readonly InterceptionContext[];\n  /** Registered interceptors */\n  readonly interceptors: readonly RegisteredInterceptor[];\n}\n\n// =============================================================================\n// InterceptingRouteManager Class\n// =============================================================================\n\n/**\n * Manages route interception logic\n *\n * @example\n * ```typescript\n * const manager = new InterceptingRouteManager();\n *\n * manager.register({\n *   pattern: '/photo/:id',\n *   level: InterceptionLevel.SameLevel,\n *   interceptWith: PhotoModal,\n *   fallback: PhotoPage,\n * });\n *\n * const resolution = manager.resolve({\n *   currentPath: '/gallery',\n *   originPath: '/gallery',\n *   targetPath: '/photo/123',\n *   params: { id: '123' },\n *   triggerType: 'link',\n *   isPopState: false,\n * });\n * ```\n */\nexport class InterceptingRouteManager {\n  private interceptors: Map<string, RegisteredInterceptor> = new Map();\n  private activeInterception: InterceptionContext | null = null;\n  private history: InterceptionContext[] = [];\n  private idCounter = 0;\n\n  /**\n   * Register an intercepting route\n   *\n   * @param config - Interception configuration\n   * @returns Interceptor ID for later reference\n   */\n  register(config: InterceptingRouteConfig): string {\n    const id = `interceptor_${++this.idCounter}`;\n    const interceptor: RegisteredInterceptor = {\n      id,\n      pattern: config.pattern,\n      config,\n      registeredAt: Date.now(),\n    };\n    this.interceptors.set(id, interceptor);\n    return id;\n  }\n\n  /**\n   * Unregister an intercepting route\n   *\n   * @param id - Interceptor ID\n   * @returns True if interceptor was found and removed\n   */\n  unregister(id: string): boolean {\n    return this.interceptors.delete(id);\n  }\n\n  /**\n   * Resolve whether a navigation should be intercepted\n   *\n   * @param context - Interception context\n   * @returns Resolution result\n   */\n  resolve(context: InterceptionContext): InterceptionResolution {\n    // Find matching interceptor\n    for (const interceptor of this.interceptors.values()) {\n      if (this.matchesPattern(interceptor.pattern, context.targetPath)) {\n        const shouldIntercept = this.shouldIntercept(interceptor.config, context);\n\n        if (shouldIntercept) {\n          const props: InterceptedRouteProps = {\n            isIntercepted: true,\n            interceptionContext: context,\n            closeInterception: () => this.closeInterception(),\n            navigateToFull: () => this.navigateToFull(context.targetPath),\n          };\n\n          this.activeInterception = context;\n          this.history.push(context);\n\n          return {\n            shouldIntercept: true,\n            component: interceptor.config.interceptWith as ComponentType<unknown>,\n            props,\n            context,\n          };\n        }\n\n        // Return fallback if pattern matches but interception condition fails\n        return {\n          shouldIntercept: false,\n          component: interceptor.config.fallback,\n          props: { ...context.params },\n          context: null,\n          skipReason: 'Interception condition not met',\n        };\n      }\n    }\n\n    // No matching interceptor\n    return {\n      shouldIntercept: false,\n      component: EmptyComponent,\n      props: {},\n      context: null,\n      skipReason: 'No matching interceptor',\n    };\n  }\n\n  /**\n   * Close the current interception\n   */\n  closeInterception(): void {\n    if (this.activeInterception) {\n      // Navigation would be handled by the consuming application\n      this.activeInterception = null;\n    }\n  }\n\n  /**\n   * Navigate to full page (break out of interception)\n   */\n  navigateToFull(_path: string): void {\n    this.activeInterception = null;\n    // Full navigation would be handled by the consuming application\n    // This would typically trigger a hard navigation or router.push\n\n  }\n\n  /**\n   * Get current interception state\n   */\n  getState(): InterceptionManagerState {\n    return {\n      activeInterception: this.activeInterception,\n      history: [...this.history],\n      interceptors: Array.from(this.interceptors.values()),\n    };\n  }\n\n  /**\n   * Check if currently in an interception\n   */\n  isIntercepted(): boolean {\n    return this.activeInterception !== null;\n  }\n\n  /**\n   * Get active interception context\n   */\n  getActiveInterception(): InterceptionContext | null {\n    return this.activeInterception;\n  }\n\n  /**\n   * Clear all interceptors\n   */\n  clearAll(): void {\n    this.interceptors.clear();\n    this.activeInterception = null;\n    this.history = [];\n  }\n\n  /**\n   * Get all registered interceptors\n   */\n  getInterceptors(): readonly RegisteredInterceptor[] {\n    return Array.from(this.interceptors.values());\n  }\n\n  /**\n   * Check if a path matches a pattern\n   *\n   * Delegates to core path matching utility.\n   */\n  private matchesPattern(pattern: string, path: string): boolean {\n    return coreMatchPathPattern(pattern, path);\n  }\n\n  /**\n   * Determine if navigation should be intercepted\n   */\n  private shouldIntercept(\n    config: InterceptingRouteConfig,\n    context: InterceptionContext\n  ): boolean {\n    // Don't intercept popstate (back/forward)\n    if (context.isPopState) {\n      return false;\n    }\n\n    // Check custom condition\n    if (config.shouldIntercept) {\n      return config.shouldIntercept(context);\n    }\n\n    // Check allowed/denied origins\n    if (config.deniedOrigins?.some(o => this.matchesPattern(o, context.originPath)) === true) {\n      return false;\n    }\n\n    if (config.allowedOrigins && config.allowedOrigins.length > 0) {\n      if (!config.allowedOrigins.some(o => this.matchesPattern(o, context.originPath))) {\n        return false;\n      }\n    }\n\n    // Check interception level\n    return this.checkInterceptionLevel(config.level, context.originPath, context.targetPath);\n  }\n\n  /**\n   * Check if interception level condition is met\n   *\n   * Uses core path depth utility.\n   */\n  private checkInterceptionLevel(\n    level: InterceptionLevel,\n    originPath: string,\n    targetPath: string\n  ): boolean {\n    const originDepth = coreGetPathDepth(originPath);\n    const targetDepth = coreGetPathDepth(targetPath);\n    const depthDiff = Math.abs(originDepth - targetDepth);\n\n    switch (level) {\n      case InterceptionLevel.SameLevel:\n        return depthDiff === 0 || depthDiff === 1;\n      case InterceptionLevel.OneUp:\n        return depthDiff <= 1;\n      case InterceptionLevel.TwoUp:\n        return depthDiff <= 2;\n      case InterceptionLevel.Root:\n        return true; // Always intercept\n      default:\n        return false;\n    }\n  }\n}\n\n// =============================================================================\n// Empty Component (for no-op rendering)\n// =============================================================================\n\n/**\n * Empty component for no-op rendering\n */\nfunction EmptyComponent(): null {\n  return null;\n}\n\n// =============================================================================\n// Factory Functions\n// =============================================================================\n\n/**\n * Create an intercepting route configuration\n *\n * @param config - Interception configuration\n * @returns Validated configuration\n */\nexport function createInterceptingRoute<TProps = unknown>(\n  config: InterceptingRouteConfig<TProps>\n): InterceptingRouteConfig<TProps> {\n  return {\n    ...config,\n    allowedOrigins: config.allowedOrigins ?? [],\n    deniedOrigins: config.deniedOrigins ?? [],\n  };\n}\n\n/**\n * Create an interception context from navigation event\n *\n * @param event - Navigation event details\n * @returns Interception context\n */\nexport function createInterceptionContext(event: {\n  currentPath: string;\n  originPath: string;\n  targetPath: string;\n  params?: Record<string, string>;\n  state?: unknown;\n  isPopState?: boolean;\n  triggerType?: NavigationTrigger;\n}): InterceptionContext {\n  return {\n    currentPath: event.currentPath,\n    originPath: event.originPath,\n    targetPath: event.targetPath,\n    params: event.params ?? {},\n    triggerType: event.triggerType ?? 'link',\n    state: event.state,\n    isPopState: event.isPopState ?? false,\n  };\n}\n\n// =============================================================================\n// Singleton Instance\n// =============================================================================\n\nlet defaultManager: InterceptingRouteManager | null = null;\n\n/**\n * Get the default interception manager\n */\nexport function getInterceptionManager(): InterceptingRouteManager {\n  defaultManager ??= new InterceptingRouteManager();\n  return defaultManager;\n}\n\n/**\n * Reset the default interception manager\n */\nexport function resetInterceptionManager(): void {\n  defaultManager?.clearAll();\n  defaultManager = null;\n}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Parse interception level from string notation\n *\n * @param notation - Dot notation (e.g., '.', '..', '...')\n * @returns Interception level\n */\nexport function parseInterceptionLevel(notation: string): InterceptionLevel {\n  switch (notation) {\n    case '.':\n      return InterceptionLevel.SameLevel;\n    case '..':\n      return InterceptionLevel.OneUp;\n    case '...':\n      return InterceptionLevel.TwoUp;\n    case '....':\n      return InterceptionLevel.Root;\n    default:\n      return InterceptionLevel.SameLevel;\n  }\n}\n\n/**\n * Get dot notation for interception level\n *\n * @param level - Interception level\n * @returns Dot notation string\n */\nexport function getInterceptionNotation(level: InterceptionLevel): string {\n  return level;\n}\n\n/**\n * Check if a route pattern contains interception markers\n *\n * @param pattern - Route pattern\n * @returns True if pattern has interception markers\n */\nexport function hasInterceptionMarker(pattern: string): boolean {\n  return /\\(\\.+\\)/.test(pattern);\n}\n\n/**\n * Extract interception level from route pattern\n *\n * @param pattern - Route pattern with interception marker\n * @returns Level and cleaned pattern, or null if no marker\n */\nexport function extractInterceptionFromPattern(\n  pattern: string\n): { level: InterceptionLevel; cleanPattern: string } | null {\n  const match = pattern.match(/\\((\\.+)\\)/);\n  if (match?.[1] == null || match[1] === '') {\n    return null;\n  }\n\n  const level = parseInterceptionLevel(match[1]);\n  const cleanPattern = pattern.replace(/\\(\\.+\\)/, '');\n\n  return { level, cleanPattern };\n}\n\n/**\n * Build intercepted route path\n *\n * @param basePath - Base path\n * @param level - Interception level\n * @param targetSegment - Target segment\n * @returns Intercepted route path\n */\nexport function buildInterceptedPath(\n  basePath: string,\n  level: InterceptionLevel,\n  targetSegment: string\n): string {\n  const notation = getInterceptionNotation(level);\n  const normalizedBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n  return `${normalizedBase}/(${notation})${targetSegment}`;\n}\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Type guard for InterceptionContext\n */\nexport function isInterceptionContext(value: unknown): value is InterceptionContext {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    'currentPath' in value &&\n    'originPath' in value &&\n    'targetPath' in value &&\n    'triggerType' in value\n  );\n}\n\n/**\n * Type guard for InterceptionLevel\n */\nexport function isInterceptionLevel(value: unknown): value is InterceptionLevel {\n  return Object.values(InterceptionLevel).includes(value as InterceptionLevel);\n}\n"],"names":["InterceptingRouteManager","config","id","interceptor","context","props","EmptyComponent","_path","pattern","path","coreMatchPathPattern","o","level","originPath","targetPath","originDepth","coreGetPathDepth","targetDepth","depthDiff","createInterceptingRoute"],"mappings":"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"}