🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@page-speed/router

Package Overview
Dependencies
Maintainers
2
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@page-speed/router - npm Package Compare versions

Comparing version
1.0.2
to
1.1.0
+40
dist/analytics/getDeviceType.cjs
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var getDeviceType_exports = {};
__export(getDeviceType_exports, {
getDeviceType: () => getDeviceType
});
module.exports = __toCommonJS(getDeviceType_exports);
var import_ssrSafe = require("../utils/ssrSafe");
const MOBILE_UA_PATTERN = /android|webos|iphone|ipad|ipod|blackberry|bb\d+|meego|iemobile|opera m(ob|in)i|windows phone|mobile/i;
function getDeviceType() {
if (!(0, import_ssrSafe.isBrowser)() || typeof navigator === "undefined") return "unknown";
try {
const ua = navigator.userAgent || "";
if (!ua) return "unknown";
return MOBILE_UA_PATTERN.test(ua) ? "mobile" : "desktop";
} catch {
return "unknown";
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
getDeviceType
});
//# sourceMappingURL=getDeviceType.cjs.map
{
"version": 3,
"sources": ["../../src/analytics/getDeviceType.ts"],
"sourcesContent": ["import { PageViewDevice } from '../types';\nimport { isBrowser } from '../utils/ssrSafe';\n\nconst MOBILE_UA_PATTERN =\n /android|webos|iphone|ipad|ipod|blackberry|bb\\d+|meego|iemobile|opera m(ob|in)i|windows phone|mobile/i;\n\n/**\n * Detects the visitor's device class from the user agent.\n * Matches the device enum accepted by the DashTrack analytics engine\n * ('mobile' | 'desktop' | 'unknown'); the backend coerces anything\n * else to 'unknown'.\n *\n * SSR-safe: returns 'unknown' outside the browser.\n */\nexport function getDeviceType(): PageViewDevice {\n if (!isBrowser() || typeof navigator === 'undefined') return 'unknown';\n\n try {\n const ua = navigator.userAgent || '';\n if (!ua) return 'unknown';\n return MOBILE_UA_PATTERN.test(ua) ? 'mobile' : 'desktop';\n } catch {\n return 'unknown';\n }\n}\n"],
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,qBAA0B;AAE1B,MAAM,oBACJ;AAUK,SAAS,gBAAgC;AAC9C,MAAI,KAAC,0BAAU,KAAK,OAAO,cAAc,YAAa,QAAO;AAE7D,MAAI;AACF,UAAM,KAAK,UAAU,aAAa;AAClC,QAAI,CAAC,GAAI,QAAO;AAChB,WAAO,kBAAkB,KAAK,EAAE,IAAI,WAAW;AAAA,EACjD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
"names": []
}
import { PageViewDevice } from '../types';
/**
* Detects the visitor's device class from the user agent.
* Matches the device enum accepted by the DashTrack analytics engine
* ('mobile' | 'desktop' | 'unknown'); the backend coerces anything
* else to 'unknown'.
*
* SSR-safe: returns 'unknown' outside the browser.
*/
export declare function getDeviceType(): PageViewDevice;
//# sourceMappingURL=getDeviceType.d.ts.map
{"version":3,"file":"getDeviceType.d.ts","sourceRoot":"","sources":["../../src/analytics/getDeviceType.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAM1C;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,cAAc,CAU9C"}
import { isBrowser } from '../utils/ssrSafe.js';
const MOBILE_UA_PATTERN = /android|webos|iphone|ipad|ipod|blackberry|bb\d+|meego|iemobile|opera m(ob|in)i|windows phone|mobile/i;
/**
* Detects the visitor's device class from the user agent.
* Matches the device enum accepted by the DashTrack analytics engine
* ('mobile' | 'desktop' | 'unknown'); the backend coerces anything
* else to 'unknown'.
*
* SSR-safe: returns 'unknown' outside the browser.
*/
export function getDeviceType() {
if (!isBrowser() || typeof navigator === 'undefined')
return 'unknown';
try {
const ua = navigator.userAgent || '';
if (!ua)
return 'unknown';
return MOBILE_UA_PATTERN.test(ua) ? 'mobile' : 'desktop';
}
catch {
return 'unknown';
}
}
//# sourceMappingURL=getDeviceType.js.map
{"version":3,"file":"getDeviceType.js","sourceRoot":"","sources":["../../src/analytics/getDeviceType.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,iBAAiB,GACrB,sGAAsG,CAAC;AAEzG;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,SAAS,EAAE,IAAI,OAAO,SAAS,KAAK,WAAW;QAAE,OAAO,SAAS,CAAC;IAEvE,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,SAAS,CAAC,SAAS,IAAI,EAAE,CAAC;QACrC,IAAI,CAAC,EAAE;YAAE,OAAO,SAAS,CAAC;QAC1B,OAAO,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var usePageViewAnalytics_exports = {};
__export(usePageViewAnalytics_exports, {
PageViewAnalytics: () => PageViewAnalytics,
usePageViewAnalytics: () => usePageViewAnalytics
});
module.exports = __toCommonJS(usePageViewAnalytics_exports);
var import_react = require("react");
var import_getDeviceType = require("./getDeviceType");
var import_usePageViews = require("./usePageViews");
const DEFAULT_API_BASE_URL = "https://api.dashtrack.com";
const ENDPOINT_PATH = "/website_page_views";
function normalizePathname(path) {
if (path.length > 1 && path.endsWith("/")) {
return path.replace(/\/+$/, "") || "/";
}
return path;
}
function sendPageView(apiBaseUrl, payload) {
try {
if (typeof fetch !== "function") return;
fetch(`${apiBaseUrl.replace(/\/+$/, "")}${ENDPOINT_PATH}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
// The endpoint requires the payload nested under `website_page_view`.
// Nest explicitly rather than relying on Rails wrap_parameters.
body: JSON.stringify({ website_page_view: payload }),
keepalive: true,
credentials: "omit",
mode: "cors"
}).catch(() => void 0);
} catch {
}
}
function usePageViewAnalytics(options) {
const {
websiteToken,
apiBaseUrl = DEFAULT_API_BASE_URL,
category = "webpage",
enabled = true,
transformPayload
} = options;
const optionsRef = (0, import_react.useRef)({ websiteToken, apiBaseUrl, category, transformPayload });
optionsRef.current = { websiteToken, apiBaseUrl, category, transformPayload };
(0, import_usePageViews.usePageViews)(
(view) => {
const current = optionsRef.current;
if (!current.websiteToken) return;
const payload = {
path: normalizePathname(view.path),
// IMPORTANT: the wire param is named website_id, but the analytics
// endpoint resolves it via websites.token (the UUID token), NOT the
// numeric websites.id.
website_id: current.websiteToken,
category: current.category,
device: (0, import_getDeviceType.getDeviceType)(),
traffic_source: typeof document !== "undefined" && document.referrer ? document.referrer : void 0
};
const finalPayload = current.transformPayload ? current.transformPayload(payload, view) : payload;
if (!finalPayload) return;
sendPageView(current.apiBaseUrl, finalPayload);
},
{ enabled: Boolean(enabled && websiteToken) }
);
}
function PageViewAnalytics(options) {
usePageViewAnalytics(options);
return null;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
PageViewAnalytics,
usePageViewAnalytics
});
//# sourceMappingURL=usePageViewAnalytics.cjs.map
{
"version": 3,
"sources": ["../../src/analytics/usePageViewAnalytics.ts"],
"sourcesContent": ["import { useRef } from 'react';\nimport {\n PageViewAnalyticsOptions,\n PageViewAnalyticsPayload,\n} from '../types';\nimport { getDeviceType } from './getDeviceType';\nimport { usePageViews } from './usePageViews';\n\nconst DEFAULT_API_BASE_URL = 'https://api.dashtrack.com';\nconst ENDPOINT_PATH = '/website_page_views';\n\n/**\n * Pages are stored under canonical no-trailing-slash slugs ('/about'), but\n * remain reachable at '/about/' \u2014 normalize so attribution always matches.\n */\nfunction normalizePathname(path: string): string {\n if (path.length > 1 && path.endsWith('/')) {\n return path.replace(/\\/+$/, '') || '/';\n }\n return path;\n}\n\n/**\n * Fire-and-forget delivery. Analytics must never break the host site, so\n * every failure mode (no fetch, network error, non-2xx) is swallowed.\n * `keepalive` lets in-flight requests survive a full page unload.\n */\nfunction sendPageView(\n apiBaseUrl: string,\n payload: PageViewAnalyticsPayload\n): void {\n try {\n if (typeof fetch !== 'function') return;\n\n fetch(`${apiBaseUrl.replace(/\\/+$/, '')}${ENDPOINT_PATH}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n // The endpoint requires the payload nested under `website_page_view`.\n // Nest explicitly rather than relying on Rails wrap_parameters.\n body: JSON.stringify({ website_page_view: payload }),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n }).catch(() => undefined);\n } catch {\n // Never propagate analytics failures.\n }\n}\n\n/**\n * Hook that reports page views to the DashTrack analytics engine\n * (`POST /website_page_views`): one for the initial load, then one per\n * client-side navigation.\n *\n * Opt-in: consumers that never call this hook are completely unaffected\n * (tree-shaken away). No-ops during SSR, when `enabled` is false, or when\n * `websiteToken` is absent.\n *\n * The visitor IP is intentionally not sent \u2014 the backend derives it from\n * the request. `traffic_source` defaults to `document.referrer`.\n *\n * @example\n * ```tsx\n * usePageViewAnalytics({ websiteToken: website.token });\n * ```\n */\nexport function usePageViewAnalytics(options: PageViewAnalyticsOptions): void {\n const {\n websiteToken,\n apiBaseUrl = DEFAULT_API_BASE_URL,\n category = 'webpage',\n enabled = true,\n transformPayload,\n } = options;\n\n // Latest values without re-subscribing the underlying listeners.\n const optionsRef = useRef({ websiteToken, apiBaseUrl, category, transformPayload });\n optionsRef.current = { websiteToken, apiBaseUrl, category, transformPayload };\n\n usePageViews(\n (view) => {\n const current = optionsRef.current;\n if (!current.websiteToken) return;\n\n const payload: PageViewAnalyticsPayload = {\n path: normalizePathname(view.path),\n // IMPORTANT: the wire param is named website_id, but the analytics\n // endpoint resolves it via websites.token (the UUID token), NOT the\n // numeric websites.id.\n website_id: current.websiteToken,\n category: current.category,\n device: getDeviceType(),\n traffic_source:\n typeof document !== 'undefined' && document.referrer\n ? document.referrer\n : undefined,\n };\n\n const finalPayload = current.transformPayload\n ? current.transformPayload(payload, view)\n : payload;\n\n if (!finalPayload) return;\n\n sendPageView(current.apiBaseUrl, finalPayload);\n },\n { enabled: Boolean(enabled && websiteToken) }\n );\n}\n\n/**\n * Declarative wrapper around `usePageViewAnalytics`. Renders nothing.\n * Drop it anywhere in the tree (inside or outside RouterProvider):\n *\n * @example\n * ```tsx\n * <RouterProvider>\n * <PageViewAnalytics websiteToken={website.token} />\n * <App />\n * </RouterProvider>\n * ```\n */\nexport function PageViewAnalytics(options: PageViewAnalyticsOptions): null {\n usePageViewAnalytics(options);\n return null;\n}\n"],
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAuB;AAKvB,2BAA8B;AAC9B,0BAA6B;AAE7B,MAAM,uBAAuB;AAC7B,MAAM,gBAAgB;AAMtB,SAAS,kBAAkB,MAAsB;AAC/C,MAAI,KAAK,SAAS,KAAK,KAAK,SAAS,GAAG,GAAG;AACzC,WAAO,KAAK,QAAQ,QAAQ,EAAE,KAAK;AAAA,EACrC;AACA,SAAO;AACT;AAOA,SAAS,aACP,YACA,SACM;AACN,MAAI;AACF,QAAI,OAAO,UAAU,WAAY;AAEjC,UAAM,GAAG,WAAW,QAAQ,QAAQ,EAAE,CAAC,GAAG,aAAa,IAAI;AAAA,MACzD,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA;AAAA;AAAA,MAG9C,MAAM,KAAK,UAAU,EAAE,mBAAmB,QAAQ,CAAC;AAAA,MACnD,WAAW;AAAA,MACX,aAAa;AAAA,MACb,MAAM;AAAA,IACR,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,EAC1B,QAAQ;AAAA,EAER;AACF;AAmBO,SAAS,qBAAqB,SAAyC;AAC5E,QAAM;AAAA,IACJ;AAAA,IACA,aAAa;AAAA,IACb,WAAW;AAAA,IACX,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAGJ,QAAM,iBAAa,qBAAO,EAAE,cAAc,YAAY,UAAU,iBAAiB,CAAC;AAClF,aAAW,UAAU,EAAE,cAAc,YAAY,UAAU,iBAAiB;AAE5E;AAAA,IACE,CAAC,SAAS;AACR,YAAM,UAAU,WAAW;AAC3B,UAAI,CAAC,QAAQ,aAAc;AAE3B,YAAM,UAAoC;AAAA,QACxC,MAAM,kBAAkB,KAAK,IAAI;AAAA;AAAA;AAAA;AAAA,QAIjC,YAAY,QAAQ;AAAA,QACpB,UAAU,QAAQ;AAAA,QAClB,YAAQ,oCAAc;AAAA,QACtB,gBACE,OAAO,aAAa,eAAe,SAAS,WACxC,SAAS,WACT;AAAA,MACR;AAEA,YAAM,eAAe,QAAQ,mBACzB,QAAQ,iBAAiB,SAAS,IAAI,IACtC;AAEJ,UAAI,CAAC,aAAc;AAEnB,mBAAa,QAAQ,YAAY,YAAY;AAAA,IAC/C;AAAA,IACA,EAAE,SAAS,QAAQ,WAAW,YAAY,EAAE;AAAA,EAC9C;AACF;AAcO,SAAS,kBAAkB,SAAyC;AACzE,uBAAqB,OAAO;AAC5B,SAAO;AACT;",
"names": []
}
import { PageViewAnalyticsOptions } from '../types';
/**
* Hook that reports page views to the DashTrack analytics engine
* (`POST /website_page_views`): one for the initial load, then one per
* client-side navigation.
*
* Opt-in: consumers that never call this hook are completely unaffected
* (tree-shaken away). No-ops during SSR, when `enabled` is false, or when
* `websiteToken` is absent.
*
* The visitor IP is intentionally not sent — the backend derives it from
* the request. `traffic_source` defaults to `document.referrer`.
*
* @example
* ```tsx
* usePageViewAnalytics({ websiteToken: website.token });
* ```
*/
export declare function usePageViewAnalytics(options: PageViewAnalyticsOptions): void;
/**
* Declarative wrapper around `usePageViewAnalytics`. Renders nothing.
* Drop it anywhere in the tree (inside or outside RouterProvider):
*
* @example
* ```tsx
* <RouterProvider>
* <PageViewAnalytics websiteToken={website.token} />
* <App />
* </RouterProvider>
* ```
*/
export declare function PageViewAnalytics(options: PageViewAnalyticsOptions): null;
//# sourceMappingURL=usePageViewAnalytics.d.ts.map
{"version":3,"file":"usePageViewAnalytics.d.ts","sourceRoot":"","sources":["../../src/analytics/usePageViewAnalytics.ts"],"names":[],"mappings":"AACA,OAAO,EACL,wBAAwB,EAEzB,MAAM,UAAU,CAAC;AA6ClB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA0C5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAGzE"}
import { useRef } from 'react';
import { getDeviceType } from './getDeviceType.js';
import { usePageViews } from './usePageViews.js';
const DEFAULT_API_BASE_URL = 'https://api.dashtrack.com';
const ENDPOINT_PATH = '/website_page_views';
/**
* Pages are stored under canonical no-trailing-slash slugs ('/about'), but
* remain reachable at '/about/' — normalize so attribution always matches.
*/
function normalizePathname(path) {
if (path.length > 1 && path.endsWith('/')) {
return path.replace(/\/+$/, '') || '/';
}
return path;
}
/**
* Fire-and-forget delivery. Analytics must never break the host site, so
* every failure mode (no fetch, network error, non-2xx) is swallowed.
* `keepalive` lets in-flight requests survive a full page unload.
*/
function sendPageView(apiBaseUrl, payload) {
try {
if (typeof fetch !== 'function')
return;
fetch(`${apiBaseUrl.replace(/\/+$/, '')}${ENDPOINT_PATH}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// The endpoint requires the payload nested under `website_page_view`.
// Nest explicitly rather than relying on Rails wrap_parameters.
body: JSON.stringify({ website_page_view: payload }),
keepalive: true,
credentials: 'omit',
mode: 'cors',
}).catch(() => undefined);
}
catch {
// Never propagate analytics failures.
}
}
/**
* Hook that reports page views to the DashTrack analytics engine
* (`POST /website_page_views`): one for the initial load, then one per
* client-side navigation.
*
* Opt-in: consumers that never call this hook are completely unaffected
* (tree-shaken away). No-ops during SSR, when `enabled` is false, or when
* `websiteToken` is absent.
*
* The visitor IP is intentionally not sent — the backend derives it from
* the request. `traffic_source` defaults to `document.referrer`.
*
* @example
* ```tsx
* usePageViewAnalytics({ websiteToken: website.token });
* ```
*/
export function usePageViewAnalytics(options) {
const { websiteToken, apiBaseUrl = DEFAULT_API_BASE_URL, category = 'webpage', enabled = true, transformPayload, } = options;
// Latest values without re-subscribing the underlying listeners.
const optionsRef = useRef({ websiteToken, apiBaseUrl, category, transformPayload });
optionsRef.current = { websiteToken, apiBaseUrl, category, transformPayload };
usePageViews((view) => {
const current = optionsRef.current;
if (!current.websiteToken)
return;
const payload = {
path: normalizePathname(view.path),
// IMPORTANT: the wire param is named website_id, but the analytics
// endpoint resolves it via websites.token (the UUID token), NOT the
// numeric websites.id.
website_id: current.websiteToken,
category: current.category,
device: getDeviceType(),
traffic_source: typeof document !== 'undefined' && document.referrer
? document.referrer
: undefined,
};
const finalPayload = current.transformPayload
? current.transformPayload(payload, view)
: payload;
if (!finalPayload)
return;
sendPageView(current.apiBaseUrl, finalPayload);
}, { enabled: Boolean(enabled && websiteToken) });
}
/**
* Declarative wrapper around `usePageViewAnalytics`. Renders nothing.
* Drop it anywhere in the tree (inside or outside RouterProvider):
*
* @example
* ```tsx
* <RouterProvider>
* <PageViewAnalytics websiteToken={website.token} />
* <App />
* </RouterProvider>
* ```
*/
export function PageViewAnalytics(options) {
usePageViewAnalytics(options);
return null;
}
//# sourceMappingURL=usePageViewAnalytics.js.map
{"version":3,"file":"usePageViewAnalytics.js","sourceRoot":"","sources":["../../src/analytics/usePageViewAnalytics.ts"],"names":[],"mappings":"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"}
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var usePageViews_exports = {};
__export(usePageViews_exports, {
usePageViews: () => usePageViews
});
module.exports = __toCommonJS(usePageViews_exports);
var import_react = require("react");
var import_ssrSafe = require("../utils/ssrSafe");
function usePageViews(onPageView, options = {}) {
const { enabled = true } = options;
const callbackRef = (0, import_react.useRef)(onPageView);
callbackRef.current = onPageView;
const lastPathRef = (0, import_react.useRef)(null);
(0, import_react.useEffect)(() => {
if (!enabled || !(0, import_ssrSafe.isBrowser)()) return;
const track = (isInitial) => {
const path = window.location.pathname;
if (lastPathRef.current === path) return;
const previousPath = lastPathRef.current;
lastPathRef.current = path;
callbackRef.current({ path, previousPath, isInitial });
};
track(true);
const handleNavigation = () => track(false);
window.addEventListener("popstate", handleNavigation);
window.addEventListener("routechange", handleNavigation);
return () => {
window.removeEventListener("popstate", handleNavigation);
window.removeEventListener("routechange", handleNavigation);
};
}, [enabled]);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
usePageViews
});
//# sourceMappingURL=usePageViews.cjs.map
{
"version": 3,
"sources": ["../../src/analytics/usePageViews.ts"],
"sourcesContent": ["import { useEffect, useRef } from 'react';\nimport { PageView, UsePageViewsOptions } from '../types';\nimport { isBrowser } from '../utils/ssrSafe';\n\n/**\n * Hook that observes page views: fires once for the initial load, then on\n * every client-side navigation (programmatic `routechange` and browser\n * back/forward `popstate`).\n *\n * Analytics-backend agnostic \u2014 pair it with any tracking call. For the\n * DashTrack analytics engine, use `usePageViewAnalytics` /\n * `<PageViewAnalytics />` instead, which are built on this hook.\n *\n * Consecutive events for the same pathname are deduped: `navigateTo`\n * dispatches both `popstate` and `routechange` for a single navigation, and\n * hash-only changes keep the same pathname \u2014 neither produces a duplicate\n * page view.\n *\n * Works outside of RouterProvider and is SSR-safe (no-ops on the server).\n *\n * @example\n * ```tsx\n * usePageViews(({ path, isInitial }) => {\n * myAnalytics.track('page_view', { path, isInitial });\n * });\n * ```\n */\nexport function usePageViews(\n onPageView: (view: PageView) => void,\n options: UsePageViewsOptions = {}\n): void {\n const { enabled = true } = options;\n\n // Keep the latest callback without re-subscribing listeners on each render.\n const callbackRef = useRef(onPageView);\n callbackRef.current = onPageView;\n\n // Survives re-runs of the effect so toggling `enabled` or remounting the\n // effect never double-fires the current path.\n const lastPathRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!enabled || !isBrowser()) return;\n\n const track = (isInitial: boolean) => {\n const path = window.location.pathname;\n if (lastPathRef.current === path) return;\n\n const previousPath = lastPathRef.current;\n lastPathRef.current = path;\n callbackRef.current({ path, previousPath, isInitial });\n };\n\n track(true);\n\n const handleNavigation = () => track(false);\n window.addEventListener('popstate', handleNavigation);\n window.addEventListener('routechange', handleNavigation);\n\n return () => {\n window.removeEventListener('popstate', handleNavigation);\n window.removeEventListener('routechange', handleNavigation);\n };\n }, [enabled]);\n}\n"],
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAkC;AAElC,qBAA0B;AAyBnB,SAAS,aACd,YACA,UAA+B,CAAC,GAC1B;AACN,QAAM,EAAE,UAAU,KAAK,IAAI;AAG3B,QAAM,kBAAc,qBAAO,UAAU;AACrC,cAAY,UAAU;AAItB,QAAM,kBAAc,qBAAsB,IAAI;AAE9C,8BAAU,MAAM;AACd,QAAI,CAAC,WAAW,KAAC,0BAAU,EAAG;AAE9B,UAAM,QAAQ,CAAC,cAAuB;AACpC,YAAM,OAAO,OAAO,SAAS;AAC7B,UAAI,YAAY,YAAY,KAAM;AAElC,YAAM,eAAe,YAAY;AACjC,kBAAY,UAAU;AACtB,kBAAY,QAAQ,EAAE,MAAM,cAAc,UAAU,CAAC;AAAA,IACvD;AAEA,UAAM,IAAI;AAEV,UAAM,mBAAmB,MAAM,MAAM,KAAK;AAC1C,WAAO,iBAAiB,YAAY,gBAAgB;AACpD,WAAO,iBAAiB,eAAe,gBAAgB;AAEvD,WAAO,MAAM;AACX,aAAO,oBAAoB,YAAY,gBAAgB;AACvD,aAAO,oBAAoB,eAAe,gBAAgB;AAAA,IAC5D;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AACd;",
"names": []
}
import { PageView, UsePageViewsOptions } from '../types';
/**
* Hook that observes page views: fires once for the initial load, then on
* every client-side navigation (programmatic `routechange` and browser
* back/forward `popstate`).
*
* Analytics-backend agnostic — pair it with any tracking call. For the
* DashTrack analytics engine, use `usePageViewAnalytics` /
* `<PageViewAnalytics />` instead, which are built on this hook.
*
* Consecutive events for the same pathname are deduped: `navigateTo`
* dispatches both `popstate` and `routechange` for a single navigation, and
* hash-only changes keep the same pathname — neither produces a duplicate
* page view.
*
* Works outside of RouterProvider and is SSR-safe (no-ops on the server).
*
* @example
* ```tsx
* usePageViews(({ path, isInitial }) => {
* myAnalytics.track('page_view', { path, isInitial });
* });
* ```
*/
export declare function usePageViews(onPageView: (view: PageView) => void, options?: UsePageViewsOptions): void;
//# sourceMappingURL=usePageViews.d.ts.map
{"version":3,"file":"usePageViews.d.ts","sourceRoot":"","sources":["../../src/analytics/usePageViews.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAGzD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,EACpC,OAAO,GAAE,mBAAwB,GAChC,IAAI,CAkCN"}
import { useEffect, useRef } from 'react';
import { isBrowser } from '../utils/ssrSafe.js';
/**
* Hook that observes page views: fires once for the initial load, then on
* every client-side navigation (programmatic `routechange` and browser
* back/forward `popstate`).
*
* Analytics-backend agnostic — pair it with any tracking call. For the
* DashTrack analytics engine, use `usePageViewAnalytics` /
* `<PageViewAnalytics />` instead, which are built on this hook.
*
* Consecutive events for the same pathname are deduped: `navigateTo`
* dispatches both `popstate` and `routechange` for a single navigation, and
* hash-only changes keep the same pathname — neither produces a duplicate
* page view.
*
* Works outside of RouterProvider and is SSR-safe (no-ops on the server).
*
* @example
* ```tsx
* usePageViews(({ path, isInitial }) => {
* myAnalytics.track('page_view', { path, isInitial });
* });
* ```
*/
export function usePageViews(onPageView, options = {}) {
const { enabled = true } = options;
// Keep the latest callback without re-subscribing listeners on each render.
const callbackRef = useRef(onPageView);
callbackRef.current = onPageView;
// Survives re-runs of the effect so toggling `enabled` or remounting the
// effect never double-fires the current path.
const lastPathRef = useRef(null);
useEffect(() => {
if (!enabled || !isBrowser())
return;
const track = (isInitial) => {
const path = window.location.pathname;
if (lastPathRef.current === path)
return;
const previousPath = lastPathRef.current;
lastPathRef.current = path;
callbackRef.current({ path, previousPath, isInitial });
};
track(true);
const handleNavigation = () => track(false);
window.addEventListener('popstate', handleNavigation);
window.addEventListener('routechange', handleNavigation);
return () => {
window.removeEventListener('popstate', handleNavigation);
window.removeEventListener('routechange', handleNavigation);
};
}, [enabled]);
}
//# sourceMappingURL=usePageViews.js.map
{"version":3,"file":"usePageViews.js","sourceRoot":"","sources":["../../src/analytics/usePageViews.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE1C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,YAAY,CAC1B,UAAoC,EACpC,UAA+B,EAAE;IAEjC,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAEnC,4EAA4E;IAC5E,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACvC,WAAW,CAAC,OAAO,GAAG,UAAU,CAAC;IAEjC,yEAAyE;IACzE,8CAA8C;IAC9C,MAAM,WAAW,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IAEhD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS,EAAE;YAAE,OAAO;QAErC,MAAM,KAAK,GAAG,CAAC,SAAkB,EAAE,EAAE;YACnC,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACtC,IAAI,WAAW,CAAC,OAAO,KAAK,IAAI;gBAAE,OAAO;YAEzC,MAAM,YAAY,GAAG,WAAW,CAAC,OAAO,CAAC;YACzC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;YAC3B,WAAW,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC;QACzD,CAAC,CAAC;QAEF,KAAK,CAAC,IAAI,CAAC,CAAC;QAEZ,MAAM,gBAAgB,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC;QACtD,MAAM,CAAC,gBAAgB,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAC;QAEzD,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC;YACzD,MAAM,CAAC,mBAAmB,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAC;QAC9D,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;AAChB,CAAC"}
+1
-1

@@ -1,2 +0,2 @@

!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OpensiteHooks={},t.React)}(this,function(t,e){"use strict";const n=e.createContext(null);n.displayName="RouterContext";const o=()=>"undefined"!=typeof window&&void 0!==window.document;function r(t,e){return o()?t():e}const a={href:"",origin:"",protocol:"https:",host:"",hostname:"",port:"",pathname:"/",search:"",hash:""};function i(){const[t,n]=e.useState(()=>r(()=>({href:window.location.href,origin:window.location.origin,protocol:window.location.protocol,host:window.location.host,hostname:window.location.hostname,port:window.location.port,pathname:window.location.pathname,search:window.location.search,hash:window.location.hash}),a));return e.useEffect(()=>{if(!o())return;const t=()=>{n({href:window.location.href,origin:window.location.origin,protocol:window.location.protocol,host:window.location.host,hostname:window.location.hostname,port:window.location.port,pathname:window.location.pathname,search:window.location.search,hash:window.location.hash})};t(),window.addEventListener("popstate",t),window.addEventListener("hashchange",t);const e=()=>t();return window.addEventListener("routechange",e),()=>{window.removeEventListener("popstate",t),window.removeEventListener("hashchange",t),window.removeEventListener("routechange",e)}},[]),t}function s(t,e=!0,n=20){if(!o())return;const r=t.replace(/^#/,""),a=document.getElementById(r);if(!a){const t=document.querySelector(`[id="${r}"]`);return t?void c(t,e,n):void console.warn(`[page-speed/router] Anchor element not found: #${r}`)}c(a,e,n)}function c(t,e,n){const o=t.getBoundingClientRect().top+window.pageYOffset-n;window.scrollTo({top:o,behavior:e?"smooth":"auto"});const r=t.id;if(r&&window.history.replaceState){const t=window.history.state;window.history.replaceState(t,"",`#${r}`)}e?setTimeout(()=>{if(t.focus({preventScroll:!0}),document.activeElement!==t){const e=t.getAttribute("tabindex");t.setAttribute("tabindex","-1"),t.focus({preventScroll:!0}),t.addEventListener("blur",()=>{null===e?t.removeAttribute("tabindex"):t.setAttribute("tabindex",e)},{once:!0})}},500):t.focus({preventScroll:!0})}function u(t=!0){o()&&window.scrollTo({top:0,behavior:t?"smooth":"auto"})}function l(){const t=e.useContext(n),r=(null==t?void 0:t.scrollBehavior)||"smooth",a=e.useCallback(t=>{if(!o())return;const e="string"==typeof t?{path:t}:t,{path:n,anchor:a,replace:i=!1,smooth:c="smooth"===r,state:l}=e;if(!n&&a)return void s(a,c);let h=n;a&&(h=`${n}#${a}`);const d=window.location.pathname,w=window.location.hash;if(d===n&&w!==(a?`#${a}`:""))return window.history[i?"replaceState":"pushState"](l||window.history.state,"",h),a&&s(a,c),void window.dispatchEvent(new CustomEvent("routechange",{detail:{path:h}}));i?window.history.replaceState(l||null,"",h):window.history.pushState(l||null,"",h),window.dispatchEvent(new PopStateEvent("popstate",{state:l})),window.dispatchEvent(new CustomEvent("routechange",{detail:{path:h}})),a?setTimeout(()=>s(a,c),0):d!==n&&u(c)},[r]),i=e.useCallback(t=>{a({path:t,replace:!0})},[a]),c=e.useCallback(()=>{o()&&window.location.reload()},[]);return{navigateTo:a,replace:i,reload:c}}function h(t={}){const{fallback:n="/",delta:r=-1}=t,{navigateTo:a}=l(),i=e.useRef(o()?window.history.length:0),s=e.useRef(0);e.useEffect(()=>{o()&&(0===s.current&&(s.current=window.history.length),i.current=window.history.length)},[]);const c=o()&&window.history.length>s.current;return{goBack:e.useCallback(t=>{if(!o())return;const e=t??r;if(window.history.length>Math.abs(e))try{window.history.go(e)}catch(i){console.warn("[page-speed/router] Failed to go back:",i),a({path:n,replace:!0})}else a({path:n,replace:!0})},[r,n,a]),canGoBack:c}}const d=new Map;function w(t,e,n=!1){const o={isMatch:!1,params:{},path:t};if("/"===e&&n)return o.isMatch="/"===t,o;const r=function(t,e){const n=`${t}:${e}`;if(d.has(n))return d.get(n);let o=t.replace(/[.+*?^${}()|[\]\\]/g,"\\$&").replace(/:(\w+)/g,"(?<$1>[^/]+)");o=e?`^${o}$`:`^${o}`;const r=new RegExp(o);return d.set(n,r),r}(e,n),a=t.match(r);return a&&(o.isMatch=!0,a.groups&&(o.params=a.groups)),o}function f(t){const e={},n=t.startsWith("?")?t.slice(1):t;if(!n)return e;return new URLSearchParams(n).forEach((t,n)=>{e[n]=t}),e}function p(t,e){return{...t,...e}}function m(t=!1){const o=e.useContext(n),{pathname:r,search:a}=i();return e.useMemo(()=>{if(!o||!o.routes||0===o.routes.length)return t&&a?f(a):{};const e=o.routes;let n={};for(const t of e){const e=w(r,t.path,t.exact);if(e.isMatch){n=e.params;break}}if(t&&a){return p(n,f(a))}return n},[o,r,a,t])}function g(t,n=!1){const{pathname:o}=i();return e.useMemo(()=>w(o,t,n),[o,t,n])}t.RouterContext=n,t.RouterProvider=function({children:t,initialPath:r,routes:a=[],scrollBehavior:i="smooth",onNavigate:s}){const c=e.useMemo(()=>({routes:a,initialPath:r,scrollBehavior:i,onNavigate:s}),[a,r,i,s]),u=e.useCallback(()=>{o()&&s&&s(window.location.pathname)},[s]);return e.useEffect(()=>{if(!o()||!s)return;r&&window.location.pathname===r&&s(r);const t=()=>{u()},e=t=>{var e;(null==(e=t.detail)?void 0:e.path)&&s(t.detail.path)};return window.addEventListener("popstate",t),window.addEventListener("routechange",e),()=>{window.removeEventListener("popstate",t),window.removeEventListener("routechange",e)}},[r,s,u]),e.createElement(n.Provider,{value:c},t)},t.browserOnly=function(t){return(...e)=>{if(o())return t(...e)}},t.buildPath=function(t,e){let n=t;return Object.entries(e).forEach(([t,e])=>{n=n.replace(`:${t}`,String(e))}),n},t.extractParams=function(t,e){return w(t,e,!0).params},t.findMatchingRoute=function(t,e){for(const n of e){const e=w(t,n.path,n.exact);if(e.isMatch)return e}return null},t.getScrollPosition=function(){return o()&&(window.pageYOffset||document.documentElement.scrollTop)||0},t.isBrowser=o,t.isSSR=()=>!o(),t.matchPath=w,t.mergeParams=p,t.normalizePath=function(t){return"/"===t?t:t.replace(/\/+$/,"")},t.parseParams=function(t,e){const n={},o=e.split("/").filter(Boolean),r=t.split("/").filter(Boolean);return o.length!==r.length||o.forEach((t,e)=>{if(t.startsWith(":")){const o=t.slice(1);n[o]=decodeURIComponent(r[e])}}),n},t.parseQueryString=f,t.safeWindow=function(t,e){if(o()&&window)try{return t(window)}catch{return e}return e},t.scrollToAnchor=s,t.scrollToTop=u,t.serializeParams=function(t){const e=new URLSearchParams;return Object.entries(t).forEach(([t,n])=>{null!=n&&e.append(t,String(n))}),e.toString()},t.ssrSafe=r,t.useAllParams=function(){const t=e.useContext(n),{pathname:o,search:r}=i();return e.useMemo(()=>{let e={};if(null==t?void 0:t.routes)for(const n of t.routes){const t=w(o,n.path,n.exact);if(t.isMatch){e=t.params;break}}return{routeParams:e,queryParams:r?f(r):{}}},[t,o,r])},t.useBack=function(t="/"){const{goBack:e}=h({fallback:t});return e},t.useGoBack=h,t.useGoForward=function(){return e.useCallback(()=>{if(o())try{window.history.forward()}catch(t){console.warn("[page-speed/router] Failed to go forward:",t)}},[])},t.useHash=function(){const{hash:t}=i();return t.replace(/^#/,"")},t.useIsActive=function(t,e=!1){return g(t,e).isMatch},t.useMultiMatch=function(t){const{pathname:n}=i();return e.useMemo(()=>{for(const{pattern:e,exact:o=!1}of t){const t=w(n,e,o);if(t.isMatch)return t}return null},[n,t])},t.useNavigate=function(){const{navigateTo:t}=l();return t},t.useNavigation=l,t.useParam=function(t){return m()[t]},t.useParams=m,t.usePathname=function(){const{pathname:t}=i();return t},t.useRouteMatch=g,t.useSearchParams=function(){const{search:t}=i(),e={};if(o()&&t){new URLSearchParams(t).forEach((t,n)=>{e[n]=t})}return e},t.useUpdateSearchParams=function(){const{navigateTo:t}=l();return e.useCallback((e,n=!1)=>{if(!o())return;const r=new URLSearchParams(window.location.search);Object.entries(e).forEach(([t,e])=>{null===e?r.delete(t):r.set(t,e)});const a=r.toString(),i=`${window.location.pathname}${a?`?${a}`:""}`;t({path:i,replace:n})},[t])},t.useUrl=i,t.validateParams=function(t,e){return e.every(e=>e in t&&""!==t[e])},Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).OpensiteHooks={},e.React)}(this,function(e,t){"use strict";const n=t.createContext(null);n.displayName="RouterContext";const o=()=>"undefined"!=typeof window&&void 0!==window.document;function r(e,t){return o()?e():t}const a={href:"",origin:"",protocol:"https:",host:"",hostname:"",port:"",pathname:"/",search:"",hash:""};function i(){const[e,n]=t.useState(()=>r(()=>({href:window.location.href,origin:window.location.origin,protocol:window.location.protocol,host:window.location.host,hostname:window.location.hostname,port:window.location.port,pathname:window.location.pathname,search:window.location.search,hash:window.location.hash}),a));return t.useEffect(()=>{if(!o())return;const e=()=>{n({href:window.location.href,origin:window.location.origin,protocol:window.location.protocol,host:window.location.host,hostname:window.location.hostname,port:window.location.port,pathname:window.location.pathname,search:window.location.search,hash:window.location.hash})};e(),window.addEventListener("popstate",e),window.addEventListener("hashchange",e);const t=()=>e();return window.addEventListener("routechange",t),()=>{window.removeEventListener("popstate",e),window.removeEventListener("hashchange",e),window.removeEventListener("routechange",t)}},[]),e}function s(e,t=!0,n=20){if(!o())return;const r=e.replace(/^#/,""),a=document.getElementById(r);if(!a){const e=document.querySelector(`[id="${r}"]`);return e?void c(e,t,n):void console.warn(`[page-speed/router] Anchor element not found: #${r}`)}c(a,t,n)}function c(e,t,n){const o=e.getBoundingClientRect().top+window.pageYOffset-n;window.scrollTo({top:o,behavior:t?"smooth":"auto"});const r=e.id;if(r&&window.history.replaceState){const e=window.history.state;window.history.replaceState(e,"",`#${r}`)}t?setTimeout(()=>{if(e.focus({preventScroll:!0}),document.activeElement!==e){const t=e.getAttribute("tabindex");e.setAttribute("tabindex","-1"),e.focus({preventScroll:!0}),e.addEventListener("blur",()=>{null===t?e.removeAttribute("tabindex"):e.setAttribute("tabindex",t)},{once:!0})}},500):e.focus({preventScroll:!0})}function u(e=!0){o()&&window.scrollTo({top:0,behavior:e?"smooth":"auto"})}function l(){const e=t.useContext(n),r=(null==e?void 0:e.scrollBehavior)||"smooth",a=t.useCallback(e=>{if(!o())return;const t="string"==typeof e?{path:e}:e,{path:n,anchor:a,replace:i=!1,smooth:c="smooth"===r,state:l}=t;if(!n&&a)return void s(a,c);let h=n;a&&(h=`${n}#${a}`);const d=window.location.pathname,w=window.location.hash;if(d===n&&w!==(a?`#${a}`:""))return window.history[i?"replaceState":"pushState"](l||window.history.state,"",h),a&&s(a,c),void window.dispatchEvent(new CustomEvent("routechange",{detail:{path:h}}));i?window.history.replaceState(l||null,"",h):window.history.pushState(l||null,"",h),window.dispatchEvent(new PopStateEvent("popstate",{state:l})),window.dispatchEvent(new CustomEvent("routechange",{detail:{path:h}})),a?setTimeout(()=>s(a,c),0):d!==n&&u(c)},[r]),i=t.useCallback(e=>{a({path:e,replace:!0})},[a]),c=t.useCallback(()=>{o()&&window.location.reload()},[]);return{navigateTo:a,replace:i,reload:c}}function h(e={}){const{fallback:n="/",delta:r=-1}=e,{navigateTo:a}=l(),i=t.useRef(o()?window.history.length:0),s=t.useRef(0);t.useEffect(()=>{o()&&(0===s.current&&(s.current=window.history.length),i.current=window.history.length)},[]);const c=o()&&window.history.length>s.current;return{goBack:t.useCallback(e=>{if(!o())return;const t=e??r;if(window.history.length>Math.abs(t))try{window.history.go(t)}catch(i){console.warn("[page-speed/router] Failed to go back:",i),a({path:n,replace:!0})}else a({path:n,replace:!0})},[r,n,a]),canGoBack:c}}const d=new Map;function w(e,t,n=!1){const o={isMatch:!1,params:{},path:e};if("/"===t&&n)return o.isMatch="/"===e,o;const r=function(e,t){const n=`${e}:${t}`;if(d.has(n))return d.get(n);let o=e.replace(/[.+*?^${}()|[\]\\]/g,"\\$&").replace(/:(\w+)/g,"(?<$1>[^/]+)");o=t?`^${o}$`:`^${o}`;const r=new RegExp(o);return d.set(n,r),r}(t,n),a=e.match(r);return a&&(o.isMatch=!0,a.groups&&(o.params=a.groups)),o}function f(e){const t={},n=e.startsWith("?")?e.slice(1):e;if(!n)return t;return new URLSearchParams(n).forEach((e,n)=>{t[n]=e}),t}function p(e,t){return{...e,...t}}function m(e=!1){const o=t.useContext(n),{pathname:r,search:a}=i();return t.useMemo(()=>{if(!o||!o.routes||0===o.routes.length)return e&&a?f(a):{};const t=o.routes;let n={};for(const e of t){const t=w(r,e.path,e.exact);if(t.isMatch){n=t.params;break}}if(e&&a){return p(n,f(a))}return n},[o,r,a,e])}function g(e,n=!1){const{pathname:o}=i();return t.useMemo(()=>w(o,e,n),[o,e,n])}function v(e,n={}){const{enabled:r=!0}=n,a=t.useRef(e);a.current=e;const i=t.useRef(null);t.useEffect(()=>{if(!r||!o())return;const e=e=>{const t=window.location.pathname;if(i.current===t)return;const n=i.current;i.current=t,a.current({path:t,previousPath:n,isInitial:e})};e(!0);const t=()=>e(!1);return window.addEventListener("popstate",t),window.addEventListener("routechange",t),()=>{window.removeEventListener("popstate",t),window.removeEventListener("routechange",t)}},[r])}const b=/android|webos|iphone|ipad|ipod|blackberry|bb\d+|meego|iemobile|opera m(ob|in)i|windows phone|mobile/i;function y(){if(!o()||"undefined"==typeof navigator)return"unknown";try{const e=navigator.userAgent||"";return e?b.test(e)?"mobile":"desktop":"unknown"}catch{return"unknown"}}const P="https://api.dashtrack.com";function E(e){const{websiteToken:n,apiBaseUrl:o=P,category:r="webpage",enabled:a=!0,transformPayload:i}=e,s=t.useRef({websiteToken:n,apiBaseUrl:o,category:r,transformPayload:i});s.current={websiteToken:n,apiBaseUrl:o,category:r,transformPayload:i},v(e=>{const t=s.current;if(!t.websiteToken)return;const n={path:(o=e.path,o.length>1&&o.endsWith("/")?o.replace(/\/+$/,"")||"/":o),website_id:t.websiteToken,category:t.category,device:y(),traffic_source:"undefined"!=typeof document&&document.referrer?document.referrer:void 0};var o;const r=t.transformPayload?t.transformPayload(n,e):n;r&&function(e,t){try{if("function"!=typeof fetch)return;fetch(`${e.replace(/\/+$/,"")}/website_page_views`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({website_page_view:t}),keepalive:!0,credentials:"omit",mode:"cors"}).catch(()=>{})}catch{}}(t.apiBaseUrl,r)},{enabled:Boolean(a&&n)})}e.PageViewAnalytics=function(e){return E(e),null},e.RouterContext=n,e.RouterProvider=function({children:e,initialPath:r,routes:a=[],scrollBehavior:i="smooth",onNavigate:s}){const c=t.useMemo(()=>({routes:a,initialPath:r,scrollBehavior:i,onNavigate:s}),[a,r,i,s]),u=t.useCallback(()=>{o()&&s&&s(window.location.pathname)},[s]);return t.useEffect(()=>{if(!o()||!s)return;r&&window.location.pathname===r&&s(r);const e=()=>{u()},t=e=>{var t;(null==(t=e.detail)?void 0:t.path)&&s(e.detail.path)};return window.addEventListener("popstate",e),window.addEventListener("routechange",t),()=>{window.removeEventListener("popstate",e),window.removeEventListener("routechange",t)}},[r,s,u]),t.createElement(n.Provider,{value:c},e)},e.browserOnly=function(e){return(...t)=>{if(o())return e(...t)}},e.buildPath=function(e,t){let n=e;return Object.entries(t).forEach(([e,t])=>{n=n.replace(`:${e}`,String(t))}),n},e.extractParams=function(e,t){return w(e,t,!0).params},e.findMatchingRoute=function(e,t){for(const n of t){const t=w(e,n.path,n.exact);if(t.isMatch)return t}return null},e.getDeviceType=y,e.getScrollPosition=function(){return o()&&(window.pageYOffset||document.documentElement.scrollTop)||0},e.isBrowser=o,e.isSSR=()=>!o(),e.matchPath=w,e.mergeParams=p,e.normalizePath=function(e){return"/"===e?e:e.replace(/\/+$/,"")},e.parseParams=function(e,t){const n={},o=t.split("/").filter(Boolean),r=e.split("/").filter(Boolean);return o.length!==r.length||o.forEach((e,t)=>{if(e.startsWith(":")){const o=e.slice(1);n[o]=decodeURIComponent(r[t])}}),n},e.parseQueryString=f,e.safeWindow=function(e,t){if(o()&&window)try{return e(window)}catch{return t}return t},e.scrollToAnchor=s,e.scrollToTop=u,e.serializeParams=function(e){const t=new URLSearchParams;return Object.entries(e).forEach(([e,n])=>{null!=n&&t.append(e,String(n))}),t.toString()},e.ssrSafe=r,e.useAllParams=function(){const e=t.useContext(n),{pathname:o,search:r}=i();return t.useMemo(()=>{let t={};if(null==e?void 0:e.routes)for(const n of e.routes){const e=w(o,n.path,n.exact);if(e.isMatch){t=e.params;break}}return{routeParams:t,queryParams:r?f(r):{}}},[e,o,r])},e.useBack=function(e="/"){const{goBack:t}=h({fallback:e});return t},e.useGoBack=h,e.useGoForward=function(){return t.useCallback(()=>{if(o())try{window.history.forward()}catch(e){console.warn("[page-speed/router] Failed to go forward:",e)}},[])},e.useHash=function(){const{hash:e}=i();return e.replace(/^#/,"")},e.useIsActive=function(e,t=!1){return g(e,t).isMatch},e.useMultiMatch=function(e){const{pathname:n}=i();return t.useMemo(()=>{for(const{pattern:t,exact:o=!1}of e){const e=w(n,t,o);if(e.isMatch)return e}return null},[n,e])},e.useNavigate=function(){const{navigateTo:e}=l();return e},e.useNavigation=l,e.usePageViewAnalytics=E,e.usePageViews=v,e.useParam=function(e){return m()[e]},e.useParams=m,e.usePathname=function(){const{pathname:e}=i();return e},e.useRouteMatch=g,e.useSearchParams=function(){const{search:e}=i(),t={};if(o()&&e){new URLSearchParams(e).forEach((e,n)=>{t[n]=e})}return t},e.useUpdateSearchParams=function(){const{navigateTo:e}=l();return t.useCallback((t,n=!1)=>{if(!o())return;const r=new URLSearchParams(window.location.search);Object.entries(t).forEach(([e,t])=>{null===t?r.delete(e):r.set(e,t)});const a=r.toString(),i=`${window.location.pathname}${a?`?${a}`:""}`;e({path:i,replace:n})},[e])},e.useUrl=i,e.validateParams=function(e,t){return t.every(t=>t in e&&""!==e[t])},Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})});
//# sourceMappingURL=page-speed-router.umd.js.map

@@ -1,1 +0,1 @@

{"version":3,"file":"page-speed-router.umd.js","sources":["../../src/context/RouterContext.tsx","../../src/utils/ssrSafe.ts","../../src/hooks/useUrl.ts","../../src/utils/scrollToAnchor.ts","../../src/hooks/useNavigation.ts","../../src/hooks/useGoBack.ts","../../src/utils/matchPath.ts","../../src/utils/parseParams.ts","../../src/hooks/useParams.ts","../../src/hooks/useRouteMatch.ts","../../src/context/RouterProvider.tsx"],"sourcesContent":["import { createContext } from 'react';\nimport { RouterContextValue } from '../types';\n\n/**\n * Context for sharing router state across the component tree\n * This context is optional - many hooks work without it\n */\nexport const RouterContext = createContext<RouterContextValue | null>(null);\n\n/**\n * Display name for React DevTools\n */\nRouterContext.displayName = 'RouterContext';","/**\n * SSR-safe utilities for handling server and client environments\n */\n\n/**\n * Checks if code is running in a browser environment\n */\nexport const isBrowser = (): boolean =>\n typeof window !== 'undefined' &&\n typeof window.document !== 'undefined';\n\n/**\n * Checks if code is running in a server environment\n */\nexport const isSSR = (): boolean => !isBrowser();\n\n/**\n * Executes a client-side function if in browser, otherwise returns server value\n * @param clientFn - Function to execute on client\n * @param serverValue - Default value to use on server\n * @returns The result of clientFn on client, serverValue on server\n */\nexport function ssrSafe<T>(clientFn: () => T, serverValue: T): T {\n return isBrowser() ? clientFn() : serverValue;\n}\n\n/**\n * Guards a function to only run in browser environment\n * @param fn - Function to guard\n * @returns A function that only runs in browser\n */\nexport function browserOnly<T extends (...args: any[]) => any>(\n fn: T\n): (...args: Parameters<T>) => ReturnType<T> | undefined {\n return (...args: Parameters<T>): ReturnType<T> | undefined => {\n if (isBrowser()) {\n return fn(...args);\n }\n return undefined;\n };\n}\n\n/**\n * Safely access window properties with fallback\n * @param accessor - Function that accesses window property\n * @param fallback - Fallback value if window is undefined\n */\nexport function safeWindow<T>(accessor: (window: Window) => T, fallback: T): T {\n if (isBrowser() && window) {\n try {\n return accessor(window);\n } catch {\n return fallback;\n }\n }\n return fallback;\n}","import { useState, useEffect } from 'react';\nimport { UrlState } from '../types';\nimport { ssrSafe, isBrowser } from '../utils/ssrSafe';\n\n/**\n * Default URL state for SSR environments\n */\nconst DEFAULT_URL: UrlState = {\n href: '',\n origin: '',\n protocol: 'https:',\n host: '',\n hostname: '',\n port: '',\n pathname: '/',\n search: '',\n hash: '',\n};\n\n/**\n * Hook that provides current URL information\n * Works outside of RouterProvider - uses browser APIs directly\n * SSR-safe with appropriate defaults\n *\n * @returns Current URL state\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { pathname, search, hash } = useUrl();\n *\n * return (\n * <div>\n * Current path: {pathname}\n * Query: {search}\n * Hash: {hash}\n * </div>\n * );\n * }\n * ```\n */\nexport function useUrl(): UrlState {\n const [url, setUrl] = useState<UrlState>(() =>\n ssrSafe(\n () => ({\n href: window.location.href,\n origin: window.location.origin,\n protocol: window.location.protocol,\n host: window.location.host,\n hostname: window.location.hostname,\n port: window.location.port,\n pathname: window.location.pathname,\n search: window.location.search,\n hash: window.location.hash,\n }),\n DEFAULT_URL\n )\n );\n\n useEffect(() => {\n if (!isBrowser()) return;\n\n const updateUrl = () => {\n setUrl({\n href: window.location.href,\n origin: window.location.origin,\n protocol: window.location.protocol,\n host: window.location.host,\n hostname: window.location.hostname,\n port: window.location.port,\n pathname: window.location.pathname,\n search: window.location.search,\n hash: window.location.hash,\n });\n };\n\n // Initial sync after hydration\n updateUrl();\n\n // Listen for navigation events\n window.addEventListener('popstate', updateUrl);\n window.addEventListener('hashchange', updateUrl);\n\n // Custom event for programmatic navigation\n const handleRouteChange = () => updateUrl();\n window.addEventListener('routechange', handleRouteChange);\n\n return () => {\n window.removeEventListener('popstate', updateUrl);\n window.removeEventListener('hashchange', updateUrl);\n window.removeEventListener('routechange', handleRouteChange);\n };\n }, []);\n\n return url;\n}\n\n/**\n * Hook that provides just the current pathname\n * Convenience wrapper around useUrl\n *\n * @returns Current pathname\n */\nexport function usePathname(): string {\n const { pathname } = useUrl();\n return pathname;\n}\n\n/**\n * Hook that provides query string parameters as an object\n * Convenience wrapper around useUrl\n *\n * @returns Parsed query parameters\n */\nexport function useSearchParams(): Record<string, string> {\n const { search } = useUrl();\n const params: Record<string, string> = {};\n\n if (isBrowser() && search) {\n const searchParams = new URLSearchParams(search);\n searchParams.forEach((value, key) => {\n params[key] = value;\n });\n }\n\n return params;\n}\n\n/**\n * Hook that provides the current hash (without #)\n * Convenience wrapper around useUrl\n *\n * @returns Current hash value\n */\nexport function useHash(): string {\n const { hash } = useUrl();\n return hash.replace(/^#/, '');\n}","import { isBrowser } from './ssrSafe';\n\n/**\n * Default offset for scroll positioning (in pixels)\n * Provides a small amount of padding from the top of the viewport\n */\nconst DEFAULT_SCROLL_OFFSET = 20;\n\n/**\n * Smoothly scrolls to an anchor element on the page\n * @param anchor - The ID of the element to scroll to (with or without #)\n * @param smooth - Whether to use smooth scrolling animation\n * @param offset - Offset from the top of the viewport in pixels\n */\nexport function scrollToAnchor(\n anchor: string,\n smooth: boolean = true,\n offset: number = DEFAULT_SCROLL_OFFSET\n): void {\n if (!isBrowser()) return;\n\n // Remove leading # if present\n const id = anchor.replace(/^#/, '');\n\n // Try to find the element\n const element = document.getElementById(id);\n\n if (!element) {\n // Try querySelector as fallback (for complex selectors)\n const fallbackElement = document.querySelector(`[id=\"${id}\"]`);\n if (!fallbackElement) {\n console.warn(`[page-speed/router] Anchor element not found: #${id}`);\n return;\n }\n scrollToElement(fallbackElement as HTMLElement, smooth, offset);\n return;\n }\n\n scrollToElement(element, smooth, offset);\n}\n\n/**\n * Scrolls to a specific element\n * @param element - The element to scroll to\n * @param smooth - Whether to use smooth scrolling\n * @param offset - Offset from the top in pixels\n */\nfunction scrollToElement(\n element: HTMLElement,\n smooth: boolean,\n offset: number\n): void {\n const elementPosition = element.getBoundingClientRect().top;\n const offsetPosition = elementPosition + window.pageYOffset - offset;\n\n // Perform the scroll\n window.scrollTo({\n top: offsetPosition,\n behavior: smooth ? 'smooth' : 'auto',\n });\n\n // Update URL hash without jumping\n const id = element.id;\n if (id && window.history.replaceState) {\n // Preserve existing state\n const currentState = window.history.state;\n window.history.replaceState(currentState, '', `#${id}`);\n }\n\n // Set focus for accessibility (after scroll completes)\n if (smooth) {\n // Wait for smooth scroll to complete (approximate)\n setTimeout(() => {\n element.focus({ preventScroll: true });\n\n // If element can't receive focus, add tabindex temporarily\n if (document.activeElement !== element) {\n const originalTabIndex = element.getAttribute('tabindex');\n element.setAttribute('tabindex', '-1');\n element.focus({ preventScroll: true });\n\n // Remove temporary tabindex on blur\n element.addEventListener('blur', () => {\n if (originalTabIndex === null) {\n element.removeAttribute('tabindex');\n } else {\n element.setAttribute('tabindex', originalTabIndex);\n }\n }, { once: true });\n }\n }, 500);\n } else {\n element.focus({ preventScroll: true });\n }\n}\n\n/**\n * Scrolls to the top of the page\n * @param smooth - Whether to use smooth scrolling\n */\nexport function scrollToTop(smooth: boolean = true): void {\n if (!isBrowser()) return;\n\n window.scrollTo({\n top: 0,\n behavior: smooth ? 'smooth' : 'auto',\n });\n}\n\n/**\n * Gets the current scroll position\n * @returns The current Y scroll position\n */\nexport function getScrollPosition(): number {\n if (!isBrowser()) return 0;\n return window.pageYOffset || document.documentElement.scrollTop || 0;\n}","import { useCallback, useContext } from 'react';\nimport { NavigationAPI, NavigateOptions } from '../types';\nimport { scrollToAnchor, scrollToTop } from '../utils/scrollToAnchor';\nimport { isBrowser } from '../utils/ssrSafe';\nimport { RouterContext } from '../context/RouterContext';\n\n/**\n * Hook that provides navigation functions\n * Works outside of RouterProvider - uses browser APIs directly\n *\n * @returns Navigation API with navigateTo, replace, and reload functions\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { navigateTo, replace, reload } = useNavigation();\n *\n * const handleClick = () => {\n * navigateTo({ path: '/about' });\n * };\n *\n * const handleAnchorClick = () => {\n * navigateTo({ path: '/blog', anchor: 'comments' });\n * };\n *\n * return (\n * <div>\n * <button onClick={handleClick}>Go to About</button>\n * <button onClick={handleAnchorClick}>Go to Blog Comments</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useNavigation(): NavigationAPI {\n const context = useContext(RouterContext);\n const scrollBehavior = context?.scrollBehavior || 'smooth';\n\n const navigateTo = useCallback(\n (options: NavigateOptions | string) => {\n if (!isBrowser()) return;\n\n // Handle string shorthand\n const navOptions: NavigateOptions =\n typeof options === 'string' ? { path: options } : options;\n\n const {\n path,\n anchor,\n replace = false,\n smooth = scrollBehavior === 'smooth',\n state,\n } = navOptions;\n\n // Handle anchor-only navigation on same page\n if (!path && anchor) {\n scrollToAnchor(anchor, smooth);\n return;\n }\n\n // Build full URL\n let fullPath = path;\n if (anchor) {\n fullPath = `${path}#${anchor}`;\n }\n\n // Check if it's the same path (anchor navigation only)\n const currentPath = window.location.pathname;\n const currentHash = window.location.hash;\n const newHash = anchor ? `#${anchor}` : '';\n\n if (currentPath === path && currentHash !== newHash) {\n // Same path, different anchor - just update hash and scroll\n window.history[replace ? 'replaceState' : 'pushState'](\n state || window.history.state,\n '',\n fullPath\n );\n\n if (anchor) {\n scrollToAnchor(anchor, smooth);\n }\n\n // Dispatch custom event\n window.dispatchEvent(\n new CustomEvent('routechange', { detail: { path: fullPath } })\n );\n return;\n }\n\n // Update history\n if (replace) {\n window.history.replaceState(state || null, '', fullPath);\n } else {\n window.history.pushState(state || null, '', fullPath);\n }\n\n // Dispatch events to trigger updates\n window.dispatchEvent(new PopStateEvent('popstate', { state }));\n window.dispatchEvent(\n new CustomEvent('routechange', { detail: { path: fullPath } })\n );\n\n // Handle scrolling\n if (anchor) {\n // Use setTimeout to ensure DOM is updated\n setTimeout(() => scrollToAnchor(anchor, smooth), 0);\n } else if (currentPath !== path) {\n // Scroll to top on regular navigation (different page)\n scrollToTop(smooth);\n }\n },\n [scrollBehavior]\n );\n\n const replace = useCallback(\n (path: string) => {\n navigateTo({ path, replace: true });\n },\n [navigateTo]\n );\n\n const reload = useCallback(() => {\n if (!isBrowser()) return;\n window.location.reload();\n }, []);\n\n return { navigateTo, replace, reload };\n}\n\n/**\n * Hook that provides a simple navigation function\n * Convenience wrapper around useNavigation\n *\n * @returns Navigate function\n */\nexport function useNavigate() {\n const { navigateTo } = useNavigation();\n return navigateTo;\n}\n\n/**\n * Hook that provides a function to update query parameters\n *\n * @returns Function to update query params\n */\nexport function useUpdateSearchParams() {\n const { navigateTo } = useNavigation();\n\n return useCallback(\n (params: Record<string, string | null>, replace = false) => {\n if (!isBrowser()) return;\n\n const currentParams = new URLSearchParams(window.location.search);\n\n // Update or remove params\n Object.entries(params).forEach(([key, value]) => {\n if (value === null) {\n currentParams.delete(key);\n } else {\n currentParams.set(key, value);\n }\n });\n\n const search = currentParams.toString();\n const newPath = `${window.location.pathname}${search ? `?${search}` : ''}`;\n\n navigateTo({ path: newPath, replace });\n },\n [navigateTo]\n );\n}","import { useCallback, useRef, useEffect } from 'react';\nimport { GoBackOptions, GoBackAPI } from '../types';\nimport { useNavigation } from './useNavigation';\nimport { isBrowser } from '../utils/ssrSafe';\n\n/**\n * Hook that provides safe back navigation with fallback\n *\n * @param options - Configuration options\n * @param options.fallback - Path to navigate to if no history exists (default: '/')\n * @param options.delta - Number of steps to go back (default: -1)\n * @returns Go back API with goBack function and canGoBack flag\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { goBack, canGoBack } = useGoBack({ fallback: '/home' });\n *\n * return (\n * <button\n * onClick={() => goBack()}\n * disabled={!canGoBack}\n * >\n * Go Back\n * </button>\n * );\n * }\n * ```\n */\nexport function useGoBack(options: GoBackOptions = {}): GoBackAPI {\n const { fallback = '/', delta = -1 } = options;\n const { navigateTo } = useNavigation();\n const historyLength = useRef<number>(isBrowser() ? window.history.length : 0);\n const initialLength = useRef<number>(0);\n\n useEffect(() => {\n if (!isBrowser()) return;\n\n // Store initial history length\n if (initialLength.current === 0) {\n initialLength.current = window.history.length;\n }\n\n // Update current length\n historyLength.current = window.history.length;\n }, []);\n\n // Track if we can go back\n const canGoBack = isBrowser() && window.history.length > initialLength.current;\n\n const goBack = useCallback(\n (customDelta?: number) => {\n if (!isBrowser()) return;\n\n const steps = customDelta ?? delta;\n\n // Check if we have enough history to go back\n if (window.history.length > Math.abs(steps)) {\n try {\n window.history.go(steps);\n } catch (error) {\n console.warn('[page-speed/router] Failed to go back:', error);\n // Fall back to navigation\n navigateTo({ path: fallback, replace: true });\n }\n } else {\n // No sufficient history, use fallback\n navigateTo({ path: fallback, replace: true });\n }\n },\n [delta, fallback, navigateTo]\n );\n\n return { goBack, canGoBack };\n}\n\n/**\n * Hook that provides a simple back function\n * Convenience wrapper around useGoBack\n *\n * @param fallback - Fallback path if no history\n * @returns Go back function\n */\nexport function useBack(fallback: string = '/') {\n const { goBack } = useGoBack({ fallback });\n return goBack;\n}\n\n/**\n * Hook that provides forward navigation\n *\n * @returns Go forward function\n */\nexport function useGoForward() {\n return useCallback(() => {\n if (!isBrowser()) return;\n\n try {\n window.history.forward();\n } catch (error) {\n console.warn('[page-speed/router] Failed to go forward:', error);\n }\n }, []);\n}","import { PathMatch, RouteParams } from '../types';\n\n/**\n * Cache for compiled regex patterns to avoid recompilation\n */\nconst patternCache = new Map<string, RegExp>();\n\n/**\n * Matches a pathname against a route pattern\n * @param pathname - The current pathname to match\n * @param pattern - The route pattern (e.g., '/blog/:slug')\n * @param exact - Whether to require an exact match\n * @returns Match result with extracted parameters\n */\nexport function matchPath(\n pathname: string,\n pattern: string,\n exact: boolean = false\n): PathMatch {\n const result: PathMatch = {\n isMatch: false,\n params: {},\n path: pathname,\n };\n\n // Handle root path edge case\n if (pattern === '/' && exact) {\n result.isMatch = pathname === '/';\n return result;\n }\n\n // Get or create regex for this pattern\n const regex = getPatternRegex(pattern, exact);\n const match = pathname.match(regex);\n\n if (match) {\n result.isMatch = true;\n\n // Extract named groups as params\n if (match.groups) {\n result.params = match.groups;\n }\n }\n\n return result;\n}\n\n/**\n * Gets or creates a regex for a route pattern\n * @param pattern - The route pattern\n * @param exact - Whether to match exactly\n * @returns Compiled RegExp\n */\nfunction getPatternRegex(pattern: string, exact: boolean): RegExp {\n const cacheKey = `${pattern}:${exact}`;\n\n if (patternCache.has(cacheKey)) {\n return patternCache.get(cacheKey)!;\n }\n\n // Escape special regex characters except for param markers\n let regexPattern = pattern\n // Escape special characters\n .replace(/[.+*?^${}()|[\\]\\\\]/g, '\\\\$&')\n // Replace :param with named capture groups\n .replace(/:(\\w+)/g, '(?<$1>[^/]+)');\n\n // Add anchors based on exact match requirement\n if (exact) {\n regexPattern = `^${regexPattern}$`;\n } else {\n regexPattern = `^${regexPattern}`;\n }\n\n const regex = new RegExp(regexPattern);\n patternCache.set(cacheKey, regex);\n\n return regex;\n}\n\n/**\n * Extracts route parameters from a path given a pattern\n * @param pathname - The current pathname\n * @param pattern - The route pattern with parameters\n * @returns Extracted parameters\n */\nexport function extractParams(pathname: string, pattern: string): RouteParams {\n const match = matchPath(pathname, pattern, true);\n return match.params;\n}\n\n/**\n * Checks if a path matches any of the provided patterns\n * @param pathname - The path to check\n * @param patterns - Array of patterns to match against\n * @returns The first matching pattern and its params, or null\n */\nexport function findMatchingRoute(\n pathname: string,\n patterns: { path: string; exact?: boolean }[]\n): PathMatch | null {\n for (const route of patterns) {\n const match = matchPath(pathname, route.path, route.exact);\n if (match.isMatch) {\n return match;\n }\n }\n return null;\n}\n\n/**\n * Builds a path from a pattern and params\n * @param pattern - The route pattern\n * @param params - The parameters to substitute\n * @returns The built path\n */\nexport function buildPath(pattern: string, params: RouteParams): string {\n let path = pattern;\n\n Object.entries(params).forEach(([key, value]) => {\n path = path.replace(`:${key}`, String(value));\n });\n\n return path;\n}\n\n/**\n * Normalizes a path by removing trailing slashes (except for root)\n * @param path - The path to normalize\n * @returns Normalized path\n */\nexport function normalizePath(path: string): string {\n if (path === '/') return path;\n return path.replace(/\\/+$/, '');\n}","import { RouteParams } from '../types';\n\n/**\n * Parses route parameters from a pathname given a pattern\n * This is a simpler alternative to matchPath for basic param extraction\n * @param pathname - The current pathname\n * @param pattern - The route pattern with parameters\n * @returns Extracted parameters\n */\nexport function parseParams(pathname: string, pattern: string): RouteParams {\n const params: RouteParams = {};\n\n // Split paths into segments\n const patternParts = pattern.split('/').filter(Boolean);\n const pathParts = pathname.split('/').filter(Boolean);\n\n // If lengths don't match, no params to extract\n if (patternParts.length !== pathParts.length) {\n return params;\n }\n\n // Extract params from matching segments\n patternParts.forEach((part, index) => {\n if (part.startsWith(':')) {\n const paramName = part.slice(1);\n params[paramName] = decodeURIComponent(pathParts[index]);\n }\n });\n\n return params;\n}\n\n/**\n * Serializes an object of parameters into a query string\n * @param params - The parameters to serialize\n * @returns Query string (without leading ?)\n */\nexport function serializeParams(params: Record<string, any>): string {\n const searchParams = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value !== null && value !== undefined) {\n searchParams.append(key, String(value));\n }\n });\n\n return searchParams.toString();\n}\n\n/**\n * Parses query string parameters from a search string\n * @param search - The search string (with or without leading ?)\n * @returns Parsed parameters\n */\nexport function parseQueryString(search: string): Record<string, string> {\n const params: Record<string, string> = {};\n\n // Remove leading ? if present\n const queryString = search.startsWith('?') ? search.slice(1) : search;\n\n if (!queryString) return params;\n\n const searchParams = new URLSearchParams(queryString);\n\n searchParams.forEach((value, key) => {\n params[key] = value;\n });\n\n return params;\n}\n\n/**\n * Merges route params and query params into a single object\n * @param routeParams - Parameters from the route pattern\n * @param queryParams - Parameters from the query string\n * @returns Merged parameters (query params override route params)\n */\nexport function mergeParams(\n routeParams: RouteParams,\n queryParams: Record<string, string>\n): RouteParams {\n return { ...routeParams, ...queryParams };\n}\n\n/**\n * Validates that required params are present\n * @param params - The parameters to validate\n * @param required - Array of required parameter names\n * @returns True if all required params are present\n */\nexport function validateParams(\n params: RouteParams,\n required: string[]\n): boolean {\n return required.every(key => key in params && params[key] !== '');\n}","import { useContext, useMemo } from 'react';\nimport { RouterContext } from '../context/RouterContext';\nimport { RouteParams } from '../types';\nimport { useUrl } from './useUrl';\nimport { matchPath } from '../utils/matchPath';\nimport { parseQueryString, mergeParams } from '../utils/parseParams';\n\n/**\n * Hook that extracts route parameters from the current URL\n * Requires RouterProvider with defined routes\n *\n * @param includeQuery - Whether to include query string parameters (default: false)\n * @returns Route parameters object\n *\n * @example\n * ```tsx\n * // With route pattern: '/blog/:slug'\n * // Current URL: '/blog/hello-world?page=2'\n *\n * function BlogPost() {\n * const params = useParams(); // { slug: 'hello-world' }\n * const allParams = useParams(true); // { slug: 'hello-world', page: '2' }\n *\n * return <div>Post: {params.slug}</div>;\n * }\n * ```\n */\nexport function useParams(includeQuery: boolean = false): RouteParams {\n const context = useContext(RouterContext);\n const { pathname, search } = useUrl();\n\n return useMemo(() => {\n // Base case - no context or routes\n if (!context || !context.routes || context.routes.length === 0) {\n if (includeQuery && search) {\n return parseQueryString(search);\n }\n return {};\n }\n\n const routes = context.routes;\n let routeParams: RouteParams = {};\n\n // Find matching route\n for (const route of routes) {\n const match = matchPath(pathname, route.path, route.exact);\n if (match.isMatch) {\n routeParams = match.params;\n break;\n }\n }\n\n // Include query params if requested\n if (includeQuery && search) {\n const queryParams = parseQueryString(search);\n return mergeParams(routeParams, queryParams);\n }\n\n return routeParams;\n }, [context, pathname, search, includeQuery]);\n}\n\n/**\n * Hook that extracts a single route parameter\n * Convenience wrapper around useParams\n *\n * @param paramName - Name of the parameter to extract\n * @returns Parameter value or undefined\n *\n * @example\n * ```tsx\n * function BlogPost() {\n * const slug = useParam('slug');\n * return <div>Post: {slug}</div>;\n * }\n * ```\n */\nexport function useParam(paramName: string): string | undefined {\n const params = useParams();\n return params[paramName];\n}\n\n/**\n * Hook that provides both route and query parameters separately\n *\n * @returns Object with route and query parameters\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { routeParams, queryParams } = useAllParams();\n *\n * return (\n * <div>\n * Route: {JSON.stringify(routeParams)}\n * Query: {JSON.stringify(queryParams)}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAllParams(): {\n routeParams: RouteParams;\n queryParams: Record<string, string>;\n} {\n const context = useContext(RouterContext);\n const { pathname, search } = useUrl();\n\n return useMemo(() => {\n let routeParams: RouteParams = {};\n\n // Extract route params if routes are defined\n if (context?.routes) {\n for (const route of context.routes) {\n const match = matchPath(pathname, route.path, route.exact);\n if (match.isMatch) {\n routeParams = match.params;\n break;\n }\n }\n }\n\n // Extract query params\n const queryParams = search ? parseQueryString(search) : {};\n\n return { routeParams, queryParams };\n }, [context, pathname, search]);\n}","import { useMemo } from 'react';\nimport { useUrl } from './useUrl';\nimport { matchPath } from '../utils/matchPath';\nimport { PathMatch } from '../types';\n\n/**\n * Hook that tests if the current path matches a pattern\n *\n * @param pattern - Route pattern to match against\n * @param exact - Whether to require an exact match (default: false)\n * @returns Match result with params\n *\n * @example\n * ```tsx\n * function BlogSection() {\n * const match = useRouteMatch('/blog/:slug');\n *\n * if (match.isMatch) {\n * return <BlogPost slug={match.params.slug} />;\n * }\n *\n * return <BlogList />;\n * }\n * ```\n */\nexport function useRouteMatch(pattern: string, exact: boolean = false): PathMatch {\n const { pathname } = useUrl();\n\n return useMemo(() => {\n return matchPath(pathname, pattern, exact);\n }, [pathname, pattern, exact]);\n}\n\n/**\n * Hook that checks if a route is currently active\n * Convenience wrapper around useRouteMatch\n *\n * @param pattern - Route pattern to check\n * @param exact - Whether to require an exact match\n * @returns Boolean indicating if route is active\n *\n * @example\n * ```tsx\n * function NavLink({ to, children }) {\n * const isActive = useIsActive(to);\n *\n * return (\n * <a\n * href={to}\n * className={isActive ? 'active' : ''}\n * >\n * {children}\n * </a>\n * );\n * }\n * ```\n */\nexport function useIsActive(pattern: string, exact: boolean = false): boolean {\n const match = useRouteMatch(pattern, exact);\n return match.isMatch;\n}\n\n/**\n * Hook that matches multiple patterns and returns the first match\n *\n * @param patterns - Array of route patterns\n * @returns First matching pattern or null\n *\n * @example\n * ```tsx\n * function ContentArea() {\n * const match = useMultiMatch([\n * '/blog/:slug',\n * '/products/:id',\n * '/about'\n * ]);\n *\n * if (match?.path.startsWith('/blog')) {\n * return <BlogPost />;\n * }\n * // ...\n * }\n * ```\n */\nexport function useMultiMatch(\n patterns: Array<{ pattern: string; exact?: boolean }>\n): PathMatch | null {\n const { pathname } = useUrl();\n\n return useMemo(() => {\n for (const { pattern, exact = false } of patterns) {\n const match = matchPath(pathname, pattern, exact);\n if (match.isMatch) {\n return match;\n }\n }\n return null;\n }, [pathname, patterns]);\n}","import React, { useEffect, useMemo, useCallback } from 'react';\nimport { RouterContext } from './RouterContext';\nimport { RouterProviderProps } from '../types';\nimport { isBrowser } from '../utils/ssrSafe';\n\n/**\n * RouterProvider component that provides routing context to child components\n * This is optional - many routing hooks work without a provider\n */\nexport function RouterProvider({\n children,\n initialPath,\n routes = [],\n scrollBehavior = 'smooth',\n onNavigate,\n}: RouterProviderProps) {\n // Memoize context value to prevent unnecessary re-renders\n const contextValue = useMemo(\n () => ({\n routes,\n initialPath,\n scrollBehavior,\n onNavigate,\n }),\n [routes, initialPath, scrollBehavior, onNavigate]\n );\n\n // Handle navigation events\n const handleNavigation = useCallback(() => {\n if (!isBrowser() || !onNavigate) return;\n onNavigate(window.location.pathname);\n }, [onNavigate]);\n\n useEffect(() => {\n if (!isBrowser() || !onNavigate) return;\n\n // Call onNavigate for initial path if it matches current location\n if (initialPath && window.location.pathname === initialPath) {\n onNavigate(initialPath);\n }\n\n // Listen for browser navigation events\n const handlePopState = () => {\n handleNavigation();\n };\n\n // Custom event for programmatic navigation\n const handleRouteChange = (event: CustomEvent) => {\n if (event.detail?.path) {\n onNavigate(event.detail.path);\n }\n };\n\n window.addEventListener('popstate', handlePopState);\n window.addEventListener('routechange', handleRouteChange as EventListener);\n\n // Cleanup\n return () => {\n window.removeEventListener('popstate', handlePopState);\n window.removeEventListener('routechange', handleRouteChange as EventListener);\n };\n }, [initialPath, onNavigate, handleNavigation]);\n\n return (\n <RouterContext.Provider value={contextValue}>\n {children}\n </RouterContext.Provider>\n );\n}\n\n/**\n * Type guard to check if RouterProvider is being used\n */\nexport function isRouterProviderAvailable(): boolean {\n if (!isBrowser()) return false;\n\n // Check if there's a RouterContext in the tree\n try {\n const context = React.useContext(RouterContext);\n return context !== null;\n } catch {\n return false;\n }\n}"],"names":["RouterContext","createContext","displayName","isBrowser","window","document","ssrSafe","clientFn","serverValue","DEFAULT_URL","href","origin","protocol","host","hostname","port","pathname","search","hash","useUrl","url","setUrl","useState","location","useEffect","updateUrl","addEventListener","handleRouteChange","removeEventListener","scrollToAnchor","anchor","smooth","offset","id","replace","element","getElementById","fallbackElement","querySelector","scrollToElement","console","warn","offsetPosition","getBoundingClientRect","top","pageYOffset","scrollTo","behavior","history","replaceState","currentState","state","setTimeout","focus","preventScroll","activeElement","originalTabIndex","getAttribute","setAttribute","removeAttribute","once","scrollToTop","useNavigation","context","useContext","scrollBehavior","navigateTo","useCallback","options","navOptions","path","fullPath","currentPath","currentHash","dispatchEvent","CustomEvent","detail","pushState","PopStateEvent","reload","useGoBack","fallback","delta","historyLength","useRef","length","initialLength","current","canGoBack","goBack","customDelta","steps","Math","abs","go","error","patternCache","Map","matchPath","pattern","exact","result","isMatch","params","regex","cacheKey","has","get","regexPattern","RegExp","set","getPatternRegex","match","groups","parseQueryString","queryString","startsWith","slice","URLSearchParams","forEach","value","key","mergeParams","routeParams","queryParams","useParams","includeQuery","useMemo","routes","route","useRouteMatch","children","initialPath","onNavigate","contextValue","handleNavigation","handlePopState","event","_a","Provider","fn","args","Object","entries","String","patterns","documentElement","scrollTop","patternParts","split","filter","Boolean","pathParts","part","index","paramName","decodeURIComponent","accessor","searchParams","append","toString","forward","currentParams","delete","newPath","required","every"],"mappings":"uRAOO,MAAMA,EAAgBC,EAAAA,cAAyC,MAKtED,EAAcE,YAAc,gBCLrB,MAAMC,EAAY,IACL,oBAAXC,aACoB,IAApBA,OAAOC,SAaT,SAASC,EAAWC,EAAmBC,GAC5C,OAAOL,IAAcI,IAAaC,CACpC,CCjBA,MAAMC,EAAwB,CAC5BC,KAAM,GACNC,OAAQ,GACRC,SAAU,SACVC,KAAM,GACNC,SAAU,GACVC,KAAM,GACNC,SAAU,IACVC,OAAQ,GACRC,KAAM,IAyBD,SAASC,IACd,MAAOC,EAAKC,GAAUC,EAAAA,SAAmB,IACvChB,EACE,KAAA,CACEI,KAAMN,OAAOmB,SAASb,KACtBC,OAAQP,OAAOmB,SAASZ,OACxBC,SAAUR,OAAOmB,SAASX,SAC1BC,KAAMT,OAAOmB,SAASV,KACtBC,SAAUV,OAAOmB,SAAST,SAC1BC,KAAMX,OAAOmB,SAASR,KACtBC,SAAUZ,OAAOmB,SAASP,SAC1BC,OAAQb,OAAOmB,SAASN,OACxBC,KAAMd,OAAOmB,SAASL,OAExBT,IAuCJ,OAnCAe,EAAAA,UAAU,KACR,IAAKrB,IAAa,OAElB,MAAMsB,EAAY,KAChBJ,EAAO,CACLX,KAAMN,OAAOmB,SAASb,KACtBC,OAAQP,OAAOmB,SAASZ,OACxBC,SAAUR,OAAOmB,SAASX,SAC1BC,KAAMT,OAAOmB,SAASV,KACtBC,SAAUV,OAAOmB,SAAST,SAC1BC,KAAMX,OAAOmB,SAASR,KACtBC,SAAUZ,OAAOmB,SAASP,SAC1BC,OAAQb,OAAOmB,SAASN,OACxBC,KAAMd,OAAOmB,SAASL,QAK1BO,IAGArB,OAAOsB,iBAAiB,WAAYD,GACpCrB,OAAOsB,iBAAiB,aAAcD,GAGtC,MAAME,EAAoB,IAAMF,IAGhC,OAFArB,OAAOsB,iBAAiB,cAAeC,GAEhC,KACLvB,OAAOwB,oBAAoB,WAAYH,GACvCrB,OAAOwB,oBAAoB,aAAcH,GACzCrB,OAAOwB,oBAAoB,cAAeD,KAE3C,IAEIP,CACT,CCjFO,SAASS,EACdC,EACAC,GAAkB,EAClBC,EAX4B,IAa5B,IAAK7B,IAAa,OAGlB,MAAM8B,EAAKH,EAAOI,QAAQ,KAAM,IAG1BC,EAAU9B,SAAS+B,eAAeH,GAExC,IAAKE,EAAS,CAEZ,MAAME,EAAkBhC,SAASiC,cAAc,QAAQL,OACvD,OAAKI,OAILE,EAAgBF,EAAgCN,EAAQC,QAHtDQ,QAAQC,KAAK,kDAAkDR,IAKnE,CAEAM,EAAgBJ,EAASJ,EAAQC,EACnC,CAQA,SAASO,EACPJ,EACAJ,EACAC,GAEA,MACMU,EADkBP,EAAQQ,wBAAwBC,IACfxC,OAAOyC,YAAcb,EAG9D5B,OAAO0C,SAAS,CACdF,IAAKF,EACLK,SAAUhB,EAAS,SAAW,SAIhC,MAAME,EAAKE,EAAQF,GACnB,GAAIA,GAAM7B,OAAO4C,QAAQC,aAAc,CAErC,MAAMC,EAAe9C,OAAO4C,QAAQG,MACpC/C,OAAO4C,QAAQC,aAAaC,EAAc,GAAI,IAAIjB,IACpD,CAGIF,EAEFqB,WAAW,KAIT,GAHAjB,EAAQkB,MAAM,CAAEC,eAAe,IAG3BjD,SAASkD,gBAAkBpB,EAAS,CACtC,MAAMqB,EAAmBrB,EAAQsB,aAAa,YAC9CtB,EAAQuB,aAAa,WAAY,MACjCvB,EAAQkB,MAAM,CAAEC,eAAe,IAG/BnB,EAAQT,iBAAiB,OAAQ,KACN,OAArB8B,EACFrB,EAAQwB,gBAAgB,YAExBxB,EAAQuB,aAAa,WAAYF,IAElC,CAAEI,MAAM,GACb,GACC,KAEHzB,EAAQkB,MAAM,CAAEC,eAAe,GAEnC,CAMO,SAASO,EAAY9B,GAAkB,GACvC5B,KAELC,OAAO0C,SAAS,CACdF,IAAK,EACLG,SAAUhB,EAAS,SAAW,QAElC,CCzEO,SAAS+B,IACd,MAAMC,EAAUC,EAAAA,WAAWhE,GACrBiE,SAAiBF,WAASE,iBAAkB,SAE5CC,EAAaC,EAAAA,YAChBC,IACC,IAAKjE,IAAa,OAGlB,MAAMkE,EACe,iBAAZD,EAAuB,CAAEE,KAAMF,GAAYA,GAE9CE,KACJA,EAAAxC,OACAA,EACAI,QAAAA,GAAU,EAAAH,OACVA,EAA4B,WAAnBkC,EAAmBd,MAC5BA,GACEkB,EAGJ,IAAKC,GAAQxC,EAEX,YADAD,EAAeC,EAAQC,GAKzB,IAAIwC,EAAWD,EACXxC,IACFyC,EAAW,GAAGD,KAAQxC,KAIxB,MAAM0C,EAAcpE,OAAOmB,SAASP,SAC9ByD,EAAcrE,OAAOmB,SAASL,KAGpC,GAAIsD,IAAgBF,GAAQG,KAFZ3C,EAAS,IAAIA,IAAW,IAkBtC,OAdA1B,OAAO4C,QAAQd,EAAU,eAAiB,aACxCiB,GAAS/C,OAAO4C,QAAQG,MACxB,GACAoB,GAGEzC,GACFD,EAAeC,EAAQC,QAIzB3B,OAAOsE,cACL,IAAIC,YAAY,cAAe,CAAEC,OAAQ,CAAEN,KAAMC,MAMjDrC,EACF9B,OAAO4C,QAAQC,aAAaE,GAAS,KAAM,GAAIoB,GAE/CnE,OAAO4C,QAAQ6B,UAAU1B,GAAS,KAAM,GAAIoB,GAI9CnE,OAAOsE,cAAc,IAAII,cAAc,WAAY,CAAE3B,WACrD/C,OAAOsE,cACL,IAAIC,YAAY,cAAe,CAAEC,OAAQ,CAAEN,KAAMC,MAI/CzC,EAEFsB,WAAW,IAAMvB,EAAeC,EAAQC,GAAS,GACxCyC,IAAgBF,GAEzBT,EAAY9B,IAGhB,CAACkC,IAGG/B,EAAUiC,EAAAA,YACbG,IACCJ,EAAW,CAAEI,OAAMpC,SAAS,KAE9B,CAACgC,IAGGa,EAASZ,EAAAA,YAAY,KACpBhE,KACLC,OAAOmB,SAASwD,UACf,IAEH,MAAO,CAAEb,aAAYhC,UAAS6C,SAChC,CCnGO,SAASC,EAAUZ,EAAyB,IACjD,MAAMa,SAAEA,EAAW,IAAAC,MAAKA,GAAQ,GAAOd,GACjCF,WAAEA,GAAeJ,IACjBqB,EAAgBC,EAAAA,OAAejF,IAAcC,OAAO4C,QAAQqC,OAAS,GACrEC,EAAgBF,EAAAA,OAAe,GAErC5D,EAAAA,UAAU,KACHrB,MAGyB,IAA1BmF,EAAcC,UAChBD,EAAcC,QAAUnF,OAAO4C,QAAQqC,QAIzCF,EAAcI,QAAUnF,OAAO4C,QAAQqC,SACtC,IAGH,MAAMG,EAAYrF,KAAeC,OAAO4C,QAAQqC,OAASC,EAAcC,QAyBvE,MAAO,CAAEE,OAvBMtB,EAAAA,YACZuB,IACC,IAAKvF,IAAa,OAElB,MAAMwF,EAAQD,GAAeR,EAG7B,GAAI9E,OAAO4C,QAAQqC,OAASO,KAAKC,IAAIF,GACnC,IACEvF,OAAO4C,QAAQ8C,GAAGH,EACpB,OAASI,GACPvD,QAAQC,KAAK,yCAA0CsD,GAEvD7B,EAAW,CAAEI,KAAMW,EAAU/C,SAAS,GACxC,MAGAgC,EAAW,CAAEI,KAAMW,EAAU/C,SAAS,KAG1C,CAACgD,EAAOD,EAAUf,IAGHsB,YACnB,CCrEA,MAAMQ,MAAmBC,IASlB,SAASC,EACdlF,EACAmF,EACAC,GAAiB,GAEjB,MAAMC,EAAoB,CACxBC,SAAS,EACTC,OAAQ,CAAA,EACRjC,KAAMtD,GAIR,GAAgB,MAAZmF,GAAmBC,EAErB,OADAC,EAAOC,QAAuB,MAAbtF,EACVqF,EAIT,MAAMG,EAqBR,SAAyBL,EAAiBC,GACxC,MAAMK,EAAW,GAAGN,KAAWC,IAE/B,GAAIJ,EAAaU,IAAID,GACnB,OAAOT,EAAaW,IAAIF,GAI1B,IAAIG,EAAeT,EAEhBjE,QAAQ,sBAAuB,QAE/BA,QAAQ,UAAW,gBAIpB0E,EADER,EACa,IAAIQ,KAEJ,IAAIA,IAGrB,MAAMJ,EAAQ,IAAIK,OAAOD,GAGzB,OAFAZ,EAAac,IAAIL,EAAUD,GAEpBA,CACT,CA9CgBO,CAAgBZ,EAASC,GACjCY,EAAQhG,EAASgG,MAAMR,GAW7B,OATIQ,IACFX,EAAOC,SAAU,EAGbU,EAAMC,SACRZ,EAAOE,OAASS,EAAMC,SAInBZ,CACT,CCSO,SAASa,EAAiBjG,GAC/B,MAAMsF,EAAiC,CAAA,EAGjCY,EAAclG,EAAOmG,WAAW,KAAOnG,EAAOoG,MAAM,GAAKpG,EAE/D,IAAKkG,EAAa,OAAOZ,EAQzB,OANqB,IAAIe,gBAAgBH,GAE5BI,QAAQ,CAACC,EAAOC,KAC3BlB,EAAOkB,GAAOD,IAGTjB,CACT,CAQO,SAASmB,EACdC,EACAC,GAEA,MAAO,IAAKD,KAAgBC,EAC9B,CCvDO,SAASC,EAAUC,GAAwB,GAChD,MAAM/D,EAAUC,EAAAA,WAAWhE,IACrBgB,SAAEA,EAAAC,OAAUA,GAAWE,IAE7B,OAAO4G,EAAAA,QAAQ,KAEb,IAAKhE,IAAYA,EAAQiE,QAAoC,IAA1BjE,EAAQiE,OAAO3C,OAChD,OAAIyC,GAAgB7G,EACXiG,EAAiBjG,GAEnB,CAAA,EAGT,MAAM+G,EAASjE,EAAQiE,OACvB,IAAIL,EAA2B,CAAA,EAG/B,IAAA,MAAWM,KAASD,EAAQ,CAC1B,MAAMhB,EAAQd,EAAUlF,EAAUiH,EAAM3D,KAAM2D,EAAM7B,OACpD,GAAIY,EAAMV,QAAS,CACjBqB,EAAcX,EAAMT,OACpB,KACF,CACF,CAGA,GAAIuB,GAAgB7G,EAAQ,CAE1B,OAAOyG,EAAYC,EADCT,EAAiBjG,GAEvC,CAEA,OAAO0G,GACN,CAAC5D,EAAS/C,EAAUC,EAAQ6G,GACjC,CCnCO,SAASI,EAAc/B,EAAiBC,GAAiB,GAC9D,MAAMpF,SAAEA,GAAaG,IAErB,OAAO4G,EAAAA,QAAQ,IACN7B,EAAUlF,EAAUmF,EAASC,GACnC,CAACpF,EAAUmF,EAASC,GACzB,oCCtBO,UAAwB+B,SAC7BA,EAAAC,YACAA,EAAAJ,OACAA,EAAS,GAAA/D,eACTA,EAAiB,SAAAoE,WACjBA,IAGA,MAAMC,EAAeP,EAAAA,QACnB,KAAA,CACEC,SACAI,cACAnE,iBACAoE,eAEF,CAACL,EAAQI,EAAanE,EAAgBoE,IAIlCE,EAAmBpE,EAAAA,YAAY,KAC9BhE,KAAgBkI,GACrBA,EAAWjI,OAAOmB,SAASP,WAC1B,CAACqH,IAgCJ,OA9BA7G,EAAAA,UAAU,KACR,IAAKrB,MAAgBkI,EAAY,OAG7BD,GAAehI,OAAOmB,SAASP,WAAaoH,GAC9CC,EAAWD,GAIb,MAAMI,EAAiB,KACrBD,KAII5G,EAAqB8G,WACrB,OAAAC,EAAAD,EAAM7D,aAAN,EAAA8D,EAAcpE,OAChB+D,EAAWI,EAAM7D,OAAON,OAQ5B,OAJAlE,OAAOsB,iBAAiB,WAAY8G,GACpCpI,OAAOsB,iBAAiB,cAAeC,GAGhC,KACLvB,OAAOwB,oBAAoB,WAAY4G,GACvCpI,OAAOwB,oBAAoB,cAAeD,KAE3C,CAACyG,EAAaC,EAAYE,oBAG1BvI,EAAc2I,SAAd,CAAuBnB,MAAOc,GAC5BH,EAGP,gBTrCO,SACLS,GAEA,MAAO,IAAIC,KACT,GAAI1I,IACF,OAAOyI,KAAMC,GAInB,cK4EO,SAAmB1C,EAAiBI,GACzC,IAAIjC,EAAO6B,EAMX,OAJA2C,OAAOC,QAAQxC,GAAQgB,QAAQ,EAAEE,EAAKD,MACpClD,EAAOA,EAAKpC,QAAQ,IAAIuF,IAAOuB,OAAOxB,MAGjClD,CACT,kBAtCO,SAAuBtD,EAAkBmF,GAE9C,OADcD,EAAUlF,EAAUmF,GAAS,GAC9BI,MACf,sBAQO,SACLvF,EACAiI,GAEA,IAAA,MAAWhB,KAASgB,EAAU,CAC5B,MAAMjC,EAAQd,EAAUlF,EAAUiH,EAAM3D,KAAM2D,EAAM7B,OACpD,GAAIY,EAAMV,QACR,OAAOU,CAEX,CACA,OAAO,IACT,sBHKO,WACL,OAAK7G,MACEC,OAAOyC,aAAexC,SAAS6I,gBAAgBC,YAD7B,CAE3B,wBFtGqB,KAAgBhJ,kDKqH9B,SAAuBmE,GAC5B,MAAa,MAATA,EAAqBA,EAClBA,EAAKpC,QAAQ,OAAQ,GAC9B,gBC7HO,SAAqBlB,EAAkBmF,GAC5C,MAAMI,EAAsB,CAAA,EAGtB6C,EAAejD,EAAQkD,MAAM,KAAKC,OAAOC,SACzCC,EAAYxI,EAASqI,MAAM,KAAKC,OAAOC,SAG7C,OAAIH,EAAa/D,SAAWmE,EAAUnE,QAKtC+D,EAAa7B,QAAQ,CAACkC,EAAMC,KAC1B,GAAID,EAAKrC,WAAW,KAAM,CACxB,MAAMuC,EAAYF,EAAKpC,MAAM,GAC7Bd,EAAOoD,GAAaC,mBAAmBJ,EAAUE,GACnD,IAROnD,CAYX,oCNiBO,SAAuBsD,EAAiC5E,GAC7D,GAAI9E,KAAeC,OACjB,IACE,OAAOyJ,EAASzJ,OAClB,CAAA,MACE,OAAO6E,CACT,CAEF,OAAOA,CACT,uDMnBO,SAAyBsB,GAC9B,MAAMuD,EAAe,IAAIxC,gBAQzB,OANAwB,OAAOC,QAAQxC,GAAQgB,QAAQ,EAAEE,EAAKD,MAChCA,SACFsC,EAAaC,OAAOtC,EAAKuB,OAAOxB,MAI7BsC,EAAaE,UACtB,6BCsDO,WAIL,MAAMjG,EAAUC,EAAAA,WAAWhE,IACrBgB,SAAEA,EAAAC,OAAUA,GAAWE,IAE7B,OAAO4G,EAAAA,QAAQ,KACb,IAAIJ,EAA2B,CAAA,EAG/B,SAAI5D,WAASiE,OACX,IAAA,MAAWC,KAASlE,EAAQiE,OAAQ,CAClC,MAAMhB,EAAQd,EAAUlF,EAAUiH,EAAM3D,KAAM2D,EAAM7B,OACpD,GAAIY,EAAMV,QAAS,CACjBqB,EAAcX,EAAMT,OACpB,KACF,CACF,CAMF,MAAO,CAAEoB,cAAaC,YAFF3G,EAASiG,EAAiBjG,GAAU,CAAA,IAGvD,CAAC8C,EAAS/C,EAAUC,GACzB,YH5CO,SAAiBgE,EAAmB,KACzC,MAAMQ,OAAEA,GAAWT,EAAU,CAAEC,aAC/B,OAAOQ,CACT,+BAOO,WACL,OAAOtB,EAAAA,YAAY,KACjB,GAAKhE,IAEL,IACEC,OAAO4C,QAAQiH,SACjB,OAASlE,GACPvD,QAAQC,KAAK,4CAA6CsD,EAC5D,GACC,GACL,YH+BO,WACL,MAAM7E,KAAEA,GAASC,IACjB,OAAOD,EAAKgB,QAAQ,KAAM,GAC5B,gBOhFO,SAAqBiE,EAAiBC,GAAiB,GAE5D,OADc8B,EAAc/B,EAASC,GACxBE,OACf,kBAwBO,SACL2C,GAEA,MAAMjI,SAAEA,GAAaG,IAErB,OAAO4G,EAAAA,QAAQ,KACb,IAAA,MAAW5B,QAAEA,EAAAC,MAASA,GAAQ,KAAW6C,EAAU,CACjD,MAAMjC,EAAQd,EAAUlF,EAAUmF,EAASC,GAC3C,GAAIY,EAAMV,QACR,OAAOU,CAEX,CACA,OAAO,MACN,CAAChG,EAAUiI,GAChB,gBLsCO,WACL,MAAM/E,WAAEA,GAAeJ,IACvB,OAAOI,CACT,+BI9DO,SAAkByF,GAEvB,OADe9B,IACD8B,EAChB,8BNuBO,WACL,MAAM3I,SAAEA,GAAaG,IACrB,OAAOH,CACT,sCAQO,WACL,MAAMC,OAAEA,GAAWE,IACboF,EAAiC,CAAA,EAEvC,GAAIpG,KAAec,EAAQ,CACJ,IAAIqG,gBAAgBrG,GAC5BsG,QAAQ,CAACC,EAAOC,KAC3BlB,EAAOkB,GAAOD,GAElB,CAEA,OAAOjB,CACT,0BEoBO,WACL,MAAMrC,WAAEA,GAAeJ,IAEvB,OAAOK,EAAAA,YACL,CAACoC,EAAuCrE,GAAU,KAChD,IAAK/B,IAAa,OAElB,MAAM+J,EAAgB,IAAI5C,gBAAgBlH,OAAOmB,SAASN,QAG1D6H,OAAOC,QAAQxC,GAAQgB,QAAQ,EAAEE,EAAKD,MACtB,OAAVA,EACF0C,EAAcC,OAAO1C,GAErByC,EAAcpD,IAAIW,EAAKD,KAI3B,MAAMvG,EAASiJ,EAAcF,WACvBI,EAAU,GAAGhK,OAAOmB,SAASP,WAAWC,EAAS,IAAIA,IAAW,KAEtEiD,EAAW,CAAEI,KAAM8F,EAASlI,aAE9B,CAACgC,GAEL,8BGjFO,SACLqC,EACA8D,GAEA,OAAOA,EAASC,MAAM7C,GAAOA,KAAOlB,GAA0B,KAAhBA,EAAOkB,GACvD"}
{"version":3,"file":"page-speed-router.umd.js","sources":["../../src/context/RouterContext.tsx","../../src/utils/ssrSafe.ts","../../src/hooks/useUrl.ts","../../src/utils/scrollToAnchor.ts","../../src/hooks/useNavigation.ts","../../src/hooks/useGoBack.ts","../../src/utils/matchPath.ts","../../src/utils/parseParams.ts","../../src/hooks/useParams.ts","../../src/hooks/useRouteMatch.ts","../../src/analytics/usePageViews.ts","../../src/analytics/getDeviceType.ts","../../src/analytics/usePageViewAnalytics.ts","../../src/context/RouterProvider.tsx"],"sourcesContent":["import { createContext } from 'react';\nimport { RouterContextValue } from '../types';\n\n/**\n * Context for sharing router state across the component tree\n * This context is optional - many hooks work without it\n */\nexport const RouterContext = createContext<RouterContextValue | null>(null);\n\n/**\n * Display name for React DevTools\n */\nRouterContext.displayName = 'RouterContext';","/**\n * SSR-safe utilities for handling server and client environments\n */\n\n/**\n * Checks if code is running in a browser environment\n */\nexport const isBrowser = (): boolean =>\n typeof window !== 'undefined' &&\n typeof window.document !== 'undefined';\n\n/**\n * Checks if code is running in a server environment\n */\nexport const isSSR = (): boolean => !isBrowser();\n\n/**\n * Executes a client-side function if in browser, otherwise returns server value\n * @param clientFn - Function to execute on client\n * @param serverValue - Default value to use on server\n * @returns The result of clientFn on client, serverValue on server\n */\nexport function ssrSafe<T>(clientFn: () => T, serverValue: T): T {\n return isBrowser() ? clientFn() : serverValue;\n}\n\n/**\n * Guards a function to only run in browser environment\n * @param fn - Function to guard\n * @returns A function that only runs in browser\n */\nexport function browserOnly<T extends (...args: any[]) => any>(\n fn: T\n): (...args: Parameters<T>) => ReturnType<T> | undefined {\n return (...args: Parameters<T>): ReturnType<T> | undefined => {\n if (isBrowser()) {\n return fn(...args);\n }\n return undefined;\n };\n}\n\n/**\n * Safely access window properties with fallback\n * @param accessor - Function that accesses window property\n * @param fallback - Fallback value if window is undefined\n */\nexport function safeWindow<T>(accessor: (window: Window) => T, fallback: T): T {\n if (isBrowser() && window) {\n try {\n return accessor(window);\n } catch {\n return fallback;\n }\n }\n return fallback;\n}","import { useState, useEffect } from 'react';\nimport { UrlState } from '../types';\nimport { ssrSafe, isBrowser } from '../utils/ssrSafe';\n\n/**\n * Default URL state for SSR environments\n */\nconst DEFAULT_URL: UrlState = {\n href: '',\n origin: '',\n protocol: 'https:',\n host: '',\n hostname: '',\n port: '',\n pathname: '/',\n search: '',\n hash: '',\n};\n\n/**\n * Hook that provides current URL information\n * Works outside of RouterProvider - uses browser APIs directly\n * SSR-safe with appropriate defaults\n *\n * @returns Current URL state\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { pathname, search, hash } = useUrl();\n *\n * return (\n * <div>\n * Current path: {pathname}\n * Query: {search}\n * Hash: {hash}\n * </div>\n * );\n * }\n * ```\n */\nexport function useUrl(): UrlState {\n const [url, setUrl] = useState<UrlState>(() =>\n ssrSafe(\n () => ({\n href: window.location.href,\n origin: window.location.origin,\n protocol: window.location.protocol,\n host: window.location.host,\n hostname: window.location.hostname,\n port: window.location.port,\n pathname: window.location.pathname,\n search: window.location.search,\n hash: window.location.hash,\n }),\n DEFAULT_URL\n )\n );\n\n useEffect(() => {\n if (!isBrowser()) return;\n\n const updateUrl = () => {\n setUrl({\n href: window.location.href,\n origin: window.location.origin,\n protocol: window.location.protocol,\n host: window.location.host,\n hostname: window.location.hostname,\n port: window.location.port,\n pathname: window.location.pathname,\n search: window.location.search,\n hash: window.location.hash,\n });\n };\n\n // Initial sync after hydration\n updateUrl();\n\n // Listen for navigation events\n window.addEventListener('popstate', updateUrl);\n window.addEventListener('hashchange', updateUrl);\n\n // Custom event for programmatic navigation\n const handleRouteChange = () => updateUrl();\n window.addEventListener('routechange', handleRouteChange);\n\n return () => {\n window.removeEventListener('popstate', updateUrl);\n window.removeEventListener('hashchange', updateUrl);\n window.removeEventListener('routechange', handleRouteChange);\n };\n }, []);\n\n return url;\n}\n\n/**\n * Hook that provides just the current pathname\n * Convenience wrapper around useUrl\n *\n * @returns Current pathname\n */\nexport function usePathname(): string {\n const { pathname } = useUrl();\n return pathname;\n}\n\n/**\n * Hook that provides query string parameters as an object\n * Convenience wrapper around useUrl\n *\n * @returns Parsed query parameters\n */\nexport function useSearchParams(): Record<string, string> {\n const { search } = useUrl();\n const params: Record<string, string> = {};\n\n if (isBrowser() && search) {\n const searchParams = new URLSearchParams(search);\n searchParams.forEach((value, key) => {\n params[key] = value;\n });\n }\n\n return params;\n}\n\n/**\n * Hook that provides the current hash (without #)\n * Convenience wrapper around useUrl\n *\n * @returns Current hash value\n */\nexport function useHash(): string {\n const { hash } = useUrl();\n return hash.replace(/^#/, '');\n}","import { isBrowser } from './ssrSafe';\n\n/**\n * Default offset for scroll positioning (in pixels)\n * Provides a small amount of padding from the top of the viewport\n */\nconst DEFAULT_SCROLL_OFFSET = 20;\n\n/**\n * Smoothly scrolls to an anchor element on the page\n * @param anchor - The ID of the element to scroll to (with or without #)\n * @param smooth - Whether to use smooth scrolling animation\n * @param offset - Offset from the top of the viewport in pixels\n */\nexport function scrollToAnchor(\n anchor: string,\n smooth: boolean = true,\n offset: number = DEFAULT_SCROLL_OFFSET\n): void {\n if (!isBrowser()) return;\n\n // Remove leading # if present\n const id = anchor.replace(/^#/, '');\n\n // Try to find the element\n const element = document.getElementById(id);\n\n if (!element) {\n // Try querySelector as fallback (for complex selectors)\n const fallbackElement = document.querySelector(`[id=\"${id}\"]`);\n if (!fallbackElement) {\n console.warn(`[page-speed/router] Anchor element not found: #${id}`);\n return;\n }\n scrollToElement(fallbackElement as HTMLElement, smooth, offset);\n return;\n }\n\n scrollToElement(element, smooth, offset);\n}\n\n/**\n * Scrolls to a specific element\n * @param element - The element to scroll to\n * @param smooth - Whether to use smooth scrolling\n * @param offset - Offset from the top in pixels\n */\nfunction scrollToElement(\n element: HTMLElement,\n smooth: boolean,\n offset: number\n): void {\n const elementPosition = element.getBoundingClientRect().top;\n const offsetPosition = elementPosition + window.pageYOffset - offset;\n\n // Perform the scroll\n window.scrollTo({\n top: offsetPosition,\n behavior: smooth ? 'smooth' : 'auto',\n });\n\n // Update URL hash without jumping\n const id = element.id;\n if (id && window.history.replaceState) {\n // Preserve existing state\n const currentState = window.history.state;\n window.history.replaceState(currentState, '', `#${id}`);\n }\n\n // Set focus for accessibility (after scroll completes)\n if (smooth) {\n // Wait for smooth scroll to complete (approximate)\n setTimeout(() => {\n element.focus({ preventScroll: true });\n\n // If element can't receive focus, add tabindex temporarily\n if (document.activeElement !== element) {\n const originalTabIndex = element.getAttribute('tabindex');\n element.setAttribute('tabindex', '-1');\n element.focus({ preventScroll: true });\n\n // Remove temporary tabindex on blur\n element.addEventListener('blur', () => {\n if (originalTabIndex === null) {\n element.removeAttribute('tabindex');\n } else {\n element.setAttribute('tabindex', originalTabIndex);\n }\n }, { once: true });\n }\n }, 500);\n } else {\n element.focus({ preventScroll: true });\n }\n}\n\n/**\n * Scrolls to the top of the page\n * @param smooth - Whether to use smooth scrolling\n */\nexport function scrollToTop(smooth: boolean = true): void {\n if (!isBrowser()) return;\n\n window.scrollTo({\n top: 0,\n behavior: smooth ? 'smooth' : 'auto',\n });\n}\n\n/**\n * Gets the current scroll position\n * @returns The current Y scroll position\n */\nexport function getScrollPosition(): number {\n if (!isBrowser()) return 0;\n return window.pageYOffset || document.documentElement.scrollTop || 0;\n}","import { useCallback, useContext } from 'react';\nimport { NavigationAPI, NavigateOptions } from '../types';\nimport { scrollToAnchor, scrollToTop } from '../utils/scrollToAnchor';\nimport { isBrowser } from '../utils/ssrSafe';\nimport { RouterContext } from '../context/RouterContext';\n\n/**\n * Hook that provides navigation functions\n * Works outside of RouterProvider - uses browser APIs directly\n *\n * @returns Navigation API with navigateTo, replace, and reload functions\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { navigateTo, replace, reload } = useNavigation();\n *\n * const handleClick = () => {\n * navigateTo({ path: '/about' });\n * };\n *\n * const handleAnchorClick = () => {\n * navigateTo({ path: '/blog', anchor: 'comments' });\n * };\n *\n * return (\n * <div>\n * <button onClick={handleClick}>Go to About</button>\n * <button onClick={handleAnchorClick}>Go to Blog Comments</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useNavigation(): NavigationAPI {\n const context = useContext(RouterContext);\n const scrollBehavior = context?.scrollBehavior || 'smooth';\n\n const navigateTo = useCallback(\n (options: NavigateOptions | string) => {\n if (!isBrowser()) return;\n\n // Handle string shorthand\n const navOptions: NavigateOptions =\n typeof options === 'string' ? { path: options } : options;\n\n const {\n path,\n anchor,\n replace = false,\n smooth = scrollBehavior === 'smooth',\n state,\n } = navOptions;\n\n // Handle anchor-only navigation on same page\n if (!path && anchor) {\n scrollToAnchor(anchor, smooth);\n return;\n }\n\n // Build full URL\n let fullPath = path;\n if (anchor) {\n fullPath = `${path}#${anchor}`;\n }\n\n // Check if it's the same path (anchor navigation only)\n const currentPath = window.location.pathname;\n const currentHash = window.location.hash;\n const newHash = anchor ? `#${anchor}` : '';\n\n if (currentPath === path && currentHash !== newHash) {\n // Same path, different anchor - just update hash and scroll\n window.history[replace ? 'replaceState' : 'pushState'](\n state || window.history.state,\n '',\n fullPath\n );\n\n if (anchor) {\n scrollToAnchor(anchor, smooth);\n }\n\n // Dispatch custom event\n window.dispatchEvent(\n new CustomEvent('routechange', { detail: { path: fullPath } })\n );\n return;\n }\n\n // Update history\n if (replace) {\n window.history.replaceState(state || null, '', fullPath);\n } else {\n window.history.pushState(state || null, '', fullPath);\n }\n\n // Dispatch events to trigger updates\n window.dispatchEvent(new PopStateEvent('popstate', { state }));\n window.dispatchEvent(\n new CustomEvent('routechange', { detail: { path: fullPath } })\n );\n\n // Handle scrolling\n if (anchor) {\n // Use setTimeout to ensure DOM is updated\n setTimeout(() => scrollToAnchor(anchor, smooth), 0);\n } else if (currentPath !== path) {\n // Scroll to top on regular navigation (different page)\n scrollToTop(smooth);\n }\n },\n [scrollBehavior]\n );\n\n const replace = useCallback(\n (path: string) => {\n navigateTo({ path, replace: true });\n },\n [navigateTo]\n );\n\n const reload = useCallback(() => {\n if (!isBrowser()) return;\n window.location.reload();\n }, []);\n\n return { navigateTo, replace, reload };\n}\n\n/**\n * Hook that provides a simple navigation function\n * Convenience wrapper around useNavigation\n *\n * @returns Navigate function\n */\nexport function useNavigate() {\n const { navigateTo } = useNavigation();\n return navigateTo;\n}\n\n/**\n * Hook that provides a function to update query parameters\n *\n * @returns Function to update query params\n */\nexport function useUpdateSearchParams() {\n const { navigateTo } = useNavigation();\n\n return useCallback(\n (params: Record<string, string | null>, replace = false) => {\n if (!isBrowser()) return;\n\n const currentParams = new URLSearchParams(window.location.search);\n\n // Update or remove params\n Object.entries(params).forEach(([key, value]) => {\n if (value === null) {\n currentParams.delete(key);\n } else {\n currentParams.set(key, value);\n }\n });\n\n const search = currentParams.toString();\n const newPath = `${window.location.pathname}${search ? `?${search}` : ''}`;\n\n navigateTo({ path: newPath, replace });\n },\n [navigateTo]\n );\n}","import { useCallback, useRef, useEffect } from 'react';\nimport { GoBackOptions, GoBackAPI } from '../types';\nimport { useNavigation } from './useNavigation';\nimport { isBrowser } from '../utils/ssrSafe';\n\n/**\n * Hook that provides safe back navigation with fallback\n *\n * @param options - Configuration options\n * @param options.fallback - Path to navigate to if no history exists (default: '/')\n * @param options.delta - Number of steps to go back (default: -1)\n * @returns Go back API with goBack function and canGoBack flag\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { goBack, canGoBack } = useGoBack({ fallback: '/home' });\n *\n * return (\n * <button\n * onClick={() => goBack()}\n * disabled={!canGoBack}\n * >\n * Go Back\n * </button>\n * );\n * }\n * ```\n */\nexport function useGoBack(options: GoBackOptions = {}): GoBackAPI {\n const { fallback = '/', delta = -1 } = options;\n const { navigateTo } = useNavigation();\n const historyLength = useRef<number>(isBrowser() ? window.history.length : 0);\n const initialLength = useRef<number>(0);\n\n useEffect(() => {\n if (!isBrowser()) return;\n\n // Store initial history length\n if (initialLength.current === 0) {\n initialLength.current = window.history.length;\n }\n\n // Update current length\n historyLength.current = window.history.length;\n }, []);\n\n // Track if we can go back\n const canGoBack = isBrowser() && window.history.length > initialLength.current;\n\n const goBack = useCallback(\n (customDelta?: number) => {\n if (!isBrowser()) return;\n\n const steps = customDelta ?? delta;\n\n // Check if we have enough history to go back\n if (window.history.length > Math.abs(steps)) {\n try {\n window.history.go(steps);\n } catch (error) {\n console.warn('[page-speed/router] Failed to go back:', error);\n // Fall back to navigation\n navigateTo({ path: fallback, replace: true });\n }\n } else {\n // No sufficient history, use fallback\n navigateTo({ path: fallback, replace: true });\n }\n },\n [delta, fallback, navigateTo]\n );\n\n return { goBack, canGoBack };\n}\n\n/**\n * Hook that provides a simple back function\n * Convenience wrapper around useGoBack\n *\n * @param fallback - Fallback path if no history\n * @returns Go back function\n */\nexport function useBack(fallback: string = '/') {\n const { goBack } = useGoBack({ fallback });\n return goBack;\n}\n\n/**\n * Hook that provides forward navigation\n *\n * @returns Go forward function\n */\nexport function useGoForward() {\n return useCallback(() => {\n if (!isBrowser()) return;\n\n try {\n window.history.forward();\n } catch (error) {\n console.warn('[page-speed/router] Failed to go forward:', error);\n }\n }, []);\n}","import { PathMatch, RouteParams } from '../types';\n\n/**\n * Cache for compiled regex patterns to avoid recompilation\n */\nconst patternCache = new Map<string, RegExp>();\n\n/**\n * Matches a pathname against a route pattern\n * @param pathname - The current pathname to match\n * @param pattern - The route pattern (e.g., '/blog/:slug')\n * @param exact - Whether to require an exact match\n * @returns Match result with extracted parameters\n */\nexport function matchPath(\n pathname: string,\n pattern: string,\n exact: boolean = false\n): PathMatch {\n const result: PathMatch = {\n isMatch: false,\n params: {},\n path: pathname,\n };\n\n // Handle root path edge case\n if (pattern === '/' && exact) {\n result.isMatch = pathname === '/';\n return result;\n }\n\n // Get or create regex for this pattern\n const regex = getPatternRegex(pattern, exact);\n const match = pathname.match(regex);\n\n if (match) {\n result.isMatch = true;\n\n // Extract named groups as params\n if (match.groups) {\n result.params = match.groups;\n }\n }\n\n return result;\n}\n\n/**\n * Gets or creates a regex for a route pattern\n * @param pattern - The route pattern\n * @param exact - Whether to match exactly\n * @returns Compiled RegExp\n */\nfunction getPatternRegex(pattern: string, exact: boolean): RegExp {\n const cacheKey = `${pattern}:${exact}`;\n\n if (patternCache.has(cacheKey)) {\n return patternCache.get(cacheKey)!;\n }\n\n // Escape special regex characters except for param markers\n let regexPattern = pattern\n // Escape special characters\n .replace(/[.+*?^${}()|[\\]\\\\]/g, '\\\\$&')\n // Replace :param with named capture groups\n .replace(/:(\\w+)/g, '(?<$1>[^/]+)');\n\n // Add anchors based on exact match requirement\n if (exact) {\n regexPattern = `^${regexPattern}$`;\n } else {\n regexPattern = `^${regexPattern}`;\n }\n\n const regex = new RegExp(regexPattern);\n patternCache.set(cacheKey, regex);\n\n return regex;\n}\n\n/**\n * Extracts route parameters from a path given a pattern\n * @param pathname - The current pathname\n * @param pattern - The route pattern with parameters\n * @returns Extracted parameters\n */\nexport function extractParams(pathname: string, pattern: string): RouteParams {\n const match = matchPath(pathname, pattern, true);\n return match.params;\n}\n\n/**\n * Checks if a path matches any of the provided patterns\n * @param pathname - The path to check\n * @param patterns - Array of patterns to match against\n * @returns The first matching pattern and its params, or null\n */\nexport function findMatchingRoute(\n pathname: string,\n patterns: { path: string; exact?: boolean }[]\n): PathMatch | null {\n for (const route of patterns) {\n const match = matchPath(pathname, route.path, route.exact);\n if (match.isMatch) {\n return match;\n }\n }\n return null;\n}\n\n/**\n * Builds a path from a pattern and params\n * @param pattern - The route pattern\n * @param params - The parameters to substitute\n * @returns The built path\n */\nexport function buildPath(pattern: string, params: RouteParams): string {\n let path = pattern;\n\n Object.entries(params).forEach(([key, value]) => {\n path = path.replace(`:${key}`, String(value));\n });\n\n return path;\n}\n\n/**\n * Normalizes a path by removing trailing slashes (except for root)\n * @param path - The path to normalize\n * @returns Normalized path\n */\nexport function normalizePath(path: string): string {\n if (path === '/') return path;\n return path.replace(/\\/+$/, '');\n}","import { RouteParams } from '../types';\n\n/**\n * Parses route parameters from a pathname given a pattern\n * This is a simpler alternative to matchPath for basic param extraction\n * @param pathname - The current pathname\n * @param pattern - The route pattern with parameters\n * @returns Extracted parameters\n */\nexport function parseParams(pathname: string, pattern: string): RouteParams {\n const params: RouteParams = {};\n\n // Split paths into segments\n const patternParts = pattern.split('/').filter(Boolean);\n const pathParts = pathname.split('/').filter(Boolean);\n\n // If lengths don't match, no params to extract\n if (patternParts.length !== pathParts.length) {\n return params;\n }\n\n // Extract params from matching segments\n patternParts.forEach((part, index) => {\n if (part.startsWith(':')) {\n const paramName = part.slice(1);\n params[paramName] = decodeURIComponent(pathParts[index]);\n }\n });\n\n return params;\n}\n\n/**\n * Serializes an object of parameters into a query string\n * @param params - The parameters to serialize\n * @returns Query string (without leading ?)\n */\nexport function serializeParams(params: Record<string, any>): string {\n const searchParams = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value !== null && value !== undefined) {\n searchParams.append(key, String(value));\n }\n });\n\n return searchParams.toString();\n}\n\n/**\n * Parses query string parameters from a search string\n * @param search - The search string (with or without leading ?)\n * @returns Parsed parameters\n */\nexport function parseQueryString(search: string): Record<string, string> {\n const params: Record<string, string> = {};\n\n // Remove leading ? if present\n const queryString = search.startsWith('?') ? search.slice(1) : search;\n\n if (!queryString) return params;\n\n const searchParams = new URLSearchParams(queryString);\n\n searchParams.forEach((value, key) => {\n params[key] = value;\n });\n\n return params;\n}\n\n/**\n * Merges route params and query params into a single object\n * @param routeParams - Parameters from the route pattern\n * @param queryParams - Parameters from the query string\n * @returns Merged parameters (query params override route params)\n */\nexport function mergeParams(\n routeParams: RouteParams,\n queryParams: Record<string, string>\n): RouteParams {\n return { ...routeParams, ...queryParams };\n}\n\n/**\n * Validates that required params are present\n * @param params - The parameters to validate\n * @param required - Array of required parameter names\n * @returns True if all required params are present\n */\nexport function validateParams(\n params: RouteParams,\n required: string[]\n): boolean {\n return required.every(key => key in params && params[key] !== '');\n}","import { useContext, useMemo } from 'react';\nimport { RouterContext } from '../context/RouterContext';\nimport { RouteParams } from '../types';\nimport { useUrl } from './useUrl';\nimport { matchPath } from '../utils/matchPath';\nimport { parseQueryString, mergeParams } from '../utils/parseParams';\n\n/**\n * Hook that extracts route parameters from the current URL\n * Requires RouterProvider with defined routes\n *\n * @param includeQuery - Whether to include query string parameters (default: false)\n * @returns Route parameters object\n *\n * @example\n * ```tsx\n * // With route pattern: '/blog/:slug'\n * // Current URL: '/blog/hello-world?page=2'\n *\n * function BlogPost() {\n * const params = useParams(); // { slug: 'hello-world' }\n * const allParams = useParams(true); // { slug: 'hello-world', page: '2' }\n *\n * return <div>Post: {params.slug}</div>;\n * }\n * ```\n */\nexport function useParams(includeQuery: boolean = false): RouteParams {\n const context = useContext(RouterContext);\n const { pathname, search } = useUrl();\n\n return useMemo(() => {\n // Base case - no context or routes\n if (!context || !context.routes || context.routes.length === 0) {\n if (includeQuery && search) {\n return parseQueryString(search);\n }\n return {};\n }\n\n const routes = context.routes;\n let routeParams: RouteParams = {};\n\n // Find matching route\n for (const route of routes) {\n const match = matchPath(pathname, route.path, route.exact);\n if (match.isMatch) {\n routeParams = match.params;\n break;\n }\n }\n\n // Include query params if requested\n if (includeQuery && search) {\n const queryParams = parseQueryString(search);\n return mergeParams(routeParams, queryParams);\n }\n\n return routeParams;\n }, [context, pathname, search, includeQuery]);\n}\n\n/**\n * Hook that extracts a single route parameter\n * Convenience wrapper around useParams\n *\n * @param paramName - Name of the parameter to extract\n * @returns Parameter value or undefined\n *\n * @example\n * ```tsx\n * function BlogPost() {\n * const slug = useParam('slug');\n * return <div>Post: {slug}</div>;\n * }\n * ```\n */\nexport function useParam(paramName: string): string | undefined {\n const params = useParams();\n return params[paramName];\n}\n\n/**\n * Hook that provides both route and query parameters separately\n *\n * @returns Object with route and query parameters\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { routeParams, queryParams } = useAllParams();\n *\n * return (\n * <div>\n * Route: {JSON.stringify(routeParams)}\n * Query: {JSON.stringify(queryParams)}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAllParams(): {\n routeParams: RouteParams;\n queryParams: Record<string, string>;\n} {\n const context = useContext(RouterContext);\n const { pathname, search } = useUrl();\n\n return useMemo(() => {\n let routeParams: RouteParams = {};\n\n // Extract route params if routes are defined\n if (context?.routes) {\n for (const route of context.routes) {\n const match = matchPath(pathname, route.path, route.exact);\n if (match.isMatch) {\n routeParams = match.params;\n break;\n }\n }\n }\n\n // Extract query params\n const queryParams = search ? parseQueryString(search) : {};\n\n return { routeParams, queryParams };\n }, [context, pathname, search]);\n}","import { useMemo } from 'react';\nimport { useUrl } from './useUrl';\nimport { matchPath } from '../utils/matchPath';\nimport { PathMatch } from '../types';\n\n/**\n * Hook that tests if the current path matches a pattern\n *\n * @param pattern - Route pattern to match against\n * @param exact - Whether to require an exact match (default: false)\n * @returns Match result with params\n *\n * @example\n * ```tsx\n * function BlogSection() {\n * const match = useRouteMatch('/blog/:slug');\n *\n * if (match.isMatch) {\n * return <BlogPost slug={match.params.slug} />;\n * }\n *\n * return <BlogList />;\n * }\n * ```\n */\nexport function useRouteMatch(pattern: string, exact: boolean = false): PathMatch {\n const { pathname } = useUrl();\n\n return useMemo(() => {\n return matchPath(pathname, pattern, exact);\n }, [pathname, pattern, exact]);\n}\n\n/**\n * Hook that checks if a route is currently active\n * Convenience wrapper around useRouteMatch\n *\n * @param pattern - Route pattern to check\n * @param exact - Whether to require an exact match\n * @returns Boolean indicating if route is active\n *\n * @example\n * ```tsx\n * function NavLink({ to, children }) {\n * const isActive = useIsActive(to);\n *\n * return (\n * <a\n * href={to}\n * className={isActive ? 'active' : ''}\n * >\n * {children}\n * </a>\n * );\n * }\n * ```\n */\nexport function useIsActive(pattern: string, exact: boolean = false): boolean {\n const match = useRouteMatch(pattern, exact);\n return match.isMatch;\n}\n\n/**\n * Hook that matches multiple patterns and returns the first match\n *\n * @param patterns - Array of route patterns\n * @returns First matching pattern or null\n *\n * @example\n * ```tsx\n * function ContentArea() {\n * const match = useMultiMatch([\n * '/blog/:slug',\n * '/products/:id',\n * '/about'\n * ]);\n *\n * if (match?.path.startsWith('/blog')) {\n * return <BlogPost />;\n * }\n * // ...\n * }\n * ```\n */\nexport function useMultiMatch(\n patterns: Array<{ pattern: string; exact?: boolean }>\n): PathMatch | null {\n const { pathname } = useUrl();\n\n return useMemo(() => {\n for (const { pattern, exact = false } of patterns) {\n const match = matchPath(pathname, pattern, exact);\n if (match.isMatch) {\n return match;\n }\n }\n return null;\n }, [pathname, patterns]);\n}","import { useEffect, useRef } from 'react';\nimport { PageView, UsePageViewsOptions } from '../types';\nimport { isBrowser } from '../utils/ssrSafe';\n\n/**\n * Hook that observes page views: fires once for the initial load, then on\n * every client-side navigation (programmatic `routechange` and browser\n * back/forward `popstate`).\n *\n * Analytics-backend agnostic — pair it with any tracking call. For the\n * DashTrack analytics engine, use `usePageViewAnalytics` /\n * `<PageViewAnalytics />` instead, which are built on this hook.\n *\n * Consecutive events for the same pathname are deduped: `navigateTo`\n * dispatches both `popstate` and `routechange` for a single navigation, and\n * hash-only changes keep the same pathname — neither produces a duplicate\n * page view.\n *\n * Works outside of RouterProvider and is SSR-safe (no-ops on the server).\n *\n * @example\n * ```tsx\n * usePageViews(({ path, isInitial }) => {\n * myAnalytics.track('page_view', { path, isInitial });\n * });\n * ```\n */\nexport function usePageViews(\n onPageView: (view: PageView) => void,\n options: UsePageViewsOptions = {}\n): void {\n const { enabled = true } = options;\n\n // Keep the latest callback without re-subscribing listeners on each render.\n const callbackRef = useRef(onPageView);\n callbackRef.current = onPageView;\n\n // Survives re-runs of the effect so toggling `enabled` or remounting the\n // effect never double-fires the current path.\n const lastPathRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!enabled || !isBrowser()) return;\n\n const track = (isInitial: boolean) => {\n const path = window.location.pathname;\n if (lastPathRef.current === path) return;\n\n const previousPath = lastPathRef.current;\n lastPathRef.current = path;\n callbackRef.current({ path, previousPath, isInitial });\n };\n\n track(true);\n\n const handleNavigation = () => track(false);\n window.addEventListener('popstate', handleNavigation);\n window.addEventListener('routechange', handleNavigation);\n\n return () => {\n window.removeEventListener('popstate', handleNavigation);\n window.removeEventListener('routechange', handleNavigation);\n };\n }, [enabled]);\n}\n","import { PageViewDevice } from '../types';\nimport { isBrowser } from '../utils/ssrSafe';\n\nconst MOBILE_UA_PATTERN =\n /android|webos|iphone|ipad|ipod|blackberry|bb\\d+|meego|iemobile|opera m(ob|in)i|windows phone|mobile/i;\n\n/**\n * Detects the visitor's device class from the user agent.\n * Matches the device enum accepted by the DashTrack analytics engine\n * ('mobile' | 'desktop' | 'unknown'); the backend coerces anything\n * else to 'unknown'.\n *\n * SSR-safe: returns 'unknown' outside the browser.\n */\nexport function getDeviceType(): PageViewDevice {\n if (!isBrowser() || typeof navigator === 'undefined') return 'unknown';\n\n try {\n const ua = navigator.userAgent || '';\n if (!ua) return 'unknown';\n return MOBILE_UA_PATTERN.test(ua) ? 'mobile' : 'desktop';\n } catch {\n return 'unknown';\n }\n}\n","import { useRef } from 'react';\nimport {\n PageViewAnalyticsOptions,\n PageViewAnalyticsPayload,\n} from '../types';\nimport { getDeviceType } from './getDeviceType';\nimport { usePageViews } from './usePageViews';\n\nconst DEFAULT_API_BASE_URL = 'https://api.dashtrack.com';\nconst ENDPOINT_PATH = '/website_page_views';\n\n/**\n * Pages are stored under canonical no-trailing-slash slugs ('/about'), but\n * remain reachable at '/about/' — normalize so attribution always matches.\n */\nfunction normalizePathname(path: string): string {\n if (path.length > 1 && path.endsWith('/')) {\n return path.replace(/\\/+$/, '') || '/';\n }\n return path;\n}\n\n/**\n * Fire-and-forget delivery. Analytics must never break the host site, so\n * every failure mode (no fetch, network error, non-2xx) is swallowed.\n * `keepalive` lets in-flight requests survive a full page unload.\n */\nfunction sendPageView(\n apiBaseUrl: string,\n payload: PageViewAnalyticsPayload\n): void {\n try {\n if (typeof fetch !== 'function') return;\n\n fetch(`${apiBaseUrl.replace(/\\/+$/, '')}${ENDPOINT_PATH}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n // The endpoint requires the payload nested under `website_page_view`.\n // Nest explicitly rather than relying on Rails wrap_parameters.\n body: JSON.stringify({ website_page_view: payload }),\n keepalive: true,\n credentials: 'omit',\n mode: 'cors',\n }).catch(() => undefined);\n } catch {\n // Never propagate analytics failures.\n }\n}\n\n/**\n * Hook that reports page views to the DashTrack analytics engine\n * (`POST /website_page_views`): one for the initial load, then one per\n * client-side navigation.\n *\n * Opt-in: consumers that never call this hook are completely unaffected\n * (tree-shaken away). No-ops during SSR, when `enabled` is false, or when\n * `websiteToken` is absent.\n *\n * The visitor IP is intentionally not sent — the backend derives it from\n * the request. `traffic_source` defaults to `document.referrer`.\n *\n * @example\n * ```tsx\n * usePageViewAnalytics({ websiteToken: website.token });\n * ```\n */\nexport function usePageViewAnalytics(options: PageViewAnalyticsOptions): void {\n const {\n websiteToken,\n apiBaseUrl = DEFAULT_API_BASE_URL,\n category = 'webpage',\n enabled = true,\n transformPayload,\n } = options;\n\n // Latest values without re-subscribing the underlying listeners.\n const optionsRef = useRef({ websiteToken, apiBaseUrl, category, transformPayload });\n optionsRef.current = { websiteToken, apiBaseUrl, category, transformPayload };\n\n usePageViews(\n (view) => {\n const current = optionsRef.current;\n if (!current.websiteToken) return;\n\n const payload: PageViewAnalyticsPayload = {\n path: normalizePathname(view.path),\n // IMPORTANT: the wire param is named website_id, but the analytics\n // endpoint resolves it via websites.token (the UUID token), NOT the\n // numeric websites.id.\n website_id: current.websiteToken,\n category: current.category,\n device: getDeviceType(),\n traffic_source:\n typeof document !== 'undefined' && document.referrer\n ? document.referrer\n : undefined,\n };\n\n const finalPayload = current.transformPayload\n ? current.transformPayload(payload, view)\n : payload;\n\n if (!finalPayload) return;\n\n sendPageView(current.apiBaseUrl, finalPayload);\n },\n { enabled: Boolean(enabled && websiteToken) }\n );\n}\n\n/**\n * Declarative wrapper around `usePageViewAnalytics`. Renders nothing.\n * Drop it anywhere in the tree (inside or outside RouterProvider):\n *\n * @example\n * ```tsx\n * <RouterProvider>\n * <PageViewAnalytics websiteToken={website.token} />\n * <App />\n * </RouterProvider>\n * ```\n */\nexport function PageViewAnalytics(options: PageViewAnalyticsOptions): null {\n usePageViewAnalytics(options);\n return null;\n}\n","import React, { useEffect, useMemo, useCallback } from 'react';\nimport { RouterContext } from './RouterContext';\nimport { RouterProviderProps } from '../types';\nimport { isBrowser } from '../utils/ssrSafe';\n\n/**\n * RouterProvider component that provides routing context to child components\n * This is optional - many routing hooks work without a provider\n */\nexport function RouterProvider({\n children,\n initialPath,\n routes = [],\n scrollBehavior = 'smooth',\n onNavigate,\n}: RouterProviderProps) {\n // Memoize context value to prevent unnecessary re-renders\n const contextValue = useMemo(\n () => ({\n routes,\n initialPath,\n scrollBehavior,\n onNavigate,\n }),\n [routes, initialPath, scrollBehavior, onNavigate]\n );\n\n // Handle navigation events\n const handleNavigation = useCallback(() => {\n if (!isBrowser() || !onNavigate) return;\n onNavigate(window.location.pathname);\n }, [onNavigate]);\n\n useEffect(() => {\n if (!isBrowser() || !onNavigate) return;\n\n // Call onNavigate for initial path if it matches current location\n if (initialPath && window.location.pathname === initialPath) {\n onNavigate(initialPath);\n }\n\n // Listen for browser navigation events\n const handlePopState = () => {\n handleNavigation();\n };\n\n // Custom event for programmatic navigation\n const handleRouteChange = (event: CustomEvent) => {\n if (event.detail?.path) {\n onNavigate(event.detail.path);\n }\n };\n\n window.addEventListener('popstate', handlePopState);\n window.addEventListener('routechange', handleRouteChange as EventListener);\n\n // Cleanup\n return () => {\n window.removeEventListener('popstate', handlePopState);\n window.removeEventListener('routechange', handleRouteChange as EventListener);\n };\n }, [initialPath, onNavigate, handleNavigation]);\n\n return (\n <RouterContext.Provider value={contextValue}>\n {children}\n </RouterContext.Provider>\n );\n}\n\n/**\n * Type guard to check if RouterProvider is being used\n */\nexport function isRouterProviderAvailable(): boolean {\n if (!isBrowser()) return false;\n\n // Check if there's a RouterContext in the tree\n try {\n const context = React.useContext(RouterContext);\n return context !== null;\n } catch {\n return false;\n }\n}"],"names":["RouterContext","createContext","displayName","isBrowser","window","document","ssrSafe","clientFn","serverValue","DEFAULT_URL","href","origin","protocol","host","hostname","port","pathname","search","hash","useUrl","url","setUrl","useState","location","useEffect","updateUrl","addEventListener","handleRouteChange","removeEventListener","scrollToAnchor","anchor","smooth","offset","id","replace","element","getElementById","fallbackElement","querySelector","scrollToElement","console","warn","offsetPosition","getBoundingClientRect","top","pageYOffset","scrollTo","behavior","history","replaceState","currentState","state","setTimeout","focus","preventScroll","activeElement","originalTabIndex","getAttribute","setAttribute","removeAttribute","once","scrollToTop","useNavigation","context","useContext","scrollBehavior","navigateTo","useCallback","options","navOptions","path","fullPath","currentPath","currentHash","dispatchEvent","CustomEvent","detail","pushState","PopStateEvent","reload","useGoBack","fallback","delta","historyLength","useRef","length","initialLength","current","canGoBack","goBack","customDelta","steps","Math","abs","go","error","patternCache","Map","matchPath","pattern","exact","result","isMatch","params","regex","cacheKey","has","get","regexPattern","RegExp","set","getPatternRegex","match","groups","parseQueryString","queryString","startsWith","slice","URLSearchParams","forEach","value","key","mergeParams","routeParams","queryParams","useParams","includeQuery","useMemo","routes","route","useRouteMatch","usePageViews","onPageView","enabled","callbackRef","lastPathRef","track","isInitial","previousPath","handleNavigation","MOBILE_UA_PATTERN","getDeviceType","navigator","ua","userAgent","test","DEFAULT_API_BASE_URL","usePageViewAnalytics","websiteToken","apiBaseUrl","category","transformPayload","optionsRef","view","payload","endsWith","website_id","device","traffic_source","referrer","finalPayload","fetch","method","headers","body","JSON","stringify","website_page_view","keepalive","credentials","mode","catch","sendPageView","Boolean","children","initialPath","onNavigate","contextValue","handlePopState","event","_a","Provider","fn","args","Object","entries","String","patterns","documentElement","scrollTop","patternParts","split","filter","pathParts","part","index","paramName","decodeURIComponent","accessor","searchParams","append","toString","forward","currentParams","delete","newPath","required","every"],"mappings":"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"}

@@ -21,2 +21,3 @@ "use strict";

__export(src_exports, {
PageViewAnalytics: () => import_usePageViewAnalytics.PageViewAnalytics,
RouterContext: () => import_RouterContext.RouterContext,

@@ -28,2 +29,3 @@ RouterProvider: () => import_RouterProvider.RouterProvider,

findMatchingRoute: () => import_matchPath.findMatchingRoute,
getDeviceType: () => import_getDeviceType.getDeviceType,
getScrollPosition: () => import_scrollToAnchor.getScrollPosition,

@@ -51,2 +53,4 @@ isBrowser: () => import_ssrSafe.isBrowser,

useNavigation: () => import_useNavigation.useNavigation,
usePageViewAnalytics: () => import_usePageViewAnalytics.usePageViewAnalytics,
usePageViews: () => import_usePageViews.usePageViews,
useParam: () => import_useParams.useParam,

@@ -69,2 +73,5 @@ useParams: () => import_useParams.useParams,

var import_useRouteMatch = require("./hooks/useRouteMatch");
var import_usePageViews = require("./analytics/usePageViews");
var import_usePageViewAnalytics = require("./analytics/usePageViewAnalytics");
var import_getDeviceType = require("./analytics/getDeviceType");
var import_scrollToAnchor = require("./utils/scrollToAnchor");

@@ -76,2 +83,3 @@ var import_matchPath = require("./utils/matchPath");

0 && (module.exports = {
PageViewAnalytics,
RouterContext,

@@ -83,2 +91,3 @@ RouterProvider,

findMatchingRoute,
getDeviceType,
getScrollPosition,

@@ -106,2 +115,4 @@ isBrowser,

useNavigation,
usePageViewAnalytics,
usePageViews,
useParam,

@@ -108,0 +119,0 @@ useParams,

{
"version": 3,
"sources": ["../src/index.ts"],
"sourcesContent": ["/**\n * @page-speed/router\n * Lightweight SSR-compatible routing for the PageSpeed ecosystem\n */\n\n// Context & Provider\nexport { RouterProvider } from './context/RouterProvider';\nexport { RouterContext } from './context/RouterContext';\n\n// Core Hooks\nexport {\n useUrl,\n usePathname,\n useSearchParams,\n useHash,\n} from './hooks/useUrl';\n\nexport {\n useNavigation,\n useNavigate,\n useUpdateSearchParams,\n} from './hooks/useNavigation';\n\nexport {\n useGoBack,\n useBack,\n useGoForward,\n} from './hooks/useGoBack';\n\nexport {\n useParams,\n useParam,\n useAllParams,\n} from './hooks/useParams';\n\nexport {\n useRouteMatch,\n useIsActive,\n useMultiMatch,\n} from './hooks/useRouteMatch';\n\n// Utilities\nexport {\n scrollToAnchor,\n scrollToTop,\n getScrollPosition,\n} from './utils/scrollToAnchor';\n\nexport {\n matchPath,\n extractParams,\n findMatchingRoute,\n buildPath,\n normalizePath,\n} from './utils/matchPath';\n\nexport {\n parseParams,\n serializeParams,\n parseQueryString,\n mergeParams,\n validateParams,\n} from './utils/parseParams';\n\nexport {\n isBrowser,\n isSSR,\n ssrSafe,\n browserOnly,\n safeWindow,\n} from './utils/ssrSafe';\n\n// Types\nexport type {\n UrlState,\n NavigateOptions,\n NavigationAPI,\n GoBackOptions,\n GoBackAPI,\n RouteParams,\n Route,\n RouterContextValue,\n RouterProviderProps,\n PathMatch,\n ScrollBehavior,\n ScrollOptions,\n} from './types';\n"],
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,4BAA+B;AAC/B,2BAA8B;AAG9B,oBAKO;AAEP,2BAIO;AAEP,uBAIO;AAEP,uBAIO;AAEP,2BAIO;AAGP,4BAIO;AAEP,uBAMO;AAEP,yBAMO;AAEP,qBAMO;",
"sourcesContent": ["/**\n * @page-speed/router\n * Lightweight SSR-compatible routing for the PageSpeed ecosystem\n */\n\n// Context & Provider\nexport { RouterProvider } from './context/RouterProvider';\nexport { RouterContext } from './context/RouterContext';\n\n// Core Hooks\nexport {\n useUrl,\n usePathname,\n useSearchParams,\n useHash,\n} from './hooks/useUrl';\n\nexport {\n useNavigation,\n useNavigate,\n useUpdateSearchParams,\n} from './hooks/useNavigation';\n\nexport {\n useGoBack,\n useBack,\n useGoForward,\n} from './hooks/useGoBack';\n\nexport {\n useParams,\n useParam,\n useAllParams,\n} from './hooks/useParams';\n\nexport {\n useRouteMatch,\n useIsActive,\n useMultiMatch,\n} from './hooks/useRouteMatch';\n\n// Analytics (opt-in \u2014 tree-shaken away unless imported)\nexport { usePageViews } from './analytics/usePageViews';\nexport {\n usePageViewAnalytics,\n PageViewAnalytics,\n} from './analytics/usePageViewAnalytics';\nexport { getDeviceType } from './analytics/getDeviceType';\n\n// Utilities\nexport {\n scrollToAnchor,\n scrollToTop,\n getScrollPosition,\n} from './utils/scrollToAnchor';\n\nexport {\n matchPath,\n extractParams,\n findMatchingRoute,\n buildPath,\n normalizePath,\n} from './utils/matchPath';\n\nexport {\n parseParams,\n serializeParams,\n parseQueryString,\n mergeParams,\n validateParams,\n} from './utils/parseParams';\n\nexport {\n isBrowser,\n isSSR,\n ssrSafe,\n browserOnly,\n safeWindow,\n} from './utils/ssrSafe';\n\n// Types\nexport type {\n UrlState,\n NavigateOptions,\n NavigationAPI,\n GoBackOptions,\n GoBackAPI,\n RouteParams,\n Route,\n RouterContextValue,\n RouterProviderProps,\n PathMatch,\n ScrollBehavior,\n ScrollOptions,\n PageView,\n PageViewDevice,\n UsePageViewsOptions,\n PageViewAnalyticsPayload,\n PageViewAnalyticsOptions,\n} from './types';\n"],
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,4BAA+B;AAC/B,2BAA8B;AAG9B,oBAKO;AAEP,2BAIO;AAEP,uBAIO;AAEP,uBAIO;AAEP,2BAIO;AAGP,0BAA6B;AAC7B,kCAGO;AACP,2BAA8B;AAG9B,4BAIO;AAEP,uBAMO;AAEP,yBAMO;AAEP,qBAMO;",
"names": []
}

@@ -12,2 +12,5 @@ /**

export { useRouteMatch, useIsActive, useMultiMatch, } from './hooks/useRouteMatch';
export { usePageViews } from './analytics/usePageViews';
export { usePageViewAnalytics, PageViewAnalytics, } from './analytics/usePageViewAnalytics';
export { getDeviceType } from './analytics/getDeviceType';
export { scrollToAnchor, scrollToTop, getScrollPosition, } from './utils/scrollToAnchor';

@@ -17,3 +20,3 @@ export { matchPath, extractParams, findMatchingRoute, buildPath, normalizePath, } from './utils/matchPath';

export { isBrowser, isSSR, ssrSafe, browserOnly, safeWindow, } from './utils/ssrSafe';
export type { UrlState, NavigateOptions, NavigationAPI, GoBackOptions, GoBackAPI, RouteParams, Route, RouterContextValue, RouterProviderProps, PathMatch, ScrollBehavior, ScrollOptions, } from './types';
export type { UrlState, NavigateOptions, NavigationAPI, GoBackOptions, GoBackAPI, RouteParams, Route, RouterContextValue, RouterProviderProps, PathMatch, ScrollBehavior, ScrollOptions, PageView, PageViewDevice, UsePageViewsOptions, PageViewAnalyticsPayload, PageViewAnalyticsOptions, } from './types';
//# sourceMappingURL=index.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAGxD,OAAO,EACL,MAAM,EACN,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,aAAa,EACb,WAAW,EACX,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,SAAS,EACT,OAAO,EACP,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,SAAS,EACT,QAAQ,EACR,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,cAAc,EACd,WAAW,EACX,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,aAAa,GACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,cAAc,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,SAAS,EACT,KAAK,EACL,OAAO,EACP,WAAW,EACX,UAAU,GACX,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EACV,QAAQ,EACR,eAAe,EACf,aAAa,EACb,aAAa,EACb,SAAS,EACT,WAAW,EACX,KAAK,EACL,kBAAkB,EAClB,mBAAmB,EACnB,SAAS,EACT,cAAc,EACd,aAAa,GACd,MAAM,SAAS,CAAC"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAGxD,OAAO,EACL,MAAM,EACN,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,aAAa,EACb,WAAW,EACX,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,SAAS,EACT,OAAO,EACP,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,SAAS,EACT,QAAQ,EACR,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG1D,OAAO,EACL,cAAc,EACd,WAAW,EACX,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,aAAa,GACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,cAAc,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,SAAS,EACT,KAAK,EACL,OAAO,EACP,WAAW,EACX,UAAU,GACX,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EACV,QAAQ,EACR,eAAe,EACf,aAAa,EACb,aAAa,EACb,SAAS,EACT,WAAW,EACX,KAAK,EACL,kBAAkB,EAClB,mBAAmB,EACnB,SAAS,EACT,cAAc,EACd,aAAa,EACb,QAAQ,EACR,cAAc,EACd,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,SAAS,CAAC"}

@@ -14,2 +14,6 @@ /**

export { useRouteMatch, useIsActive, useMultiMatch, } from './hooks/useRouteMatch.js';
// Analytics (opt-in — tree-shaken away unless imported)
export { usePageViews } from './analytics/usePageViews.js';
export { usePageViewAnalytics, PageViewAnalytics, } from './analytics/usePageViewAnalytics.js';
export { getDeviceType } from './analytics/getDeviceType.js';
// Utilities

@@ -16,0 +20,0 @@ export { scrollToAnchor, scrollToTop, getScrollPosition, } from './utils/scrollToAnchor.js';

@@ -1,1 +0,1 @@

{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAqB;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAExD,aAAa;AACb,OAAO,EACL,MAAM,EACN,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,aAAa,EACb,WAAW,EACX,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,SAAS,EACT,OAAO,EACP,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,SAAS,EACT,QAAQ,EACR,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,uBAAuB,CAAC;AAE/B,YAAY;AACZ,OAAO,EACL,cAAc,EACd,WAAW,EACX,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,aAAa,GACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,cAAc,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,SAAS,EACT,KAAK,EACL,OAAO,EACP,WAAW,EACX,UAAU,GACX,MAAM,iBAAiB,CAAC"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAqB;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAExD,aAAa;AACb,OAAO,EACL,MAAM,EACN,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,aAAa,EACb,WAAW,EACX,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,SAAS,EACT,OAAO,EACP,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,SAAS,EACT,QAAQ,EACR,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,uBAAuB,CAAC;AAE/B,wDAAwD;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D,YAAY;AACZ,OAAO,EACL,cAAc,EACd,WAAW,EACX,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,aAAa,GACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,cAAc,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,SAAS,EACT,KAAK,EACL,OAAO,EACP,WAAW,EACX,UAAU,GACX,MAAM,iBAAiB,CAAC"}
{
"version": 3,
"sources": ["../src/types.ts"],
"sourcesContent": ["// Type definitions for @page-speed/router\nimport { ReactNode } from 'react';\n\nexport interface UrlState {\n href: string;\n origin: string;\n protocol: string;\n host: string;\n hostname: string;\n port: string;\n pathname: string;\n search: string;\n hash: string;\n}\n\nexport interface NavigateOptions {\n path: string;\n anchor?: string;\n replace?: boolean;\n smooth?: boolean;\n state?: any;\n}\n\nexport interface NavigationAPI {\n navigateTo: (options: NavigateOptions | string) => void;\n replace: (path: string) => void;\n reload: () => void;\n}\n\nexport interface GoBackOptions {\n fallback?: string;\n delta?: number;\n}\n\nexport interface GoBackAPI {\n goBack: (delta?: number) => void;\n canGoBack: boolean;\n}\n\nexport type RouteParams = Record<string, string>;\n\nexport interface Route {\n path: string;\n exact?: boolean;\n}\n\nexport interface RouterContextValue {\n routes?: Route[];\n initialPath?: string;\n scrollBehavior?: ScrollBehavior;\n onNavigate?: (path: string) => void;\n}\n\nexport interface RouterProviderProps {\n children: ReactNode;\n initialPath?: string;\n routes?: Route[];\n scrollBehavior?: ScrollBehavior;\n onNavigate?: (path: string) => void;\n}\n\nexport interface PathMatch {\n isMatch: boolean;\n params: Record<string, string>;\n path: string;\n}\n\nexport type ScrollBehavior = 'smooth' | 'auto' | 'instant';\n\nexport interface ScrollOptions {\n smooth?: boolean;\n offset?: number;\n}"],
"sourcesContent": ["// Type definitions for @page-speed/router\nimport { ReactNode } from 'react';\n\nexport interface UrlState {\n href: string;\n origin: string;\n protocol: string;\n host: string;\n hostname: string;\n port: string;\n pathname: string;\n search: string;\n hash: string;\n}\n\nexport interface NavigateOptions {\n path: string;\n anchor?: string;\n replace?: boolean;\n smooth?: boolean;\n state?: any;\n}\n\nexport interface NavigationAPI {\n navigateTo: (options: NavigateOptions | string) => void;\n replace: (path: string) => void;\n reload: () => void;\n}\n\nexport interface GoBackOptions {\n fallback?: string;\n delta?: number;\n}\n\nexport interface GoBackAPI {\n goBack: (delta?: number) => void;\n canGoBack: boolean;\n}\n\nexport type RouteParams = Record<string, string>;\n\nexport interface Route {\n path: string;\n exact?: boolean;\n}\n\nexport interface RouterContextValue {\n routes?: Route[];\n initialPath?: string;\n scrollBehavior?: ScrollBehavior;\n onNavigate?: (path: string) => void;\n}\n\nexport interface RouterProviderProps {\n children: ReactNode;\n initialPath?: string;\n routes?: Route[];\n scrollBehavior?: ScrollBehavior;\n onNavigate?: (path: string) => void;\n}\n\nexport interface PathMatch {\n isMatch: boolean;\n params: Record<string, string>;\n path: string;\n}\n\nexport type ScrollBehavior = 'smooth' | 'auto' | 'instant';\n\nexport interface ScrollOptions {\n smooth?: boolean;\n offset?: number;\n}\n\n// \u2500\u2500\u2500 Analytics (opt-in) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type PageViewDevice = 'mobile' | 'desktop' | 'unknown';\n\nexport interface PageView {\n /** Pathname of the viewed page */\n path: string;\n /** Pathname of the previously viewed page, null for the initial view */\n previousPath: string | null;\n /** True for the first page view after load/hydration */\n isInitial: boolean;\n}\n\nexport interface UsePageViewsOptions {\n /** Set false to suspend tracking entirely (default true) */\n enabled?: boolean;\n}\n\nexport interface PageViewAnalyticsPayload {\n path: string;\n /**\n * The website's UUID token (websites.token). The analytics endpoint\n * resolves websites by token even though the wire param is `website_id` \u2014\n * never send the numeric website id here.\n */\n website_id: string;\n category: string;\n device: PageViewDevice;\n traffic_source?: string;\n}\n\nexport interface PageViewAnalyticsOptions {\n /**\n * The website's UUID token (websites.token), NOT the numeric website id.\n * Tracking no-ops when absent, so consumers without a token are unaffected.\n */\n websiteToken: string | null | undefined;\n /** Analytics API origin (default https://api.dashtrack.com) */\n apiBaseUrl?: string;\n /** Page view category (default 'webpage') */\n category?: string;\n /** Set false to suspend tracking entirely (default true) */\n enabled?: boolean;\n /**\n * Inspect/extend the payload before it is sent, or return null to skip\n * reporting this particular view.\n */\n transformPayload?: (\n payload: PageViewAnalyticsPayload,\n view: PageView\n ) => PageViewAnalyticsPayload | null;\n}\n"],
"mappings": ";;;;;;;;;;;;;;AAAA;AAAA;",
"names": []
}

@@ -61,2 +61,45 @@ import { ReactNode } from 'react';

}
export type PageViewDevice = 'mobile' | 'desktop' | 'unknown';
export interface PageView {
/** Pathname of the viewed page */
path: string;
/** Pathname of the previously viewed page, null for the initial view */
previousPath: string | null;
/** True for the first page view after load/hydration */
isInitial: boolean;
}
export interface UsePageViewsOptions {
/** Set false to suspend tracking entirely (default true) */
enabled?: boolean;
}
export interface PageViewAnalyticsPayload {
path: string;
/**
* The website's UUID token (websites.token). The analytics endpoint
* resolves websites by token even though the wire param is `website_id` —
* never send the numeric website id here.
*/
website_id: string;
category: string;
device: PageViewDevice;
traffic_source?: string;
}
export interface PageViewAnalyticsOptions {
/**
* The website's UUID token (websites.token), NOT the numeric website id.
* Tracking no-ops when absent, so consumers without a token are unaffected.
*/
websiteToken: string | null | undefined;
/** Analytics API origin (default https://api.dashtrack.com) */
apiBaseUrl?: string;
/** Page view category (default 'webpage') */
category?: string;
/** Set false to suspend tracking entirely (default true) */
enabled?: boolean;
/**
* Inspect/extend the payload before it is sent, or return null to skip
* reporting this particular view.
*/
transformPayload?: (payload: PageViewAnalyticsPayload, view: PageView) => PageViewAnalyticsPayload | null;
}
//# sourceMappingURL=types.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,GAAG,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,KAAK,IAAI,CAAC;IACxD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEjD,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,SAAS,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE3D,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
{
"name": "@page-speed/router",
"version": "1.0.2",
"version": "1.1.0",
"description": "Lightweight SSR-compatible routing for the PageSpeed ecosystem",

@@ -5,0 +5,0 @@ "keywords": [

@@ -332,2 +332,52 @@ # @page-speed/router

Note: `onNavigate` only fires for the initial load when `initialPath` matches
the current location. For reliable page-view tracking (initial load + every
navigation), use the analytics hooks below.
## Page View Analytics (Opt-In)
Fully optional — if you never import these, they are tree-shaken away and the
router behaves exactly as before. Apps that use the router without analytics
(showcases, internal tools) need no changes.
### `usePageViews()` — bring your own backend
Fires once after mount and once per client-side navigation (`routechange` and
browser back/forward). Consecutive duplicate paths are deduped, so the double
event dispatched by `navigateTo` and hash-only changes never double-count.
```tsx
import { usePageViews } from '@page-speed/router';
usePageViews(({ path, previousPath, isInitial }) => {
myAnalytics.track('page_view', { path, isInitial });
});
```
### `usePageViewAnalytics()` / `<PageViewAnalytics />` — DashTrack analytics engine
Reports page views to `POST /website_page_views` on the DashTrack API.
```tsx
import { RouterProvider, PageViewAnalytics } from '@page-speed/router';
<RouterProvider>
<PageViewAnalytics websiteToken={website.token} />
<App />
</RouterProvider>
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `websiteToken` | `string \| null \| undefined` | - | The website's UUID `token` (**not** the numeric website id). Tracking no-ops when absent. |
| `apiBaseUrl` | `string` | `https://api.dashtrack.com` | Analytics API origin |
| `category` | `string` | `'webpage'` | Page view category |
| `enabled` | `boolean` | `true` | Set `false` to suspend tracking (e.g. in development) |
| `transformPayload` | `(payload, view) => payload \| null` | - | Inspect/extend the payload, or return `null` to skip a view |
Delivery details: payload is nested under the `website_page_view` wrapper key,
sent with `keepalive: true` and no credentials; failures are swallowed so
analytics can never break the host site. The visitor IP is derived server-side
from the request — no client-side IP lookup is performed.
## API Reference

@@ -334,0 +384,0 @@