@page-speed/router
Advanced tools
| "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,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":"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"} | ||
| {"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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"} |
+11
-0
@@ -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": [] | ||
| } |
+4
-1
@@ -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"} |
+4
-0
@@ -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": [] | ||
| } |
+43
-0
@@ -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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"} |
+1
-1
| { | ||
| "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": [ |
+50
-0
@@ -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 @@ |
Network access
Supply chain riskThis module accesses the network.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
287212
21.86%117
18.18%3056
19.56%480
11.63%1
Infinity%6
Infinity%