diff --git a/.gitignore b/.gitignore index 98e3153..f4bb6ad 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules coverage dist types +*.tsbuildinfo # Include Rollup config !rollup.config.js diff --git a/CODEBASE_REVIEW.md b/CODEBASE_REVIEW.md new file mode 100644 index 0000000..73ef3b6 --- /dev/null +++ b/CODEBASE_REVIEW.md @@ -0,0 +1,188 @@ +# Codebase Review: react-fathom + +**Date:** 2026-02-08 +**Version:** 0.2.0 + +## Project Overview + +**react-fathom** is a React wrapper library for [Fathom Analytics](https://usefathom.com/), a privacy-first analytics service. It provides: + +- Automatic pageview tracking via declarative components and hooks +- Integrations for 6 frameworks: React, Next.js (App + Pages Router), React Router, Gatsby, TanStack Router, and React Native +- Full TypeScript support with strict typing +- Tree-shakeable builds (UMD, ESM, CJS) +- Debug mode with event stream UI +- React Native support via WebView with offline queuing + +The codebase is well-organized across ~64 source files (~40 implementation + ~24 test files, ~8,300 lines total), with a Rollup build pipeline, Vitest test suite, CircleCI CI/CD, and a full Next.js documentation site. + +--- + +## Architecture & Design (Strong) + +**Strengths:** +- Clean separation of concerns: core provider, framework-specific adapters, hooks, and declarative components are each in their own modules +- Framework-agnostic core (`FathomProvider` + `FathomContext`) with per-framework adapters that compose on top +- Both declarative (``, ``, ``) and imperative (`useFathom()`) APIs +- Proper React patterns: Context, `useCallback`/`useMemo`, ref forwarding, `useImperativeHandle` +- `clientRef` prop allows parent access to the client instance, solving a common context limitation +- Nested provider support with option inheritance (shallow merge) + +**Observations:** +- Multiple framework adapters each implement their own `buildUrl` function with near-identical logic (Next.js utils, React Router, Gatsby, TanStack Router). This is a clear opportunity to extract a shared URL-building utility. + +--- + +## TypeScript & Type Safety (Strong) + +The project uses `"strict": true` throughout with well-defined types exported for consumers. + +**Minor issues:** + +| Location | Issue | +|---|---| +| `src/native/useNavigationTracking.ts:60,82` | Uses `as any` type casts when traversing nested navigator state | +| `src/native/FathomWebView.tsx:182-186` | `error.message` in a catch block could be `undefined` if the thrown value isn't an `Error` | +| `src/types.ts:140` | `debug?: DebugOptions \| boolean` — the boolean shorthand mapping to `{ enabled: true, console: true }` could surprise users expecting `console: false` | + +--- + +## Error Handling (Adequate, with gaps) + +**What's covered:** +- `onError` callback on `FathomProvider` for catching tracking failures +- `safeClientCall` wrapper prevents unhandled exceptions in tracking methods +- Try-catch around debug subscriber notifications (`FathomProvider.tsx:118`) +- localStorage errors handled gracefully in `EventStream.tsx:114-116` +- React Native WebView message parsing wrapped in try-catch + +**Gaps identified:** + +1. **No `siteId` validation** — an empty string or `undefined` is silently accepted +2. **`FathomWebView.tsx:182-186`** — injected JS error handler accesses `error.message` without verifying the caught value is an `Error` instance +3. **`NativeFathomProvider.tsx:87-91`** — `client.setWebViewReady()` is not wrapped in try-catch +4. **`createWebViewClient.ts:95-98`** — when the command queue overflows `maxQueueSize`, the oldest command is silently dropped with only a debug log. No error callback is invoked. +5. **No rate limiting** on tracking calls — a malicious or buggy component could fire events in a tight loop +6. **No timeout** for WebView readiness in React Native — if the WebView never loads, queued commands sit indefinitely + +--- + +## Security Concerns (Low Risk) + +1. **`transformUrl` callbacks not validated** — In all router adapters (e.g., `ReactRouterFathomTrackView.tsx:111-117`), the return value of user-provided `transformUrl` is used directly without any validation. A misconfigured transform could inject unexpected values. + +2. **Debug mode global event broadcasting** — `FathomProvider.tsx:87` dispatches `CustomEvent('react-fathom:debug', ...)` on `window`, which is globally accessible. If debug mode is accidentally enabled in production, tracking data leaks to any listener. + +3. **WebView message origin** — `FathomWebView.tsx:212` processes incoming WebView messages without verifying origin. + +4. **No input sanitization** on event names or goal parameters before sending to Fathom. + +These are all low-severity given this is a client-side analytics library, but worth documenting. + +--- + +## Potential Bugs + +### Medium Priority + +1. **Shared module-level debug event counter** (`FathomProvider.tsx:12-13`): + ```typescript + let debugEventCounter = 0 + const generateDebugEventId = () => `debug-${Date.now()}-${++debugEventCounter}` + ``` + This counter is module-scoped, meaning multiple `FathomProvider` instances share it. Debug event IDs can collide across providers. Should be instance-scoped (e.g., via `useRef`). + +2. **Unconditional console.log on mount** (`FathomProvider.tsx:57-60`): + ```typescript + useEffect(() => { + console.log('[react-fathom] FathomProvider mounted, debugProp:', debugProp, ...) + }, []) + ``` + This logs on every provider mount regardless of whether debug mode is enabled. + +3. **Race condition in `NativeFathomProvider`** — Multiple rapid `setWebViewReady()` calls could cause duplicate event processing from the queue. + +### Low Priority + +4. **`useTrackOnMount` suppresses exhaustive-deps** (line 23) — Intentionally fires only on mount, but if `options` changes dynamically, the stale closure won't pick it up. + +5. **`EventStream` localStorage contention** (lines 131-139) — Multiple tabs writing simultaneously with no locking; last write wins. + +6. **`useTrackOnVisible` `hasTracked` ref never resets** — If a component transitions from `trackOnce=false` to `trackOnce=true` after already tracking, it won't re-track. + +--- + +## Test Coverage (Good) + +- **24 test files** covering all major functionality +- `FathomProvider.test.tsx` alone is ~750 lines with comprehensive scenarios +- All hooks, components, and framework adapters have dedicated test files +- Uses Vitest + @testing-library/react + jsdom + +**Gaps:** +- Error recovery paths are lightly tested (only ~4 tests for `onError` in `FathomProvider.test.tsx`) +- No tests for localStorage failures in `EventStream` +- No tests for malformed WebView messages +- No tests for queue overflow behavior in `createWebViewClient` + +--- + +## Code Quality & Consistency (Strong) + +**Positives:** +- Consistent style enforced by ESLint + Prettier (single quotes, no semicolons) +- Display names set on all components for React DevTools +- Good JSDoc comments with `@example` blocks +- Proper cleanup in all `useEffect` hooks (event listeners, observers) +- `useCallback`/`useMemo` used appropriately to prevent unnecessary re-renders + +**Issues:** +- **Duplicate URL-building logic** across 4+ files (`next/utils.ts`, `ReactRouterFathomTrackView.tsx`, `GatsbyFathomTrackView.tsx`, `TanStackRouterFathomTrackView.tsx`) — should be extracted to a shared utility +- **Inconsistent error logging** — some files use `console.error`, others `console.warn`, some use both +- **`disableAutoTrack` naming** — negative boolean props (`disable*`) are harder to reason about than positive ones (`enableAutoTrack`) + +--- + +## Build & Infrastructure (Solid) + +- **Rollup** config handles 7 entry points across 3 output formats (UMD, ESM, CJS) +- **`add-use-client.js`** post-build script properly adds `'use client'` directives for Next.js RSC compatibility +- **CircleCI** runs build + tests with coverage reporting to Coveralls and Codecov +- **Tree-shaking** works correctly via package.json `exports` map with separate entry points per framework + +--- + +## Performance (Good) + +- IntersectionObserver used for visibility tracking with reasonable threshold (0.1) +- Queue management in React Native prevents unbounded memory growth +- Event listeners properly cleaned up + +**Minor concerns:** +- `EventStream` accesses localStorage on every visibility toggle — could benefit from debouncing +- URL-building functions in router adapters are not memoized (reconstructed on every navigation) + +--- + +## Summary + +| Category | Rating | Notes | +|---|---|---| +| Architecture | Strong | Clean modular design with good separation | +| TypeScript | Strong | Strict mode, well-typed APIs, minor `any` casts | +| Error Handling | Adequate | Core paths covered; gaps in edge cases | +| Security | Low risk | Appropriate for client-side analytics lib | +| Testing | Good | 24 test files; some edge case gaps | +| Code Quality | Strong | Consistent style, proper React patterns | +| Build/Infra | Solid | Multi-format builds, CI/CD, coverage | + +--- + +## Top Recommended Improvements + +1. **Extract shared URL-building utility** to eliminate duplication across 4+ router adapters +2. **Make debug event counter instance-scoped** (use `useRef` instead of module-level variable) +3. **Remove unconditional `console.log` on provider mount** (or gate it behind debug mode check) +4. **Add `siteId` validation** (warn on empty/missing values) +5. **Add tests for edge cases**: error recovery, queue overflow, malformed WebView messages +6. **Add production warning** if debug mode is accidentally enabled diff --git a/README.md b/README.md index 848f6b9..3524986 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,12 @@ # react-fathom -[![npm](https://img.shields.io/npm/v/react-fathom?style=flat-square)](https://www.pkgstats.com/pkg:react-fathom) -[![NPM](https://img.shields.io/npm/l/react-fathom?style=flat-square)](LICENSE) -[![npm](https://img.shields.io/npm/dt/react-fathom?style=flat-square)](https://www.pkgstats.com/pkg:react-fathom) +[![npm](https://img.shields.io/npm/v/react-fathom?style=flat-square)](https://www.npmjs.com/package/react-fathom) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-fathom?style=flat-square)](https://bundlephobia.com/package/react-fathom) -[![GitHub stars](https://img.shields.io/github/stars/ryanhefner/react-fathom?style=flat-square)](https://github.com/ryanhefner/react-fathom/stargazers) [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/) -[![codecov](https://codecov.io/gh/ryanhefner/react-fathom/branch/main/graph/badge.svg)](https://codecov.io/gh/ryanhefner/react-fathom) - -**Privacy-focused analytics for React, Next.js, and React Native.** Easily integrate [Fathom Analytics](https://usefathom.com/ref/EKONBS) into your applications with automatic pageview tracking, custom event tracking, and full TypeScript support. - -## Table of Contents - -- [Quick Start](#quick-start) -- [Why react-fathom?](#why-react-fathom) -- [Features](#features) -- [Installation](#install) -- [Usage](#usage) - - [Basic React Setup](#basic-react-setup) - - [Next.js App Router](#nextjs-app-router) - - [Next.js Pages Router](#nextjs-pages-router) - - [React Native](#react-native) -- [API Reference](#api) -- [Troubleshooting](#troubleshooting) -- [Contributing](#contributing) -- [License](#license) + +**Privacy-focused analytics for React, Next.js, and React Native.** + +📖 **[Full Documentation](https://react-fathom.com)** · [API Reference](https://react-fathom.com/api/providers) · [Troubleshooting](https://react-fathom.com/troubleshooting) ## Quick Start @@ -51,188 +33,43 @@ import { useFathom } from 'react-fathom' function MyComponent() { const { trackEvent } = useFathom() - - return ( - - ) + return } ``` -That's it! Pageviews are tracked automatically. +Pageviews are tracked automatically. ## Why react-fathom? -### Privacy-First Analytics - -[Fathom Analytics](https://usefathom.com/ref/EKONBS) is a privacy-focused alternative to Google Analytics. Unlike traditional analytics platforms: - -- **No cookies required** - GDPR, CCPA, and PECR compliant out of the box -- **No personal data collection** - Respects user privacy by design -- **No consent banners needed** - Simplified compliance for your websites -- **Fast and lightweight** - Won't slow down your site +[Fathom Analytics](https://usefathom.com/ref/EKONBS) is a privacy-focused alternative to Google Analytics—no cookies, no consent banners, GDPR compliant by default. -### Why Use This Package? +The official `fathom-client` works, but: -- **React-native integration** - Works seamlessly with React's component model and hooks -- **Automatic tracking** - Pageviews tracked automatically on route changes -- **Next.js optimized** - First-class support for both App Router and Pages Router -- **React Native support** - Full mobile support with offline queuing -- **TypeScript-first** - Complete type definitions for a great developer experience -- **Tree-shakeable** - Only bundle what you use +| Problem | react-fathom solution | +|---------|----------------------| +| Web only—no React Native | **Full React Native support** with offline event queuing | +| Next.js App Router requires boilerplate | **`NextFathomProviderApp`** works directly in Server Component layouts | +| Imperative API only | **Hooks** (`useFathom`, `useTrackOnMount`, `useTrackOnVisible`) and **declarative components** (``, ``) | +| No tree-shaking | **Fully tree-shakeable**—bundle only what you use | -**New to Fathom?** Get a **$10 credit** when you sign up using [this referral link](https://usefathom.com/ref/EKONBS). +**New to Fathom?** Get a **$10 credit** with [this referral link](https://usefathom.com/ref/EKONBS). ## Features -- 🚀 **Zero-config** Fathom Analytics integration for React -- 📦 **Tree-shakeable** - Only bundle what you use -- 🔄 **Automatic pageview tracking** for Next.js (Pages Router & App Router) -- 📱 **React Native support** with offline queuing and navigation tracking -- 💪 **Full TypeScript** support with type definitions -- 🎯 **Flexible** - Works with any React app, Next.js, or React Native -- ⚡ **Lightweight** - Minimal bundle size impact - -## Install - -Via [npm](https://npmjs.com/package/react-fathom) - -```sh -npm install react-fathom fathom-client -``` - -Via [Yarn](https://yarn.pm/react-fathom) - -```sh -yarn add react-fathom fathom-client -``` - -## Peer Dependencies - -- `react` >= 16.8 -- `react-dom` >= 16.8 (only if using web) -- `fathom-client` >= 3.0.0 (only if using web, not needed for React Native) -- `next` >= 10.0.0 (only if using Next.js providers) -- `react-native` >= 0.60.0 (only if using React Native) -- `react-native-webview` >= 11.0.0 (only if using React Native) +- 🔒 Privacy-first Fathom Analytics integration +- ⚛️ Hooks API and declarative tracking components +- 📱 React Native with offline queuing and navigation tracking +- ⚡ Next.js App Router and Pages Router support +- 🛤️ React Router v6+ and Remix support +- 🏠 Gatsby support with @reach/router integration +- 🧭 TanStack Router support with type-safe routing +- 🐛 Debug mode for development with event subscription hooks +- 🌳 Tree-shakeable, fully typed (TypeScript) ## Usage -### Basic React Setup - -Wrap your app with `FathomProvider`: - -```tsx -import { FathomProvider } from 'react-fathom' - -function App() { - return {/* Your app */} -} -``` - -### Using the Hook - -Access Fathom methods via the `useFathom` hook: - -```tsx -import { useFathom } from 'react-fathom' - -function MyComponent() { - const { trackPageview, trackEvent, trackGoal, load } = useFathom() - - const handleClick = () => { - trackEvent('button-click', { _value: 100 }) // Optional: value in cents - } - - const handlePurchase = () => { - trackGoal('purchase', 2999) // $29.99 in cents - } - - return ( - <> - - - - ) -} -``` - -### Convenience Hooks - -Track events and pageviews with convenience hooks: - -```tsx -import { - useTrackOnMount, - useTrackOnClick, - useTrackOnVisible, -} from 'react-fathom' - -function MyComponent() { - // Track pageview on mount - useTrackOnMount({ url: '/custom-page' }) - - // Track event on click - const handleClick = useTrackOnClick({ - eventName: 'button-click', - _value: 100, // Optional: value in cents - callback: (e) => { - console.log('Tracked click!', e) - }, - }) - - // Track event when element becomes visible - const ref = useTrackOnVisible({ - eventName: 'section-viewed', - _value: 1, // Optional: value in cents - callback: (entry) => { - console.log('Element is visible!', entry) - }, - }) - - return ( - <> - -
This will be tracked when visible
- - ) -} -``` - -### Declarative Components - -Use declarative components for tracking: - -```tsx -import { TrackPageview, TrackClick, TrackVisible } from 'react-fathom' - -function MyPage() { - return ( - <> - {/* Track pageview on mount */} - -
Page content
-
- - {/* Track click events */} - - - - - {/* Track when element becomes visible */} - -
Hero section
-
- - ) -} -``` - ### Next.js App Router -**Recommended:** Use `NextFathomProviderApp` for easy integration in App Router layouts: - ```tsx // app/layout.tsx import { NextFathomProviderApp } from 'react-fathom/next' @@ -250,33 +87,10 @@ export default function RootLayout({ children }) { } ``` -**Alternative:** You can also use `FathomProvider` with `NextFathomTrackViewApp` separately if you need more control: - -```tsx -// app/layout.tsx -import { FathomProvider } from 'react-fathom' -import { NextFathomTrackViewApp } from 'react-fathom/next' - -export default function RootLayout({ children }) { - return ( - - - - - {children} - - - - ) -} -``` - -> **Note:** Since `FathomProvider` uses React hooks, you'll need to wrap it in a Client Component when using it directly in a Server Component layout. `NextFathomProviderApp` handles this for you automatically. +📖 [Full Next.js guide](https://react-fathom.com/nextjs) ### Next.js Pages Router -Use `FathomProvider` with `NextFathomTrackViewPages` for automatic route tracking: - ```tsx // pages/_app.tsx import { FathomProvider } from 'react-fathom' @@ -290,1001 +104,236 @@ function MyApp({ Component, pageProps }) { ) } - -export default MyApp -``` - -## Default Options Merging - -The `FathomProvider` supports setting default options that automatically merge with any options passed to tracking calls. This is useful for setting app-wide defaults like custom event IDs or referrer information. - -### How Merging Works - -Default options are spread first, then any options you pass to individual tracking calls are spread second. This means: - -- **Default options** provide base values for all tracking calls -- **Provided options** override defaults when specified -- You can set defaults once and forget about them - -```tsx - - {/* All trackEvent calls will include _site_id: 'my-app' unless overridden */} - -``` - -```tsx -// Inside your component -const { trackEvent } = useFathom() - -// Uses default: { _site_id: 'my-app' } -trackEvent('button-click') - -// Merges with default: { _site_id: 'my-app', _value: 100 } -trackEvent('purchase', { _value: 100 }) - -// Overrides default: { _site_id: 'custom-site', _value: 50 } -trackEvent('special-event', { _site_id: 'custom-site', _value: 50 }) -``` - -### Nested Providers - -When nesting `FathomProvider` components, child providers inherit defaults from their parent but can override them: - -```tsx - - {/* Events here use _site_id: 'global' */} - - - {/* Events here use _site_id: 'dashboard' */} - - -``` - -## Custom Client Implementation - -The `FathomProvider` accepts an optional `client` prop that allows you to provide a custom Fathom client implementation. This is useful for: - -- **React Native apps** that need a custom tracking implementation -- **Testing** with mock clients -- **Server-side rendering** scenarios -- **Custom analytics pipelines** that wrap Fathom - -### FathomClient Interface - -Your custom client must implement the `FathomClient` interface: - -```tsx -import type { FathomClient, EventOptions, LoadOptions, PageViewOptions } from 'react-fathom' - -const myCustomClient: FathomClient = { - load: (siteId: string, options?: LoadOptions) => { - // Initialize your tracking - }, - trackPageview: (opts?: PageViewOptions) => { - // Track pageview - }, - trackEvent: (eventName: string, opts?: EventOptions) => { - // Track custom event - }, - trackGoal: (code: string, cents: number) => { - // Track goal conversion - }, - setSite: (id: string) => { - // Change site ID - }, - blockTrackingForMe: () => { - // Block tracking - }, - enableTrackingForMe: () => { - // Enable tracking - }, - isTrackingEnabled: () => { - // Return tracking status - return true - }, -} ``` -### React Native - -For React Native apps, use the dedicated `/native` export. This uses a hidden WebView to load Fathom's official tracking script, ensuring full compatibility with Fathom Analytics. - -**Install the required peer dependency:** - -```bash -npm install react-native-webview -# or -yarn add react-native-webview -``` +📖 [Full Next.js guide](https://react-fathom.com/nextjs) -**Basic setup:** +### React Router / Remix ```tsx -import { NativeFathomProvider } from 'react-fathom/native' +// App.tsx or root.tsx +import { BrowserRouter } from 'react-router-dom' +import { FathomProvider } from 'react-fathom' +import { ReactRouterFathomTrackView } from 'react-fathom/react-router' function App() { return ( - console.log('Fathom ready!')} - > - - + + + + ... + + ) } ``` -> **Note:** The provider renders a hidden WebView (0x0 pixels) that loads the Fathom script. Events are queued until the WebView is ready, then automatically sent. - -#### React Navigation Integration +📖 [Full React Router guide](https://react-fathom.com/react-router) -Track screen navigation as pageviews with React Navigation: +### Gatsby ```tsx -import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native' -import { NativeFathomProvider, useNavigationTracking } from 'react-fathom/native' - -function App() { - const navigationRef = useNavigationContainerRef() - - return ( - - - - - - - ) -} +// gatsby-browser.js or Layout component +import { FathomProvider } from 'react-fathom' +import { GatsbyFathomTrackView } from 'react-fathom/gatsby' -function NavigationTracker({ navigationRef }) { - useNavigationTracking({ - navigationRef, - transformRouteName: (name) => `/screens/${name}`, - }) - return null -} +export const wrapRootElement = ({ element }) => ( + + + {element} + +) ``` -#### App State Tracking +📖 [Full Gatsby guide](https://react-fathom.com/gatsby) -Track when users foreground/background your app: +### TanStack Router ```tsx -import { useAppStateTracking } from 'react-fathom/native' +// src/routes/__root.tsx +import { createRootRoute, Outlet } from '@tanstack/react-router' +import { FathomProvider } from 'react-fathom' +import { TanStackRouterFathomTrackView } from 'react-fathom/tanstack-router' -function AppTracker() { - useAppStateTracking({ - foregroundEventName: 'app-resumed', - backgroundEventName: 'app-paused', - onStateChange: (state) => console.log('App state:', state), - }) - return null -} +export const Route = createRootRoute({ + component: () => ( + + + + + ), +}) ``` -#### Using Custom Domains +📖 [Full TanStack Router guide](https://react-fathom.com/tanstack-router) -If you use [Fathom's custom domains feature](https://usefathom.com/docs/script/custom-domains), specify your domain: +### Hooks ```tsx - - - -``` - -#### Advanced: Manual WebView Client Setup - -For advanced use cases, you can manually set up the WebView client: +import { useFathom, useTrackOnMount, useTrackOnClick, useTrackOnVisible } from 'react-fathom' -```tsx -import { useRef, useMemo, useCallback } from 'react' -import { - FathomWebView, - createWebViewClient, - FathomProvider, - type FathomWebViewRef, -} from 'react-fathom/native' +function MyComponent() { + const { trackEvent, trackGoal } = useFathom() -function App() { - const webViewRef = useRef(null) + // Track on mount + useTrackOnMount({ url: '/custom-page' }) - const client = useMemo( - () => createWebViewClient(() => webViewRef.current, { debug: __DEV__ }), - [] - ) + // Track clicks + const handleClick = useTrackOnClick({ eventName: 'cta-click', _value: 100 }) - const handleReady = useCallback(() => { - client.setWebViewReady() - }, [client]) + // Track visibility + const ref = useTrackOnVisible({ eventName: 'hero-viewed' }) return ( - - - - + <> +
Hero section
+ + + ) } ``` -### Mock Client for Testing - -```tsx -import { FathomProvider, type FathomClient } from 'react-fathom' - -const mockClient: FathomClient = { - load: jest.fn(), - trackPageview: jest.fn(), - trackEvent: jest.fn(), - trackGoal: jest.fn(), - setSite: jest.fn(), - blockTrackingForMe: jest.fn(), - enableTrackingForMe: jest.fn(), - isTrackingEnabled: jest.fn(() => true), -} - -// In your tests -render( - - - -) - -// Assert tracking calls -expect(mockClient.trackEvent).toHaveBeenCalledWith('button-click', { _value: 100 }) -``` - -## API - -### `FathomProvider` - -Main provider component for React apps. Supports composable nesting - nested providers can override `client`, `defaultPageviewOptions`, or `defaultEventOptions`. - -**Props:** +📖 [Hooks API reference](https://react-fathom.com/api/hooks) -- `siteId` (string, optional): Your Fathom Analytics site ID -- `client` (FathomClient, optional): Custom Fathom client instance -- `clientRef` (MutableRefObject, optional): Ref that will be populated with the resolved client instance, allowing the parent component to access the client directly -- `clientOptions` (LoadOptions, optional): Options passed to `fathom-client` -- `defaultPageviewOptions` (PageViewOptions, optional): Default options merged into all `trackPageview` calls -- `defaultEventOptions` (EventOptions, optional): Default options merged into all `trackEvent` calls - -**Example:** - -```tsx - - {/* Your app */} - -``` - -**Using clientRef for parent access:** +### Declarative Components ```tsx -import { useRef } from 'react' -import { FathomProvider, FathomClient } from 'react-fathom' - -function App() { - const clientRef = useRef(null) - - const handleDeepLink = (url: string) => { - // Parent can track events directly via the ref - clientRef.current?.trackEvent('deep_link', { _url: url }) - } +import { TrackPageview, TrackClick, TrackVisible } from 'react-fathom' +function MyPage() { return ( - - - - ) -} -``` - -### `NextFathomProviderApp` - -Client component wrapper that combines `FathomProvider` and `NextFathomTrackViewApp` for easy integration in Next.js App Router layouts. This component is marked with `'use client'` and can be used directly in Server Components like the root `layout.tsx` file. - -**Props:** - -- `siteId` (string, optional): Your Fathom Analytics site ID -- `client` (FathomClient, optional): Custom Fathom client instance -- `clientOptions` (LoadOptions, optional): Options passed to `fathom-client` -- `defaultPageviewOptions` (PageViewOptions, optional): Default options merged into all `trackPageview` calls -- `defaultEventOptions` (EventOptions, optional): Default options merged into all `trackEvent` calls -- `disableAutoTrack` (boolean, optional): Disable automatic pageview tracking on route changes (defaults to false) -- `children` (ReactNode, required): Child components to render - -**Example:** + <> + -```tsx -// app/layout.tsx -import { NextFathomProviderApp } from 'react-fathom/next' + +
Tracks when visible
+
-export default function RootLayout({ children }) { - return ( - - - - {children} - - - + + + + ) } ``` -### `NextFathomTrackViewApp` - -Component that tracks pageviews for Next.js App Router. Must be used within a `FathomProvider`. - -**Props:** - -- `disableAutoTrack` (boolean, optional): Disable automatic pageview tracking on route changes (defaults to false) - -**Example:** - -```tsx - - - {/* Your app */} - -``` - -### `NextFathomTrackViewPages` - -Component that tracks pageviews for Next.js Pages Router. Must be used within a `FathomProvider`. - -**Props:** - -- `disableAutoTrack` (boolean, optional): Disable automatic pageview tracking on route changes (defaults to false) - -**Example:** - -```tsx - - - {/* Your app */} - -``` - -### `useFathom()` - -Hook to access Fathom methods and context. - -**Returns:** - -- `trackPageview(options?)`: Track a pageview (automatically merges `defaultPageviewOptions`) -- `trackEvent(eventName, options?)`: Track a custom event (automatically merges `defaultEventOptions`) -- `trackGoal(code, cents)`: Track a goal conversion -- `load(siteId, options?)`: Load Fathom with a site ID -- `setSite(siteId)`: Change the site ID -- `blockTrackingForMe()`: Block tracking for current user -- `enableTrackingForMe()`: Enable tracking for current user -- `isTrackingEnabled()`: Check if tracking is enabled -- `client`: The Fathom client instance -- `defaultPageviewOptions`: Current default pageview options -- `defaultEventOptions`: Current default event options - -### `useTrackOnMount(options?)` - -Hook to track a pageview when a component mounts. - -**Options:** - -- `url` (string, optional): URL to track -- `referrer` (string, optional): Referrer URL -- All other `PageViewOptions` from `fathom-client` - -### `useTrackOnClick(options)` - -Hook that returns a click handler function to track events. +📖 [Components API reference](https://react-fathom.com/api/components) -**Options:** - -- `eventName` (string, required): Event name to track -- `preventDefault` (boolean, optional): Whether to prevent default behavior (defaults to false) -- `callback` ((e?: MouseEvent) => void, optional): Callback function to run after tracking -- All other `EventOptions` from `fathom-client` - -### `useTrackOnVisible(options)` - -Hook that returns a ref to attach to an element. Tracks an event when the element becomes visible. - -**Options:** - -- `eventName` (string, required): Event name to track -- `callback` ((entry: IntersectionObserverEntry) => void, optional): Callback function to run after tracking -- `threshold` (number, optional): IntersectionObserver threshold (defaults to 0.1) -- `rootMargin` (string, optional): IntersectionObserver rootMargin -- All other `EventOptions` from `fathom-client` - -### `TrackPageview` - -Component that tracks a pageview when it mounts. - -**Props:** - -- `url` (string, optional): URL to track -- `referrer` (string, optional): Referrer URL -- `children` (ReactNode, optional): Child elements to render -- All other `PageViewOptions` from `fathom-client` - -### `TrackClick` - -Component that tracks an event when clicked. - -**Props:** - -- `eventName` (string, required): Event name to track -- `preventDefault` (boolean, optional): Whether to prevent default behavior (defaults to false) -- `children` (ReactNode, required): Child element(s) to render -- All other `EventOptions` from `fathom-client` - -### `TrackVisible` - -Component that tracks an event when it becomes visible. - -**Props:** - -- `eventName` (string, required): Event name to track -- `threshold` (number, optional): IntersectionObserver threshold (defaults to 0.1) -- `rootMargin` (string, optional): IntersectionObserver rootMargin -- `children` (ReactNode, required): Child element(s) to render -- `as` (string, optional): HTML element type to render (defaults to 'div') -- All other `EventOptions` from `fathom-client` - -## Native API - -The `/native` export provides React Native-specific components and hooks. It uses a hidden WebView to load Fathom's official tracking script, ensuring full compatibility with Fathom Analytics (both Fathom Pro and self-hosted Fathom Lite). - -### `NativeFathomProvider` - -Convenience provider for React Native apps that manages a hidden WebView with Fathom's tracking script. - -**Props:** - -- `siteId` (string, required): Your Fathom Analytics site ID -- `loadOptions` (LoadOptions, optional): Options passed to `fathom.load()` in the WebView -- `scriptDomain` (string, optional): Custom domain for Fathom script (defaults to 'cdn.usefathom.com') -- `defaultPageviewOptions` (PageViewOptions, optional): Default options merged into all `trackPageview` calls -- `defaultEventOptions` (EventOptions, optional): Default options merged into all `trackEvent` calls -- `trackAppState` (boolean, optional): Enable automatic app state tracking (defaults to false) -- `debug` (boolean, optional): Enable debug logging (defaults to false) -- `onReady` (() => void, optional): Called when the Fathom script has loaded -- `onError` ((error: string) => void, optional): Called when an error occurs loading the script -- `clientRef` (MutableRefObject, optional): Ref that will be populated with the WebView-based client instance, allowing the parent component to access the client directly (includes queue management methods) -- `children` (ReactNode, required): Child components to render - -**Example:** +### React Native -```tsx - console.log('Analytics ready!')} - onError={(err) => console.error('Analytics error:', err)} -> - - +```bash +npm install react-native-webview ``` -**Using clientRef for parent access:** - ```tsx -import { useRef } from 'react' -import { NativeFathomProvider, WebViewFathomClient } from 'react-fathom/native' +import { NativeFathomProvider } from 'react-fathom/native' function App() { - const clientRef = useRef(null) - - const handleDeepLink = (url: string) => { - // Parent can track events directly via the ref - clientRef.current?.trackEvent('deep_link', { _url: url }) - - // Can also check queue status (React Native specific) - console.log('Queued events:', clientRef.current?.getQueueLength()) - } - return ( - - + + ) } ``` -### `FathomWebView` - -Hidden WebView component that loads and manages the Fathom Analytics script. Used internally by `NativeFathomProvider`, but can be used directly for advanced setups. - -**Props:** - -- `siteId` (string, required): Your Fathom Analytics site ID -- `loadOptions` (LoadOptions, optional): Options passed to `fathom.load()` -- `scriptDomain` (string, optional): Custom domain for Fathom script (defaults to 'cdn.usefathom.com') -- `onReady` (() => void, optional): Called when the Fathom script has loaded -- `onError` ((error: string) => void, optional): Called when an error occurs -- `debug` (boolean, optional): Enable debug logging (defaults to false) - -**Ref Methods (FathomWebViewRef):** - -- `trackPageview(opts?)`: Track a pageview -- `trackEvent(eventName, opts?)`: Track a custom event -- `trackGoal(code, cents)`: Track a goal conversion -- `blockTrackingForMe()`: Block tracking for current user -- `enableTrackingForMe()`: Enable tracking for current user -- `isReady()`: Check if the WebView is ready - -### `createWebViewClient(getWebViewRef, options?)` - -Factory function to create a client that communicates with a FathomWebView. - -**Parameters:** - -- `getWebViewRef` (() => FathomWebViewRef | null): Function that returns the WebView ref -- `options` (WebViewClientOptions, optional): - - `debug` (boolean): Enable debug logging (defaults to false) - - `enableQueue` (boolean): Enable command queuing before WebView is ready (defaults to true) - - `maxQueueSize` (number): Maximum commands to queue (defaults to 100) - -**Returns:** A `FathomClient` instance with additional methods: - -- `processQueue()`: Manually process queued commands (returns number of processed) -- `getQueueLength()`: Get the current queue length -- `setWebViewReady()`: Call when WebView signals it's ready (flushes queue) - -### `useAppStateTracking(options?)` - -Hook that tracks app state changes (foreground/background) as Fathom events. - -**Options:** - -- `foregroundEventName` (string, optional): Event name for foreground (defaults to 'app-foreground') -- `backgroundEventName` (string, optional): Event name for background (defaults to 'app-background') -- `eventOptions` (EventOptions, optional): Additional options for app state events -- `onStateChange` ((state: 'active' | 'background' | 'inactive') => void, optional): Callback on state change - -### `useNavigationTracking(options)` - -Hook that tracks React Navigation screen changes as pageviews. - -**Options:** - -- `navigationRef` (RefObject, required): React Navigation container ref -- `transformRouteName` ((name: string) => string, optional): Transform route names before tracking -- `shouldTrackRoute` ((name: string, params?: object) => boolean, optional): Filter which routes to track -- `includeParams` (boolean, optional): Include route params in tracked URL (defaults to false) - -**Example:** - -```tsx -const navigationRef = useNavigationContainerRef() - -useNavigationTracking({ - navigationRef, - transformRouteName: (name) => `/app/${name.toLowerCase()}`, - shouldTrackRoute: (name) => !name.startsWith('Modal'), - includeParams: true, -}) -``` - -## Tree-shaking - -This library is optimized for tree-shaking. When you import only what you need: - -```tsx -import { useFathom } from 'react-fathom' -``` - -Bundlers will automatically exclude unused code, keeping your bundle size minimal. - -## TypeScript - -Full TypeScript support is included. Types are automatically generated and exported. - -### Exported Types - -For convenience, `react-fathom` re-exports the core types from `fathom-client` so you don't need to import from multiple packages: - -```tsx -import type { - // From react-fathom - FathomClient, - FathomContextInterface, - FathomProviderProps, - // Re-exported from fathom-client - EventOptions, - LoadOptions, - PageViewOptions, -} from 'react-fathom' - -// No need for this anymore: -// import type { EventOptions } from 'fathom-client' -``` - -This simplifies your imports when building custom clients or working with typed event options. - -## Troubleshooting - -### Common Issues - -#### Events not appearing in Fathom dashboard - -**1. Verify your site ID** - -Your site ID should match exactly what's shown in your [Fathom dashboard](https://app.usefathom.com). It's typically an 8-character alphanumeric string like `ABCD1234`. - -```tsx -// Double-check this value - -``` - -**2. Check for ad blockers** - -Many ad blockers and privacy extensions block analytics scripts. To test: -- Open an incognito/private window with extensions disabled -- Or temporarily whitelist your development domain - -**3. Domain restrictions** - -Fathom only tracks events from domains you've configured. For local development: - -```tsx - -``` - -**4. Inspect network requests** - -Open your browser's Network tab and look for requests to `cdn.usefathom.com`. If you see: -- **No requests**: The script isn't loading (check provider setup) -- **Blocked requests**: Ad blocker is interfering -- **Failed requests**: Check your site ID and domain configuration +📖 [Full React Native guide](https://react-fathom.com/react-native) -#### Duplicate pageview tracking +### Debug Mode -If you're seeing double pageviews, you likely have multiple tracking setups: +Enable debug mode to log tracking calls and subscribe to events for custom UI: ```tsx -// WRONG: Both auto tracking AND manual tracking - - {/* This tracks pageviews */} - {/* AND clientOptions.auto defaults to true, which also tracks */} - - -// CORRECT: Use one or the other - - - -``` - -#### useFathom returns undefined methods - -This was the old behavior. As of the latest version, `useFathom()` returns stub methods that warn in development when called outside a provider. If you're seeing `undefined`: - -1. Update to the latest version: `npm update react-fathom` -2. Ensure your component is inside a `FathomProvider` +// Console logging during development + -#### Next.js App Router: "use client" errors - -Server Components can't use hooks directly. Use the pre-configured client component: - -```tsx -// app/layout.tsx -import { NextFathomProviderApp } from 'react-fathom/next' +// Subscribe to events programmatically +import { useDebugSubscription } from 'react-fathom' -export default function RootLayout({ children }) { - return ( - - - - {children} - - - - ) -} -``` - -If you need a custom setup, create your own client component wrapper: +function DebugPanel() { + const { events, debugEnabled, clearEvents } = useDebugSubscription({ + maxEvents: 20, + onEvent: (event) => console.log('Tracked:', event) + }) -```tsx -// components/AnalyticsProvider.tsx -'use client' -import { FathomProvider } from 'react-fathom' + if (!debugEnabled) return null -export function AnalyticsProvider({ children }) { return ( - - {children} - +
+ {events.map(e =>
{e.type}: {e.eventName || e.url}
)} +
) } ``` -#### Next.js: Environment variables not loading - -Ensure your environment variable is prefixed with `NEXT_PUBLIC_` to be available client-side: - -```bash -# .env.local -NEXT_PUBLIC_FATHOM_SITE_ID=YOUR_SITE_ID # ✓ Correct -FATHOM_SITE_ID=YOUR_SITE_ID # ✗ Won't work client-side -``` - -#### React Native: Events not sending - -**1. Verify react-native-webview is installed** - -The native module requires `react-native-webview`: - -```bash -npm install react-native-webview -# For iOS, also run: -cd ios && pod install -``` - -**2. Check WebView is ready** - -Events are queued until the WebView loads. Use the `onReady` callback to verify: - -```tsx - console.log('Fathom WebView ready!')} - onError={(err) => console.error('Fathom error:', err)} -> -``` - -**3. Verify network connectivity** - -The WebView needs network access to load the Fathom script. Events are queued before the WebView is ready but won't send if the script fails to load. - -**4. Check for WebView restrictions** - -Some enterprise MDM solutions or app configurations may block WebViews from loading external scripts. Verify that `cdn.usefathom.com` (or your custom domain) is accessible. - -**5. Debug with logging** - -Enable debug mode to see all tracking activity: - -```tsx - -``` - -### Debugging Tips - -#### Enable verbose logging - -For web, check the browser console. For React Native, enable debug mode: - -```tsx -// React Native - -``` - -#### Verify tracking in real-time - -Fathom's dashboard updates in real-time. Open your dashboard alongside your app to see events as they're tracked. - -#### Test with a mock client - -For debugging, replace the real client with a mock that logs everything: - -```tsx -const debugClient = { - load: (id, opts) => console.log('load:', id, opts), - trackPageview: (opts) => console.log('pageview:', opts), - trackEvent: (name, opts) => console.log('event:', name, opts), - trackGoal: (code, cents) => console.log('goal:', code, cents), - setSite: (id) => console.log('setSite:', id), - blockTrackingForMe: () => console.log('blocked'), - enableTrackingForMe: () => console.log('enabled'), - isTrackingEnabled: () => true, -} - - -``` - -### Getting Help +Debug mode does not block actual tracking—events are still sent to Fathom. -- [Open an issue](https://github.com/ryanhefner/react-fathom/issues) on GitHub -- [Search existing issues](https://github.com/ryanhefner/react-fathom/issues?q=is%3Aissue) for solutions -- [Fathom Analytics documentation](https://usefathom.com/docs) for platform-specific questions +## API Overview -## Contributing +### Providers -Contributions are welcome! Whether it's bug fixes, new features, documentation improvements, or examples, we appreciate your help. +| Component | Use case | +|-----------|----------| +| `FathomProvider` | Basic React apps | +| `NextFathomProviderApp` | Next.js App Router | +| `NextFathomTrackViewPages` | Next.js Pages Router (add inside `FathomProvider`) | +| `ReactRouterFathomTrackView` | React Router v6+ / Remix (add inside `FathomProvider`) | +| `GatsbyFathomTrackView` | Gatsby (add inside `FathomProvider`) | +| `TanStackRouterFathomTrackView` | TanStack Router (add inside `FathomProvider`) | +| `NativeFathomProvider` | React Native | -### Ways to Contribute +### Hooks -| Type | Description | +| Hook | Description | |------|-------------| -| **Bug Reports** | Found a bug? [Open an issue](https://github.com/ryanhefner/react-fathom/issues/new) with reproduction steps | -| **Feature Requests** | Have an idea? Discuss it in an issue first | -| **Bug Fixes** | PRs for documented issues are always welcome | -| **Documentation** | Help improve docs, add examples, fix typos | -| **Tests** | Increase test coverage or add edge case tests | +| `useFathom()` | Returns `trackPageview`, `trackEvent`, `trackGoal`, and more | +| `useTrackOnMount(opts?)` | Track pageview when component mounts | +| `useTrackOnClick(opts)` | Returns click handler that tracks event | +| `useTrackOnVisible(opts)` | Returns ref; tracks when element becomes visible | +| `useDebugSubscription(opts?)` | Subscribe to debug events for custom UI | -### Development Setup +### Components -**Prerequisites:** -- Node.js 18+ -- npm 9+ +| Component | Description | +|-----------|-------------| +| `` | Track pageview on mount | +| `` | Track event on click | +| `` | Track when visible (IntersectionObserver) | -**1. Clone and install:** +📖 [Full API Reference](https://react-fathom.com/api/providers) -```bash -git clone https://github.com/ryanhefner/react-fathom.git -cd react-fathom -npm install -``` +## Common Issues -**2. Run the development workflow:** - -```bash -# Run tests in watch mode during development -npm run test:watch - -# Run the full test suite -npm test - -# Build the package -npm run build - -# Type check without emitting -npm run typecheck -``` - -### Project Structure - -``` -react-fathom/ -├── src/ -│ ├── index.ts # Main entry point -│ ├── FathomProvider.tsx # Core provider component -│ ├── FathomContext.tsx # React context -│ ├── types.ts # TypeScript definitions -│ ├── hooks/ # React hooks -│ │ ├── useFathom.ts -│ │ ├── useTrackOnClick.ts -│ │ ├── useTrackOnMount.ts -│ │ └── useTrackOnVisible.ts -│ ├── components/ # Declarative tracking components -│ │ ├── TrackClick.tsx -│ │ ├── TrackPageview.tsx -│ │ └── TrackVisible.tsx -│ ├── next/ # Next.js-specific exports -│ │ └── index.ts -│ └── native/ # React Native exports -│ ├── index.ts -│ ├── FathomWebView.tsx -│ ├── createWebViewClient.ts -│ ├── NativeFathomProvider.tsx -│ ├── useNavigationTracking.ts -│ └── useAppStateTracking.ts -├── examples/ # Example applications -│ ├── next-app/ # Next.js App Router example -│ └── next-pages/ # Next.js Pages Router example -└── dist/ # Built output (generated) -``` - -### Testing Guidelines - -We use [Vitest](https://vitest.dev/) for testing. All new features should include tests. - -```bash -# Run all tests -npm test - -# Run tests in watch mode -npm run test:watch - -# Run tests with coverage -npm run test:coverage -``` - -**Writing tests:** +**Events not appearing?** +1. Verify site ID matches your [Fathom dashboard](https://app.usefathom.com) +2. Check for ad blockers (test in incognito) +3. Add `{ includedDomains: ['localhost'] }` to `clientOptions` +**Duplicate pageviews?** ```tsx -// src/hooks/useMyHook.test.tsx -import { renderHook } from '@testing-library/react' -import { describe, it, expect, vi } from 'vitest' -import { useMyHook } from './useMyHook' -import { FathomProvider } from '../FathomProvider' - -describe('useMyHook', () => { - it('should track events correctly', () => { - const mockClient = { - trackEvent: vi.fn(), - // ... other required methods - } - - const wrapper = ({ children }) => ( - {children} - ) - - const { result } = renderHook(() => useMyHook(), { wrapper }) - - result.current.doSomething() - - expect(mockClient.trackEvent).toHaveBeenCalledWith('expected-event', {}) - }) -}) +// Disable fathom-client's auto tracking when using NextFathomTrackViewApp + ``` -### Code Style - -- **TypeScript**: All code should be fully typed -- **Formatting**: We use Prettier (run `npm run format` before committing) -- **Linting**: ESLint catches common issues (run `npm run lint`) -- **Naming**: - - Components: PascalCase (`TrackClick.tsx`) - - Hooks: camelCase with `use` prefix (`useFathom.ts`) - - Types: PascalCase (`FathomClient`) - -### Submitting a Pull Request - -1. **Fork** the repository -2. **Create a branch** from `main`: - ```bash - git checkout -b fix/my-bug-fix - # or - git checkout -b feature/my-new-feature - ``` -3. **Make your changes** with clear, focused commits -4. **Add or update tests** for your changes -5. **Ensure CI passes**: - ```bash - npm run lint - npm test - npm run build - ``` -6. **Submit a PR** with a clear description of what and why - -### Commit Message Guidelines - -Use clear, descriptive commit messages: +📖 [Full Troubleshooting Guide](https://react-fathom.com/troubleshooting) -``` -feat: add useTrackOnScroll hook for scroll tracking -fix: resolve duplicate pageview tracking in Next.js -docs: add troubleshooting section for ad blockers -test: add tests for native offline queue -refactor: simplify FathomContext default values -``` +## Documentation -Prefixes: `feat`, `fix`, `docs`, `test`, `refactor`, `chore`, `perf` +- 📖 [Getting Started](https://react-fathom.com/getting-started) +- ⚛️ [React Guide](https://react-fathom.com/react) +- 🛤️ [React Router Guide](https://react-fathom.com/react-router) +- 🏠 [Gatsby Guide](https://react-fathom.com/gatsby) +- 🧭 [TanStack Router Guide](https://react-fathom.com/tanstack-router) +- ⚡ [Next.js Guide](https://react-fathom.com/nextjs) +- 📱 [React Native Guide](https://react-fathom.com/react-native) +- 📚 [API Reference](https://react-fathom.com/api/providers) +- 🔧 [Troubleshooting](https://react-fathom.com/troubleshooting) +- 🤝 [Contributing](https://react-fathom.com/contributing) ## License diff --git a/docs/.env.example b/docs/.env.example new file mode 100644 index 0000000..b04238c --- /dev/null +++ b/docs/.env.example @@ -0,0 +1,6 @@ +# Fathom Analytics +# Get your site ID from https://app.usefathom.com → Settings → Site ID +NEXT_PUBLIC_FATHOM_SITE_ID= + +# Site URL (for sitemap and canonical URLs) +SITE_URL=https://react-fathom.com diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..28f8b53 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules/ + +# Next.js build output +.next/ +out/ + +# Next.js auto-generated files +next-env.d.ts + +# Pagefind search index (generated during build) +public/pagefind/ + +# Lock files (project uses yarn) +package-lock.json + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/docs/app/color-mode.tsx b/docs/app/color-mode.tsx new file mode 100644 index 0000000..5911fde --- /dev/null +++ b/docs/app/color-mode.tsx @@ -0,0 +1,11 @@ +'use client' + +import { ThemeProvider } from 'next-themes' + +export function ColorModeProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/docs/app/docs/[[...slug]]/page.tsx b/docs/app/docs/[[...slug]]/page.tsx new file mode 100644 index 0000000..8c1e6bc --- /dev/null +++ b/docs/app/docs/[[...slug]]/page.tsx @@ -0,0 +1,87 @@ +import { notFound } from 'next/navigation' +import { MDXRemote } from 'next-mdx-remote/rsc' +import rehypePrettyCode from 'rehype-pretty-code' +import { DocsLayout } from '@/components/docs' +import { + getDocBySlug, + getAllDocSlugs, + getDocsNav, + extractTOC, + getAdjacentPages, + getBreadcrumbs, + getLastUpdated, +} from '@/lib/docs' +import { getMDXComponents } from '@/components/docs/MDXComponents' + +export async function generateStaticParams() { + const slugs = getAllDocSlugs() + return slugs.map((slug) => ({ slug })) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug?: string[] }> +}) { + const { slug = [] } = await params + const doc = getDocBySlug(slug) + + if (!doc) { + return { title: 'Not Found' } + } + + return { + title: doc.frontmatter.title, + description: doc.frontmatter.description, + } +} + +const rehypePrettyCodeOptions = { + theme: { + dark: 'github-dark', + light: 'github-light', + }, + keepBackground: false, + defaultLang: 'plaintext', +} + +export default async function DocPage({ + params, +}: { + params: Promise<{ slug?: string[] }> +}) { + const { slug = [] } = await params + const doc = getDocBySlug(slug) + + if (!doc) { + notFound() + } + + const nav = getDocsNav() + const toc = extractTOC(doc.content) + const adjacentPages = getAdjacentPages(slug) + const breadcrumbs = getBreadcrumbs(slug) + const lastUpdated = getLastUpdated(slug) + + return ( + + + + ) +} diff --git a/docs/app/globals.css b/docs/app/globals.css new file mode 100644 index 0000000..4569ca7 --- /dev/null +++ b/docs/app/globals.css @@ -0,0 +1,91 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +/* Print styles */ +@media print { + /* Hide navigation elements */ + header, + nav, + aside, + [data-pagefind-body]::before, + [aria-label="Search"], + [aria-label="Table of contents"], + [aria-label="Breadcrumb"] { + display: none !important; + } + + /* Hide interactive elements */ + button, + [role="button"] { + display: none !important; + } + + /* Reset layout */ + body { + background: white !important; + color: black !important; + } + + main { + max-width: 100% !important; + padding: 0 !important; + margin: 0 !important; + } + + /* Typography for print */ + h1, h2, h3, h4, h5, h6 { + page-break-after: avoid; + color: black !important; + } + + p, li { + orphans: 3; + widows: 3; + } + + /* Code blocks */ + pre, code { + background: #f5f5f5 !important; + color: black !important; + border: 1px solid #ddd !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + } + + pre { + page-break-inside: avoid; + } + + /* Links */ + a { + color: black !important; + text-decoration: underline !important; + } + + a[href^="http"]::after { + content: " (" attr(href) ")"; + font-size: 0.8em; + color: #666; + } + + /* Hide footer navigation */ + footer { + display: none !important; + } + + /* Page margins */ + @page { + margin: 2cm; + } +} diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx new file mode 100644 index 0000000..11316cd --- /dev/null +++ b/docs/app/layout.tsx @@ -0,0 +1,81 @@ +import type { ReactNode } from 'react' +import type { Metadata } from 'next' +import { Provider } from './provider' +import './globals.css' + +const SITE_URL = process.env.SITE_URL || 'https://react-fathom.com' + +export const metadata: Metadata = { + title: { + default: 'react-fathom', + template: '%s – react-fathom', + }, + description: + 'Privacy-focused analytics for React, Next.js, and React Native. A lightweight integration for Fathom Analytics with support for React Router, Gatsby, and TanStack Router.', + keywords: [ + 'react', + 'fathom', + 'analytics', + 'privacy', + 'nextjs', + 'react-native', + 'react-router', + 'gatsby', + 'tanstack-router', + 'gdpr', + 'ccpa', + 'cookie-free', + ], + authors: [{ name: 'Ryan Hefner', url: 'https://github.com/ryanhefner' }], + creator: 'Ryan Hefner', + metadataBase: new URL(SITE_URL), + openGraph: { + type: 'website', + locale: 'en_US', + url: SITE_URL, + siteName: 'react-fathom', + title: 'react-fathom - Privacy-focused analytics for React', + description: + 'A lightweight React integration for Fathom Analytics. Track page views and custom events while respecting user privacy.', + images: [ + { + url: '/og-image.svg', + width: 1200, + height: 630, + alt: 'react-fathom - Privacy-focused analytics for React', + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: 'react-fathom - Privacy-focused analytics for React', + description: + 'A lightweight React integration for Fathom Analytics. Track page views and custom events while respecting user privacy.', + creator: '@ryanhefner', + images: ['/og-image.svg'], + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + alternates: { + canonical: SITE_URL, + }, +} + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ) +} diff --git a/docs/app/not-found.tsx b/docs/app/not-found.tsx new file mode 100644 index 0000000..6b67bb9 --- /dev/null +++ b/docs/app/not-found.tsx @@ -0,0 +1,105 @@ +'use client' + +import { Box, Container, Heading, Text, VStack, Link, Flex, HStack } from '@chakra-ui/react' +import NextLink from 'next/link' +import { Search } from '@/components/docs/Search' +import { ColorModeButton } from '@/components/docs/ColorModeButton' + +const popularPages = [ + { title: 'Getting Started', href: '/docs/getting-started' }, + { title: 'React', href: '/docs/react' }, + { title: 'Next.js', href: '/docs/nextjs' }, + { title: 'API Reference', href: '/docs/api' }, +] + +function SimpleNavbar() { + return ( + + + + + react-fathom + + + + Docs + + + + + + + ) +} + +export default function NotFound() { + return ( + <> + + + + + + 404 + + + Page not found + + + The page you're looking for doesn't exist or has been moved. + + + + + + Try searching for what you need: + + + + + + + Or check out these popular pages: + + + {popularPages.map((page) => ( + + {page.title} + + ))} + + + + + ← Back to home + + + + + ) +} diff --git a/docs/app/page.tsx b/docs/app/page.tsx new file mode 100644 index 0000000..d878673 --- /dev/null +++ b/docs/app/page.tsx @@ -0,0 +1,5 @@ +import { LandingPage } from '@/components/LandingPage' + +export default function Home() { + return +} diff --git a/docs/app/provider.tsx b/docs/app/provider.tsx new file mode 100644 index 0000000..8a9f7e0 --- /dev/null +++ b/docs/app/provider.tsx @@ -0,0 +1,24 @@ +'use client' + +import { ChakraProvider, defaultSystem } from '@chakra-ui/react' +import { NextFathomProviderApp } from '@/lib/fathom-next' +import { ColorModeProvider } from './color-mode' +import { EventStream } from '../components/docs/EventStream' + +export function Provider({ children }: { children: React.ReactNode }) { + const siteId = process.env.NEXT_PUBLIC_FATHOM_SITE_ID || 'DEMO' + + return ( + + + + {children} + + + + + ) +} diff --git a/docs/app/robots.ts b/docs/app/robots.ts new file mode 100644 index 0000000..0f7b85d --- /dev/null +++ b/docs/app/robots.ts @@ -0,0 +1,15 @@ +import { MetadataRoute } from 'next' + +export const dynamic = 'force-static' + +const SITE_URL = process.env.SITE_URL || 'https://react-fathom.com' + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + }, + sitemap: `${SITE_URL}/sitemap.xml`, + } +} diff --git a/docs/app/sitemap.ts b/docs/app/sitemap.ts new file mode 100644 index 0000000..e52aa5c --- /dev/null +++ b/docs/app/sitemap.ts @@ -0,0 +1,28 @@ +import { MetadataRoute } from 'next' +import { getAllDocSlugs } from '@/lib/docs' + +export const dynamic = 'force-static' + +const SITE_URL = process.env.SITE_URL || 'https://react-fathom.com' + +export default function sitemap(): MetadataRoute.Sitemap { + const slugs = getAllDocSlugs() + + // Landing page + const landingPage = { + url: SITE_URL, + lastModified: new Date(), + changeFrequency: 'monthly' as const, + priority: 1, + } + + // Documentation pages under /docs + const docPages = slugs.map((slug) => ({ + url: slug.length === 0 ? `${SITE_URL}/docs` : `${SITE_URL}/docs/${slug.join('/')}`, + lastModified: new Date(), + changeFrequency: 'weekly' as const, + priority: slug.length === 0 ? 0.9 : 0.8, + })) + + return [landingPage, ...docPages] +} diff --git a/docs/components/LandingPage.tsx b/docs/components/LandingPage.tsx new file mode 100644 index 0000000..17a0bef --- /dev/null +++ b/docs/components/LandingPage.tsx @@ -0,0 +1,460 @@ +'use client' + +import { + Box, + Container, + Flex, + Grid, + Heading, + HStack, + Image, + Link, + Text, + VStack, +} from '@chakra-ui/react' +import NextLink from 'next/link' +import { Navbar } from '@/components/docs/Navbar' +import { ColorModeButton } from '@/components/docs/ColorModeButton' +import { useState } from 'react' + +const frameworks = [ + { + name: 'React', + logo: '/logos/react.svg', + description: 'Drop-in provider and hooks for any React app', + href: '/docs/react', + }, + { + name: 'Next.js', + logo: '/logos/nextjs.svg', + description: 'App Router and Pages Router support with SSR handling', + href: '/docs/nextjs', + invertInDark: true, + }, + { + name: 'React Router', + logo: '/logos/react-router.svg', + description: 'Automatic pageview tracking for React Router v6+ and Remix', + href: '/docs/react-router', + }, + { + name: 'Gatsby', + logo: '/logos/gatsby.svg', + description: 'Integration with @reach/router for Gatsby sites', + href: '/docs/gatsby', + }, + { + name: 'TanStack Router', + logo: '/logos/tanstack.svg', + description: 'Type-safe routing with automatic pageview tracking', + href: '/docs/tanstack-router', + }, + { + name: 'React Native', + logo: '/logos/react-native.svg', + description: 'Navigation tracking and app state handling for mobile', + href: '/docs/react-native', + }, +] + +const features = [ + { + icon: '🔒', + title: 'Privacy-First', + description: 'GDPR, CCPA, and PECR compliant. No cookies required.', + }, + { + icon: '⚡', + title: 'Lightweight', + description: 'Tree-shakeable with tiny bundle size.', + }, + { + icon: '📘', + title: 'TypeScript', + description: 'Full type safety with comprehensive type definitions.', + }, + { + icon: '🎯', + title: 'Simple API', + description: 'Intuitive hooks and components that just work.', + }, + { + icon: '🐛', + title: 'Debug Mode', + description: 'Built-in event stream for development and testing.', + }, + { + icon: '🌳', + title: 'Tree-Shakeable', + description: 'Import only what you need for minimal bundle impact.', + }, +] + +const installCommand = 'npm install react-fathom' + +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false) + + const handleCopy = async () => { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( + + {copied ? '✓' : '📋'} + + ) +} + +function LandingNavbar() { + return ( + + + + + react-fathom + + + + Documentation + + + API + + + GitHub + + + + + + + ) +} + +export function LandingPage() { + return ( + + + + {/* Hero Section */} + + + + Privacy-focused analytics +
+ + for React + +
+ + A lightweight React integration for Fathom Analytics. + Track page views and custom events while respecting user privacy. + + + {/* Framework Logo Grid */} + + {frameworks.map((framework) => ( + + + {framework.name} + + + ))} + + + {/* Install command */} + + $ + {installCommand} + + + + {/* CTA buttons */} + + + Get Started + + + View on GitHub + + +
+
+ + {/* Frameworks Section */} + + + + + + Works with your stack + + + First-class support for popular React frameworks + + + + + {frameworks.map((framework) => ( + + + + {framework.name} + + {framework.name} + + + {framework.description} + + + + + ))} + + + + + + {/* Features Section */} + + + + + Why react-fathom? + + + Simple, privacy-focused analytics integration + + + + + {features.map((feature) => ( + + {feature.icon} + + + {feature.title} + + + {feature.description} + + + + ))} + + + + + {/* Code Example Section */} + + + + + + Get started in minutes + + + Just wrap your app and start tracking + + + + + + + + + + + import + {'{ '} + FathomProvider + {' }'} + from + 'react-fathom' + ; + export default function + App + () {'{'} + return + ( + {'<'} + FathomProvider + siteId + = + "ABCDEFGH" + {'>'} + {'<'} + YourApp + /{'>'} + {' + FathomProvider + {'>'} + ); + {'}'} + + + + + + Read the full documentation → + + + + + + + {/* Footer */} + + + + + MIT {new Date().getFullYear()} © Ryan Hefner + + + + Documentation + + + GitHub + + + Fathom Analytics + + + + + +
+ ) +} diff --git a/docs/components/docs/Accordion.tsx b/docs/components/docs/Accordion.tsx new file mode 100644 index 0000000..6740667 --- /dev/null +++ b/docs/components/docs/Accordion.tsx @@ -0,0 +1,76 @@ +'use client' + +import { Box, Flex, Text } from '@chakra-ui/react' +import { useState, type ReactNode } from 'react' + +interface AccordionItemProps { + title: string + children: ReactNode + defaultOpen?: boolean +} + +export function AccordionItem({ title, children, defaultOpen = false }: AccordionItemProps) { + const [isOpen, setIsOpen] = useState(defaultOpen) + + return ( + + setIsOpen(!isOpen)} + cursor="pointer" + textAlign="left" + > + {title} + + ▼ + + + {isOpen && ( + + {children} + + )} + + ) +} + +interface AccordionProps { + children: ReactNode +} + +export function Accordion({ children }: AccordionProps) { + return ( + + {children} + + ) +} + +// Collapsible is an alias for single-item usage +interface CollapsibleProps { + title: string + children: ReactNode + defaultOpen?: boolean +} + +export function Collapsible({ title, children, defaultOpen = false }: CollapsibleProps) { + return ( + + + {children} + + + ) +} diff --git a/docs/components/docs/AnnouncementBanner.tsx b/docs/components/docs/AnnouncementBanner.tsx new file mode 100644 index 0000000..0cf7572 --- /dev/null +++ b/docs/components/docs/AnnouncementBanner.tsx @@ -0,0 +1,97 @@ +'use client' + +import { Box, CloseButton, Flex, Link, Text } from '@chakra-ui/react' +import { useState, useEffect } from 'react' + +interface AnnouncementBannerProps { + id: string + message: string + linkText?: string + linkHref?: string + variant?: 'info' | 'warning' | 'success' + dismissible?: boolean +} + +const variantStyles = { + info: { + bg: 'blue.600', + color: 'white', + }, + warning: { + bg: 'yellow.500', + color: 'black', + }, + success: { + bg: 'green.600', + color: 'white', + }, +} + +export function AnnouncementBanner({ + id, + message, + linkText, + linkHref, + variant = 'info', + dismissible = true, +}: AnnouncementBannerProps) { + const [isDismissed, setIsDismissed] = useState(true) // Start hidden to prevent flash + const storageKey = `announcement-dismissed-${id}` + + useEffect(() => { + // Check localStorage after mount + const dismissed = localStorage.getItem(storageKey) + setIsDismissed(dismissed === 'true') + }, [storageKey]) + + const handleDismiss = () => { + localStorage.setItem(storageKey, 'true') + setIsDismissed(true) + } + + if (isDismissed) { + return null + } + + const styles = variantStyles[variant] + + return ( + + + + {message} + {linkText && linkHref && ( + <> + {' '} + + {linkText} → + + + )} + + {dismissible && ( + + )} + + + ) +} diff --git a/docs/components/docs/Breadcrumbs.tsx b/docs/components/docs/Breadcrumbs.tsx new file mode 100644 index 0000000..07ccd47 --- /dev/null +++ b/docs/components/docs/Breadcrumbs.tsx @@ -0,0 +1,50 @@ +'use client' + +import { Flex, Link, Text } from '@chakra-ui/react' +import NextLink from 'next/link' + +interface BreadcrumbItem { + title: string + href?: string +} + +interface BreadcrumbsProps { + items: BreadcrumbItem[] +} + +export function Breadcrumbs({ items }: BreadcrumbsProps) { + if (items.length <= 1) return null + + return ( + + {items.map((item, index) => { + const isLast = index === items.length - 1 + + return ( + + {index > 0 && /} + {isLast || !item.href ? ( + {item.title} + ) : ( + + {item.title} + + )} + + ) + })} + + ) +} diff --git a/docs/components/docs/Callout.tsx b/docs/components/docs/Callout.tsx new file mode 100644 index 0000000..0c94f7b --- /dev/null +++ b/docs/components/docs/Callout.tsx @@ -0,0 +1,65 @@ +'use client' + +import { Box, Flex, Text } from '@chakra-ui/react' +import type { ReactNode } from 'react' + +interface CalloutProps { + type?: 'info' | 'warning' | 'error' | 'tip' + title?: string + children: ReactNode +} + +const calloutStyles = { + info: { + bg: 'blue.50', + borderColor: 'blue.500', + icon: 'ℹ️', + _dark: { bg: 'blue.950' }, + }, + warning: { + bg: 'yellow.50', + borderColor: 'yellow.500', + icon: '⚠️', + _dark: { bg: 'yellow.950' }, + }, + error: { + bg: 'red.50', + borderColor: 'red.500', + icon: '🚫', + _dark: { bg: 'red.950' }, + }, + tip: { + bg: 'green.50', + borderColor: 'green.500', + icon: '💡', + _dark: { bg: 'green.950' }, + }, +} + +export function Callout({ type = 'info', title, children }: CalloutProps) { + const styles = calloutStyles[type] + + return ( + + + {styles.icon} + + {title && ( + {title} + )} + + {children} + + + + + ) +} diff --git a/docs/components/docs/Cards.tsx b/docs/components/docs/Cards.tsx new file mode 100644 index 0000000..6873586 --- /dev/null +++ b/docs/components/docs/Cards.tsx @@ -0,0 +1,61 @@ +'use client' + +import { Box, Grid, Link, Text } from '@chakra-ui/react' +import NextLink from 'next/link' +import type { ReactNode } from 'react' + +interface CardsProps { + children: ReactNode + cols?: number +} + +export function Cards({ children, cols = 2 }: CardsProps) { + return ( + + {children} + + ) +} + +interface CardProps { + title: string + href: string + children?: ReactNode +} + +export function Card({ title, href, children }: CardProps) { + const isExternal = href.startsWith('http') + + const content = ( + + {title} + {children && ( + {children} + )} + + ) + + if (isExternal) { + return ( + + {content} + + ) + } + + return ( + + {content} + + ) +} diff --git a/docs/components/docs/Changelog.tsx b/docs/components/docs/Changelog.tsx new file mode 100644 index 0000000..066e9a3 --- /dev/null +++ b/docs/components/docs/Changelog.tsx @@ -0,0 +1,85 @@ +'use client' + +import { Box, Flex, Heading, Link, Text, VStack } from '@chakra-ui/react' + +interface Commit { + hash: string + message: string + author: string +} + +interface ChangelogEntryProps { + version: string + date: string + commits: Commit[] + repoUrl?: string +} + +export function ChangelogEntry({ version, date, commits, repoUrl = 'https://github.com/ryanhefner/react-fathom' }: ChangelogEntryProps) { + return ( + + + + + {version} + + + + {date} + + + + {commits.map((commit) => ( + + + {commit.hash} + + {commit.message} + + {commit.author} + + + ))} + + + ) +} + +interface ChangelogProps { + entries: { + version: string + date: string + commits: Commit[] + }[] + repoUrl?: string +} + +export function Changelog({ entries, repoUrl }: ChangelogProps) { + if (entries.length === 0) { + return ( + + No releases found. + + ) + } + + return ( + + {entries.map((entry) => ( + + ))} + + ) +} diff --git a/docs/components/docs/CodeBlock.tsx b/docs/components/docs/CodeBlock.tsx new file mode 100644 index 0000000..301284b --- /dev/null +++ b/docs/components/docs/CodeBlock.tsx @@ -0,0 +1,153 @@ +'use client' + +import { Box, IconButton } from '@chakra-ui/react' +import { useState, useRef, type ReactNode, type ComponentProps } from 'react' +import { useFathom } from '@/lib/fathom' + +// Pre component for rehype-pretty-code +export function Pre({ children, ...props }: ComponentProps<'pre'>) { + const [copied, setCopied] = useState(false) + const preRef = useRef(null) + const { trackEvent } = useFathom() + + const handleCopy = async () => { + const text = preRef.current?.textContent || '' + await navigator.clipboard.writeText(text) + setCopied(true) + trackEvent('code-copy') + setTimeout(() => setCopied(false), 2000) + } + + return ( + + + {copied ? '✓' : '📋'} + +
+        
+        {children}
+      
+
+ ) +} + +// Figure wrapper for code blocks with filename (from rehype-pretty-code) +export function Figure({ children, ...props }: ComponentProps<'figure'>) { + const isCodeBlock = 'data-rehype-pretty-code-figure' in props + + if (!isCodeBlock) { + return
{children}
+ } + + return ( +
+ + {children} +
+ ) +} + +// Figcaption for filename +export function Figcaption({ children, ...props }: ComponentProps<'figcaption'>) { + const isCodeTitle = 'data-rehype-pretty-code-title' in props + + if (!isCodeTitle) { + return
{children}
+ } + + return ( +
+ + {children} +
+ ) +} diff --git a/docs/components/docs/ColorModeButton.tsx b/docs/components/docs/ColorModeButton.tsx new file mode 100644 index 0000000..5b1c361 --- /dev/null +++ b/docs/components/docs/ColorModeButton.tsx @@ -0,0 +1,29 @@ +'use client' + +import { IconButton } from '@chakra-ui/react' +import { useTheme } from 'next-themes' +import { useEffect, useState } from 'react' + +export function ColorModeButton() { + const { theme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return + } + + return ( + setTheme(theme === 'dark' ? 'light' : 'dark')} + > + {theme === 'dark' ? '☀️' : '🌙'} + + ) +} diff --git a/docs/components/docs/DocsLayout.tsx b/docs/components/docs/DocsLayout.tsx new file mode 100644 index 0000000..172ad7e --- /dev/null +++ b/docs/components/docs/DocsLayout.tsx @@ -0,0 +1,175 @@ +'use client' + +import { Box, Container, Flex, HStack, Link, Text } from '@chakra-ui/react' +import NextLink from 'next/link' +import { AnnouncementBanner } from './AnnouncementBanner' +import { Navbar } from './Navbar' +import { Sidebar } from './Sidebar' +import { TableOfContents } from './TableOfContents' +import { Breadcrumbs } from './Breadcrumbs' +import { KeyboardShortcuts } from './KeyboardShortcuts' +import type { NavItem, TOCItem, Frontmatter, AdjacentPages, BreadcrumbItem } from '@/lib/docs' + +const GITHUB_REPO = 'https://github.com/ryanhefner/react-fathom' +const DOCS_PATH = 'docs/content' + +interface Announcement { + id: string + message: string + linkText?: string + linkHref?: string + variant?: 'info' | 'warning' | 'success' +} + +interface DocsLayoutProps { + children: React.ReactNode + nav: NavItem[] + toc: TOCItem[] + frontmatter: Frontmatter + adjacentPages?: AdjacentPages + slug?: string[] + breadcrumbs?: BreadcrumbItem[] + lastUpdated?: string | null + announcement?: Announcement +} + +function getEditUrl(slug: string[]): string { + const filePath = slug.length === 0 ? 'index' : slug.join('/') + return `${GITHUB_REPO}/edit/main/${DOCS_PATH}/${filePath}.mdx` +} + +function formatDate(dateString: string): string { + const date = new Date(dateString) + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) +} + +export function DocsLayout({ + children, + nav, + toc, + frontmatter, + adjacentPages, + slug = [], + breadcrumbs, + lastUpdated, + announcement, +}: DocsLayoutProps) { + const editUrl = getEditUrl(slug) + + return ( + + + {announcement && ( + + )} + + + + + + + {breadcrumbs && breadcrumbs.length > 1 && ( + + )} + {frontmatter.title && ( + + {frontmatter.title} + + )} + {frontmatter.description && ( + + {frontmatter.description} + + )} + + {children} + + + {lastUpdated && ( + + Last updated: {formatDate(lastUpdated)} + + )} + + Edit this page on GitHub → + + + {adjacentPages && ( + + {adjacentPages.prev ? ( + + + + ← Previous + {adjacentPages.prev.title} + + + + ) : } + {adjacentPages.next ? ( + + + + Next → + {adjacentPages.next.title} + + + + ) : } + + )} + + + MIT {new Date().getFullYear()} © Ryan Hefner + + + + + + + + + ) +} diff --git a/docs/components/docs/EventStream.tsx b/docs/components/docs/EventStream.tsx new file mode 100644 index 0000000..9ddd878 --- /dev/null +++ b/docs/components/docs/EventStream.tsx @@ -0,0 +1,275 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Box, Button, Flex, IconButton, Text, VStack } from '@chakra-ui/react' +import { type DebugEvent } from '@/lib/fathom' + +const STORAGE_KEY = 'react-fathom-event-stream-visible' + +function EventIcon({ type }: { type: DebugEvent['type'] }) { + const icons = { + pageview: '📄', + event: '🎯', + goal: '🏆', + } + return {icons[type]} +} + +function formatTime(timestamp: number): string { + return new Date(timestamp).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) +} + +function EventCard({ event }: { event: DebugEvent }) { + const bgColor = { + pageview: { light: 'blue.50', dark: 'blue.950' }, + event: { light: 'purple.50', dark: 'purple.950' }, + goal: { light: 'green.50', dark: 'green.950' }, + } + const borderColor = { + pageview: 'blue.400', + event: 'purple.400', + goal: 'green.400', + } + + let title = '' + let subtitle = '' + + switch (event.type) { + case 'pageview': + title = 'Pageview' + subtitle = event.url || window.location.pathname + break + case 'event': + title = 'Event' + subtitle = event.eventName || '' + break + case 'goal': + title = 'Goal' + subtitle = `${event.goalCode} ($${((event.goalCents || 0) / 100).toFixed(2)})` + break + } + + return ( + + + + + + {title} + + + + {formatTime(event.timestamp)} + + + + {subtitle} + + + ) +} + +interface EventStreamProps { + /** + * Force show the EventStream panel regardless of debug context. + * Useful for docs site where context may not be shared properly with linked packages. + */ + forceShow?: boolean +} + +export function EventStream({ forceShow = false }: EventStreamProps) { + const [isVisible, setIsVisible] = useState(false) + const [isHydrated, setIsHydrated] = useState(false) + const [events, setEvents] = useState([]) + + // Load visibility state from localStorage on mount + useEffect(() => { + setIsHydrated(true) + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored !== null) { + setIsVisible(stored === 'true') + } + } catch { + // localStorage not available (private browsing, etc.) + } + }, []) + + // Keyboard shortcut (Cmd/Ctrl + .) to toggle + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === '.') { + e.preventDefault() + setIsVisible((prev) => !prev) + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, []) + + // Save visibility state to localStorage + useEffect(() => { + if (isHydrated) { + try { + localStorage.setItem(STORAGE_KEY, String(isVisible)) + } catch { + // localStorage not available + } + } + }, [isVisible, isHydrated]) + + // Subscribe to global debug events via custom event + useEffect(() => { + const handleDebugEvent = (e: CustomEvent) => { + setEvents((prev) => [e.detail, ...prev].slice(0, 20)) + } + window.addEventListener('react-fathom:debug' as any, handleDebugEvent) + return () => window.removeEventListener('react-fathom:debug' as any, handleDebugEvent) + }, []) + + const clearEvents = () => setEvents([]) + + // Don't render until hydrated to avoid SSR mismatch + if (!isHydrated) { + return null + } + + // Show if forceShow is true (bypasses context check) + if (!forceShow) { + return null + } + + return ( + <> + {/* Toggle button */} + setIsVisible(!isVisible)} + boxShadow="lg" + > + {isVisible ? '✕' : '📊'} + + + {/* Event stream panel */} + {isVisible && ( + + {/* Header */} + + + 📊 + Event Stream + + + + + {/* Events list */} + + {events.length === 0 ? ( + + 🔍 + + No events yet. +
+ Navigate or interact to see tracking events. +
+
+ ) : ( + + {events.map((event) => ( + + ))} + + )} +
+ + {/* Footer */} + + + {events.length} event{events.length !== 1 ? 's' : ''} • Press{' '} + + ⌘. + {' '} + to toggle + + +
+ )} + + ) +} diff --git a/docs/components/docs/FileTree.tsx b/docs/components/docs/FileTree.tsx new file mode 100644 index 0000000..c1c2225 --- /dev/null +++ b/docs/components/docs/FileTree.tsx @@ -0,0 +1,119 @@ +'use client' + +import { Box, Flex, Text } from '@chakra-ui/react' +import { useState, type ReactNode } from 'react' + +interface FileTreeProps { + children: ReactNode +} + +export function FileTree({ children }: FileTreeProps) { + return ( + + {children} + + ) +} + +interface FolderProps { + name: string + children?: ReactNode + defaultOpen?: boolean +} + +export function Folder({ name, children, defaultOpen = true }: FolderProps) { + const [isOpen, setIsOpen] = useState(defaultOpen) + const hasChildren = Boolean(children) + + return ( + + hasChildren && setIsOpen(!isOpen)} + > + + {hasChildren ? (isOpen ? '▼' : '▶') : ''} + + 📁 + {name} + + {isOpen && children && ( + + {children} + + )} + + ) +} + +interface FileProps { + name: string + highlight?: boolean + added?: boolean + removed?: boolean +} + +export function File({ name, highlight, added, removed }: FileProps) { + // Determine file icon based on extension + const getIcon = (filename: string) => { + const ext = filename.split('.').pop()?.toLowerCase() + switch (ext) { + case 'ts': + case 'tsx': + return '📘' + case 'js': + case 'jsx': + return '📒' + case 'json': + return '📋' + case 'md': + case 'mdx': + return '📝' + case 'css': + case 'scss': + return '🎨' + case 'html': + return '🌐' + case 'svg': + case 'png': + case 'jpg': + case 'gif': + return '🖼️' + case 'env': + return '🔐' + case 'gitignore': + return '🚫' + default: + return '📄' + } + } + + let color = 'inherit' + if (highlight) color = 'blue.400' + if (added) color = 'green.400' + if (removed) color = 'red.400' + + return ( + + + {getIcon(name)} + + {name} + {added && +} + {removed && -} + + + ) +} diff --git a/docs/components/docs/KeyboardShortcuts.tsx b/docs/components/docs/KeyboardShortcuts.tsx new file mode 100644 index 0000000..e0d6857 --- /dev/null +++ b/docs/components/docs/KeyboardShortcuts.tsx @@ -0,0 +1,63 @@ +'use client' + +import { useEffect } from 'react' +import { useRouter } from 'next/navigation' + +interface KeyboardShortcutsProps { + prevHref?: string + nextHref?: string +} + +export function KeyboardShortcuts({ prevHref, nextHref }: KeyboardShortcutsProps) { + const router = useRouter() + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Ignore if user is typing in an input, textarea, or contenteditable + const target = e.target as HTMLElement + if ( + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.isContentEditable + ) { + return + } + + // Ignore if modifier keys are pressed (except for Cmd/Ctrl+K which Search handles) + if (e.altKey || e.shiftKey) { + return + } + + switch (e.key) { + case 'j': + // Next page + if (nextHref) { + e.preventDefault() + router.push(nextHref) + } + break + case 'k': + // Previous page + if (prevHref) { + e.preventDefault() + router.push(prevHref) + } + break + case '/': + // Focus search (Cmd+K is handled by Search component) + if (!e.metaKey && !e.ctrlKey) { + e.preventDefault() + // Dispatch a custom event that Search component listens to + window.dispatchEvent(new CustomEvent('open-search')) + } + break + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [router, prevHref, nextHref]) + + // This component doesn't render anything + return null +} diff --git a/docs/components/docs/MDXComponents.tsx b/docs/components/docs/MDXComponents.tsx new file mode 100644 index 0000000..087d344 --- /dev/null +++ b/docs/components/docs/MDXComponents.tsx @@ -0,0 +1,204 @@ +import { + Box, + Code, + Heading, + Link, + List, + Table, + Text, +} from '@chakra-ui/react' +import NextLink from 'next/link' +import type { MDXComponents as MDXComponentsType } from 'mdx/types' +import { Accordion, AccordionItem, Collapsible } from './Accordion' +import { Callout } from './Callout' +import { Pre, Figure, Figcaption } from './CodeBlock' +import { FileTree, Folder, File } from './FileTree' +import { Steps } from './Steps' +import { Tabs, Tab } from './Tabs' +import { Cards, Card } from './Cards' +import { PackageInstall, NpmToYarn } from './PackageInstall' + +function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, '') +} + +export function getMDXComponents(): MDXComponentsType { + return MDXComponents +} + +export const MDXComponents: MDXComponentsType = { + h1: (props) => ( + + ), + h2: (props) => { + const id = props.children ? slugify(String(props.children)) : undefined + return ( + + {props.children} + {id && ( + + # + + )} + + ) + }, + h3: (props) => { + const id = props.children ? slugify(String(props.children)) : undefined + return ( + + {props.children} + {id && ( + + # + + )} + + ) + }, + h4: (props) => ( + + ), + p: (props) => ( + + ), + a: ({ href, ...props }) => { + const isExternal = href?.startsWith('http') + if (isExternal) { + return ( + + ) + } + return ( + + + + ) + }, + ul: (props) => ( + + ), + ol: (props) => ( + + ), + li: (props) => ( + + ), + code: ({ children, className, ...props }) => { + // If it has data-theme, it's from rehype-pretty-code - pass through + if ('data-theme' in props || 'data-language' in props) { + return {children} + } + // If it's inline code (no className), render as inline + if (!className) { + return ( + + {children} + + ) + } + // Block code without highlighting + return {children} + }, + pre: Pre, + figure: Figure, + figcaption: Figcaption, + table: (props) => ( + + + + ), + thead: Table.Header, + tbody: Table.Body, + tr: Table.Row, + th: (props) => ( + + ), + td: Table.Cell, + blockquote: (props) => ( + + ), + hr: () => , + // Custom components + Accordion, + AccordionItem, + Callout, + Collapsible, + FileTree, + Folder, + File, + Steps, + Tabs, + Tab, + Cards, + Card, + PackageInstall, + NpmToYarn, +} diff --git a/docs/components/docs/MDXContent.tsx b/docs/components/docs/MDXContent.tsx new file mode 100644 index 0000000..d6c6531 --- /dev/null +++ b/docs/components/docs/MDXContent.tsx @@ -0,0 +1,12 @@ +'use client' + +import { MDXRemote, type MDXRemoteSerializeResult } from 'next-mdx-remote' +import { MDXComponents } from './MDXComponents' + +interface MDXContentProps { + source: MDXRemoteSerializeResult +} + +export function MDXContent({ source }: MDXContentProps) { + return +} diff --git a/docs/components/docs/MobileNav.tsx b/docs/components/docs/MobileNav.tsx new file mode 100644 index 0000000..93ae15e --- /dev/null +++ b/docs/components/docs/MobileNav.tsx @@ -0,0 +1,119 @@ +'use client' + +import { + Box, + IconButton, + Link, + Text, + VStack, +} from '@chakra-ui/react' +import NextLink from 'next/link' +import { usePathname } from 'next/navigation' +import { useState } from 'react' +import type { NavItem } from '@/lib/docs' + +interface MobileNavProps { + nav: NavItem[] +} + +export function MobileNav({ nav }: MobileNavProps) { + const [isOpen, setIsOpen] = useState(false) + const pathname = usePathname() + + return ( + + setIsOpen(!isOpen)} + > + {isOpen ? '✕' : '☰'} + + {isOpen && ( + + + {nav.map((item) => ( + setIsOpen(false)} + /> + ))} + + + )} + + ) +} + +interface MobileNavItemProps { + item: NavItem + pathname: string + onClose: () => void + depth?: number +} + +function MobileNavItem({ item, pathname, onClose, depth = 0 }: MobileNavItemProps) { + const isActive = pathname === item.href + const hasChildren = item.children && item.children.length > 0 + + return ( + + {item.href ? ( + 0 ? `${depth * 16 + 12}px` : 3} + borderRadius="md" + fontWeight={isActive ? 'semibold' : 'normal'} + color={isActive ? 'fg' : 'fg.muted'} + bg={isActive ? 'bg.muted' : 'transparent'} + onClick={onClose} + > + {item.title} + + ) : ( + 0 ? `${depth * 16 + 12}px` : 3} + fontSize="sm" + fontWeight="semibold" + textTransform="uppercase" + letterSpacing="wider" + color="fg.muted" + mt={depth === 0 ? 4 : 0} + > + {item.title} + + )} + {hasChildren && ( + + {item.children!.map((child) => ( + + ))} + + )} + + ) +} diff --git a/docs/components/docs/Navbar.tsx b/docs/components/docs/Navbar.tsx new file mode 100644 index 0000000..856cce7 --- /dev/null +++ b/docs/components/docs/Navbar.tsx @@ -0,0 +1,62 @@ +'use client' + +import { + Box, + Container, + Flex, + HStack, + Link, +} from '@chakra-ui/react' +import NextLink from 'next/link' +import { ColorModeButton } from './ColorModeButton' +import { MobileNav } from './MobileNav' +import { Search } from './Search' +import type { NavItem } from '@/lib/docs' + +interface NavbarProps { + nav: NavItem[] +} + +export function Navbar({ nav }: NavbarProps) { + return ( + + + + + + react-fathom + + + + Docs + + + API + + + GitHub + + + + + + + + + + + + ) +} diff --git a/docs/components/docs/PackageInstall.tsx b/docs/components/docs/PackageInstall.tsx new file mode 100644 index 0000000..b322b02 --- /dev/null +++ b/docs/components/docs/PackageInstall.tsx @@ -0,0 +1,208 @@ +'use client' + +import { Box, Flex, IconButton } from '@chakra-ui/react' +import { useState } from 'react' + +type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun' + +interface PackageInstallProps { + packages: string | string[] + dev?: boolean +} + +const commands: Record = { + npm: { install: 'npm install', devFlag: '-D' }, + yarn: { install: 'yarn add', devFlag: '-D' }, + pnpm: { install: 'pnpm add', devFlag: '-D' }, + bun: { install: 'bun add', devFlag: '-d' }, +} + +export function PackageInstall({ packages, dev = false }: PackageInstallProps) { + const [manager, setManager] = useState('npm') + const [copied, setCopied] = useState(false) + + const packageList = Array.isArray(packages) ? packages.join(' ') : packages + const { install, devFlag } = commands[manager] + const command = dev ? `${install} ${devFlag} ${packageList}` : `${install} ${packageList}` + + const handleCopy = async () => { + await navigator.clipboard.writeText(command) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + const managers: PackageManager[] = ['npm', 'yarn', 'pnpm', 'bun'] + + return ( + + {/* Tab header */} + + {managers.map((m) => ( + setManager(m)} + > + {m} + + ))} + + + {copied ? '✓' : '📋'} + + + + {/* Command */} + + {command} + + + ) +} + +// Simpler version that just transforms npm commands to other managers +interface NpmToYarnProps { + children: string +} + +export function NpmToYarn({ children }: NpmToYarnProps) { + const [manager, setManager] = useState('npm') + const [copied, setCopied] = useState(false) + + // Transform npm command to other package managers + const transformCommand = (cmd: string, to: PackageManager): string => { + let result = cmd.trim() + + if (to === 'npm') return result + + // npm install -> yarn/pnpm add/bun add + result = result.replace(/^npm install/, commands[to].install) + result = result.replace(/^npm i /, `${commands[to].install} `) + + // npm run -> yarn/pnpm/bun (no run needed for yarn) + if (to === 'yarn') { + result = result.replace(/^npm run /, 'yarn ') + } else { + result = result.replace(/^npm run /, `${to} run `) + } + + // npm init -> yarn init/pnpm init/bun init + result = result.replace(/^npm init/, `${to} init`) + + // npm ci -> yarn install --frozen-lockfile/pnpm install --frozen-lockfile + if (to === 'yarn') { + result = result.replace(/^npm ci$/, 'yarn install --frozen-lockfile') + } else if (to === 'pnpm') { + result = result.replace(/^npm ci$/, 'pnpm install --frozen-lockfile') + } else if (to === 'bun') { + result = result.replace(/^npm ci$/, 'bun install --frozen-lockfile') + } + + // -D flag + result = result.replace(/ -D /, ` ${commands[to].devFlag} `) + result = result.replace(/ --save-dev /, ` ${commands[to].devFlag} `) + + return result + } + + const command = transformCommand(children, manager) + + const handleCopy = async () => { + await navigator.clipboard.writeText(command) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + const managers: PackageManager[] = ['npm', 'yarn', 'pnpm', 'bun'] + + return ( + + + {managers.map((m) => ( + setManager(m)} + > + {m} + + ))} + + + {copied ? '✓' : '📋'} + + + + {command} + + + ) +} diff --git a/docs/components/docs/Search.tsx b/docs/components/docs/Search.tsx new file mode 100644 index 0000000..98b1e80 --- /dev/null +++ b/docs/components/docs/Search.tsx @@ -0,0 +1,297 @@ +'use client' + +import { + Box, + Flex, + Input, + Link, + Text, + VStack, +} from '@chakra-ui/react' +import NextLink from 'next/link' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useFathom } from '@/lib/fathom' + +interface SearchResult { + url: string + title: string + excerpt: string +} + +interface PagefindResult { + url: string + meta: { title?: string } + excerpt: string +} + +interface PagefindUI { + search: (query: string) => Promise<{ results: { data: () => Promise }[] }> +} + +export function Search() { + const [isOpen, setIsOpen] = useState(false) + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const pagefindRef = useRef(null) + const inputRef = useRef(null) + const { trackEvent } = useFathom() + + // Load Pagefind on first open + useEffect(() => { + if (isOpen && !pagefindRef.current) { + const loadPagefind = async () => { + try { + // Use dynamic import with webpackIgnore to prevent build-time resolution + const pagefindPath = '/pagefind/pagefind.js' + const pagefind = await import(/* webpackIgnore: true */ pagefindPath) + await pagefind.init() + pagefindRef.current = pagefind + } catch (e) { + console.warn('Pagefind not available (run `npm run build` first)') + } + } + loadPagefind() + } + }, [isOpen]) + + // Focus input when modal opens + useEffect(() => { + if (isOpen && inputRef.current) { + setTimeout(() => inputRef.current?.focus(), 100) + } + }, [isOpen]) + + // Keyboard shortcut (Cmd/Ctrl + K) and custom event + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault() + trackEvent('search-open') + setIsOpen(true) + } + if (e.key === 'Escape') { + setIsOpen(false) + } + } + const handleOpenSearch = () => { + trackEvent?.('search-open') + setIsOpen(true) + } + + document.addEventListener('keydown', handleKeyDown) + window.addEventListener('open-search', handleOpenSearch) + return () => { + document.removeEventListener('keydown', handleKeyDown) + window.removeEventListener('open-search', handleOpenSearch) + } + }, [trackEvent]) + + // Search handler + const handleSearch = useCallback(async (searchQuery: string) => { + setQuery(searchQuery) + + if (!searchQuery.trim() || !pagefindRef.current) { + setResults([]) + return + } + + setIsLoading(true) + try { + const search = await pagefindRef.current.search(searchQuery) + const searchResults = await Promise.all( + search.results.slice(0, 8).map(async (result) => { + const data = await result.data() + // Strip .html extension from URLs since Next.js routes don't have them + const url = data.url.replace(/\.html$/, '') + return { + url, + title: data.meta?.title || 'Untitled', + excerpt: data.excerpt, + } + }) + ) + setResults(searchResults) + } catch (e) { + console.error('Search error:', e) + setResults([]) + } finally { + setIsLoading(false) + } + }, []) + + const handleResultClick = (result: SearchResult) => { + trackEvent('search-result-click') + setIsOpen(false) + setQuery('') + setResults([]) + } + + const handleOpen = () => { + trackEvent?.('search-open') + setIsOpen(true) + } + + return ( + <> + {/* Search trigger button */} + + 🔍 + Search... + + ⌘K + + + + {/* Search modal */} + {isOpen && ( + setIsOpen(false)} + > + {/* Backdrop */} + + + {/* Modal content */} + + e.stopPropagation()} + > + {/* Search input */} + + 🔍 + handleSearch(e.target.value)} + placeholder="Search documentation..." + variant="flushed" + border="none" + fontSize="lg" + _focus={{ boxShadow: 'none' }} + /> + setIsOpen(false)} + > + ESC + + + + {/* Results */} + + {isLoading ? ( + + Searching... + + ) : results.length > 0 ? ( + + {results.map((result, i) => ( + handleResultClick(result)} + > + + + + {result.title} + + + + + + ))} + + ) : query ? ( + + No results found for "{query}" + + ) : ( + + Start typing to search... + + )} + + + {/* Footer */} + + ↑↓ to navigate + ↵ to select + + + + + )} + + ) +} diff --git a/docs/components/docs/Sidebar.tsx b/docs/components/docs/Sidebar.tsx new file mode 100644 index 0000000..52a4702 --- /dev/null +++ b/docs/components/docs/Sidebar.tsx @@ -0,0 +1,95 @@ +'use client' + +import { Box, Link, Text, VStack } from '@chakra-ui/react' +import NextLink from 'next/link' +import { usePathname } from 'next/navigation' +import type { NavItem } from '@/lib/docs' + +interface SidebarProps { + nav: NavItem[] +} + +export function Sidebar({ nav }: SidebarProps) { + const pathname = usePathname() + + return ( + + + {nav.map((item) => ( + + ))} + + + ) +} + +interface SidebarItemProps { + item: NavItem + pathname: string + depth?: number +} + +function SidebarItem({ item, pathname, depth = 0 }: SidebarItemProps) { + const isActive = pathname === item.href + const hasChildren = item.children && item.children.length > 0 + + return ( + + {item.href ? ( + 0 ? `${depth * 12 + 12}px` : 3} + borderRadius="md" + fontSize="sm" + fontWeight={isActive ? 'semibold' : 'normal'} + color={isActive ? 'fg' : 'fg.muted'} + bg={isActive ? 'bg.muted' : 'transparent'} + _hover={{ bg: 'bg.muted', color: 'fg' }} + > + {item.title} + + ) : ( + 0 ? `${depth * 12 + 12}px` : 3} + fontSize="xs" + fontWeight="semibold" + textTransform="uppercase" + letterSpacing="wider" + color="fg.muted" + mt={depth === 0 ? 4 : 0} + _first={{ mt: 0 }} + > + {item.title} + + )} + {hasChildren && ( + + {item.children!.map((child) => ( + + ))} + + )} + + ) +} diff --git a/docs/components/docs/Steps.tsx b/docs/components/docs/Steps.tsx new file mode 100644 index 0000000..719a998 --- /dev/null +++ b/docs/components/docs/Steps.tsx @@ -0,0 +1,41 @@ +'use client' + +import { Box, Flex, Text } from '@chakra-ui/react' +import type { ReactNode } from 'react' + +interface StepsProps { + children: ReactNode +} + +export function Steps({ children }: StepsProps) { + return ( + + {children} + + ) +} + +interface StepProps { + title: string + children: ReactNode +} + +export function Step({ title, children }: StepProps) { + return ( + + + {title} + + {children} + + + ) +} diff --git a/docs/components/docs/TableOfContents.tsx b/docs/components/docs/TableOfContents.tsx new file mode 100644 index 0000000..23c2d25 --- /dev/null +++ b/docs/components/docs/TableOfContents.tsx @@ -0,0 +1,89 @@ +'use client' + +import { Box, Link, Text, VStack } from '@chakra-ui/react' +import { useEffect, useState } from 'react' +import type { TOCItem } from '@/lib/docs' + +interface TableOfContentsProps { + toc: TOCItem[] +} + +export function TableOfContents({ toc }: TableOfContentsProps) { + const [activeId, setActiveId] = useState('') + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id) + } + }) + }, + { rootMargin: '-80px 0px -80% 0px' } + ) + + const headings = document.querySelectorAll('h2, h3') + headings.forEach((heading) => observer.observe(heading)) + + return () => observer.disconnect() + }, []) + + if (toc.length === 0) { + return null + } + + return ( + + + On this page + + + {toc.map((item) => ( + + ))} + + + ) +} + +interface TOCLinkProps { + item: TOCItem + activeId: string +} + +function TOCLink({ item, activeId }: TOCLinkProps) { + const isActive = activeId === item.id + + return ( + <> + + {item.title} + + {item.children?.map((child) => ( + + ))} + + ) +} diff --git a/docs/components/docs/Tabs.tsx b/docs/components/docs/Tabs.tsx new file mode 100644 index 0000000..d057a7f --- /dev/null +++ b/docs/components/docs/Tabs.tsx @@ -0,0 +1,106 @@ +'use client' + +import { Box, Flex } from '@chakra-ui/react' +import { useState, createContext, useContext, type ReactNode } from 'react' + +interface TabsContextValue { + activeTab: string + setActiveTab: (tab: string) => void +} + +const TabsContext = createContext(null) + +interface TabsProps { + defaultValue?: string + children: ReactNode +} + +export function Tabs({ defaultValue, children }: TabsProps) { + const [activeTab, setActiveTab] = useState(defaultValue || '') + + return ( + + {children} + + ) +} + +interface TabListProps { + children: ReactNode +} + +export function TabList({ children }: TabListProps) { + return ( + + {children} + + ) +} + +interface TabTriggerProps { + value: string + children: ReactNode +} + +export function TabTrigger({ value, children }: TabTriggerProps) { + const context = useContext(TabsContext) + if (!context) throw new Error('TabTrigger must be used within Tabs') + + const isActive = context.activeTab === value + + return ( + context.setActiveTab(value)} + > + {children} + + ) +} + +interface TabContentProps { + value: string + children: ReactNode +} + +export function TabContent({ value, children }: TabContentProps) { + const context = useContext(TabsContext) + if (!context) throw new Error('TabContent must be used within Tabs') + + if (context.activeTab !== value) return null + + return {children} +} + +// Simple Tab component for MDX +interface TabProps { + label: string + children: ReactNode +} + +export function Tab({ label, children }: TabProps) { + return ( + + + {label} + + {children} + + ) +} diff --git a/docs/components/docs/index.ts b/docs/components/docs/index.ts new file mode 100644 index 0000000..8a3efc0 --- /dev/null +++ b/docs/components/docs/index.ts @@ -0,0 +1,21 @@ +export { Accordion, AccordionItem, Collapsible } from './Accordion' +export { AnnouncementBanner } from './AnnouncementBanner' +export { Breadcrumbs } from './Breadcrumbs' +export { Callout } from './Callout' +export { Cards, Card } from './Cards' +export { Changelog, ChangelogEntry } from './Changelog' +export { Pre, Figure, Figcaption } from './CodeBlock' +export { ColorModeButton } from './ColorModeButton' +export { DocsLayout } from './DocsLayout' +export { FileTree, Folder, File } from './FileTree' +export { KeyboardShortcuts } from './KeyboardShortcuts' +export { MDXComponents } from './MDXComponents' +export { MDXContent } from './MDXContent' +export { MobileNav } from './MobileNav' +export { Navbar } from './Navbar' +export { PackageInstall, NpmToYarn } from './PackageInstall' +export { Search } from './Search' +export { Sidebar } from './Sidebar' +export { Steps, Step } from './Steps' +export { TableOfContents } from './TableOfContents' +export { Tabs, TabList, TabTrigger, TabContent, Tab } from './Tabs' diff --git a/docs/content/_meta.ts b/docs/content/_meta.ts new file mode 100644 index 0000000..87c53f9 --- /dev/null +++ b/docs/content/_meta.ts @@ -0,0 +1,15 @@ +export default { + index: 'Introduction', + 'getting-started': 'Getting Started', + react: 'React', + 'react-router': 'React Router', + gatsby: 'Gatsby', + 'tanstack-router': 'TanStack Router', + nextjs: 'Next.js', + 'react-native': 'React Native', + api: 'API Reference', + guides: 'Guides', + troubleshooting: 'Troubleshooting', + contributing: 'Contributing', + changelog: 'Changelog', +} diff --git a/docs/content/api/_meta.ts b/docs/content/api/_meta.ts new file mode 100644 index 0000000..82b47df --- /dev/null +++ b/docs/content/api/_meta.ts @@ -0,0 +1,6 @@ +export default { + providers: 'Providers', + hooks: 'Hooks', + components: 'Components', + native: 'Native API', +} diff --git a/docs/content/api/components.mdx b/docs/content/api/components.mdx new file mode 100644 index 0000000..80182ea --- /dev/null +++ b/docs/content/api/components.mdx @@ -0,0 +1,176 @@ +--- +title: "Components" +description: "API reference for TrackPageview, TrackClick, and TrackVisible components." +--- + +# Components + +## TrackPageview + +Track a pageview when the component mounts. + +```tsx +import { TrackPageview } from 'react-fathom' + +function Dashboard() { + return ( + +
Dashboard content
+
+ ) +} +``` + +### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `url` | `string` | No | URL to track | +| `referrer` | `string` | No | Referrer URL | +| `children` | `ReactNode` | No | Child elements to render | + +### Without Children + +```tsx +<> + + + +``` + +### With Referrer + +```tsx + + + +``` + +--- + +## TrackClick + +Track an event when a child element is clicked. + +```tsx +import { TrackClick } from 'react-fathom' + +function SignUpSection() { + return ( + + + + ) +} +``` + +### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `eventName` | `string` | Yes | Event name to track | +| `_value` | `number` | No | Value in cents | +| `preventDefault` | `boolean` | No | Prevent default click behavior | +| `children` | `ReactNode` | Yes | Clickable child element | + +### With Value + +```tsx + + + +``` + +### With Links + +```tsx + + View Documentation + +``` + +### Multiple Children + +The click handler is attached to a wrapper `div`: + +```tsx + +
+

Feature

+

Description

+
+
+``` + +--- + +## TrackVisible + +Track an event when an element becomes visible in the viewport. + +```tsx +import { TrackVisible } from 'react-fathom' + +function PricingSection() { + return ( + +
+

Pricing

+ {/* pricing content */} +
+
+ ) +} +``` + +### Props + +| Prop | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `eventName` | `string` | Yes | - | Event name to track | +| `_value` | `number` | No | - | Value in cents | +| `threshold` | `number` | No | `0.1` | Visibility threshold (0-1) | +| `rootMargin` | `string` | No | - | IntersectionObserver root margin | +| `as` | `string` | No | `'div'` | HTML element to render | +| `children` | `ReactNode` | Yes | - | Child elements | + +### Custom Element Type + +```tsx + +

Footer content

+
+``` + +```tsx + +

Section Title

+
+``` + +### With Threshold + +```tsx +// Track when 50% of the element is visible + +
+

Welcome

+
+
+``` + +### With Root Margin + +```tsx +// Track 100px before the element enters the viewport + +
Content
+
+``` + +### Implementation Notes + +- Uses `IntersectionObserver` internally +- Only tracks once per mount +- Passes through all additional props to the wrapper element +- Falls back gracefully if IntersectionObserver is unavailable diff --git a/docs/content/api/hooks.mdx b/docs/content/api/hooks.mdx new file mode 100644 index 0000000..06995bb --- /dev/null +++ b/docs/content/api/hooks.mdx @@ -0,0 +1,244 @@ +--- +title: "Hooks" +description: "API reference for useFathom, useTrackOnMount, useTrackOnClick, and useTrackOnVisible hooks." +--- + +# Hooks + +## useFathom + +Main hook for accessing Fathom tracking methods. + +```tsx +import { useFathom } from 'react-fathom' + +function MyComponent() { + const { + trackPageview, + trackEvent, + trackGoal, + load, + setSite, + blockTrackingForMe, + enableTrackingForMe, + isTrackingEnabled, + client, + defaultPageviewOptions, + defaultEventOptions, + } = useFathom() +} +``` + +### Return Value + +| Property | Type | Description | +|----------|------|-------------| +| `trackPageview` | `(options?) => void` | Track a pageview | +| `trackEvent` | `(eventName, options?) => void` | Track a custom event | +| `trackGoal` | `(code, cents) => void` | Track a goal conversion | +| `load` | `(siteId, options?) => void` | Load Fathom with a site ID | +| `setSite` | `(siteId) => void` | Change the site ID | +| `blockTrackingForMe` | `() => void` | Block tracking for current user | +| `enableTrackingForMe` | `() => void` | Enable tracking for current user | +| `isTrackingEnabled` | `() => boolean` | Check if tracking is enabled | +| `client` | `FathomClient` | The underlying client instance | +| `defaultPageviewOptions` | `PageViewOptions` | Current default pageview options | +| `defaultEventOptions` | `EventOptions` | Current default event options | + +### trackPageview + +```tsx +const { trackPageview } = useFathom() + +// Track current page +trackPageview() + +// Track with custom URL +trackPageview({ url: '/custom-page' }) + +// Track with referrer +trackPageview({ url: '/landing', referrer: 'https://google.com' }) +``` + +**Options:** + +| Option | Type | Description | +|--------|------|-------------| +| `url` | `string` | URL to track | +| `referrer` | `string` | Referrer URL | + +### trackEvent + +```tsx +const { trackEvent } = useFathom() + +// Basic event +trackEvent('button-click') + +// Event with value (in cents) +trackEvent('purchase', { _value: 2999 }) // $29.99 +``` + +**Options:** + +| Option | Type | Description | +|--------|------|-------------| +| `_value` | `number` | Value in cents | +| `_site_id` | `string` | Override site ID | + +### trackGoal + +```tsx +const { trackGoal } = useFathom() + +// Track a goal conversion +trackGoal('SIGNUP', 0) // No value +trackGoal('PURCHASE', 4999) // $49.99 +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `code` | `string` | Goal code from Fathom dashboard | +| `cents` | `number` | Value in cents | + +--- + +## useTrackOnMount + +Track a pageview when a component mounts. + +```tsx +import { useTrackOnMount } from 'react-fathom' + +function LandingPage() { + useTrackOnMount({ url: '/landing' }) + + return
Welcome!
+} +``` + +### Options + +| Option | Type | Description | +|--------|------|-------------| +| `url` | `string` | URL to track | +| `referrer` | `string` | Referrer URL | + +All options are passed to `trackPageview`. + +--- + +## useTrackOnClick + +Returns a click handler that tracks an event. + +```tsx +import { useTrackOnClick } from 'react-fathom' + +function CTAButton() { + const handleClick = useTrackOnClick({ + eventName: 'cta-click', + _value: 100, + }) + + return +} +``` + +### Options + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `eventName` | `string` | Yes | Event name to track | +| `preventDefault` | `boolean` | No | Prevent default click behavior | +| `callback` | `(e?: MouseEvent) => void` | No | Called after tracking | +| `_value` | `number` | No | Value in cents | + +### With Callback + +```tsx +const handleClick = useTrackOnClick({ + eventName: 'signup-click', + callback: (e) => { + // Do something after tracking + router.push('/signup') + }, +}) +``` + +### With preventDefault + +```tsx +const handleClick = useTrackOnClick({ + eventName: 'link-click', + preventDefault: true, + callback: () => { + // Handle navigation manually + window.location.href = '/external-link' + }, +}) + +return External Link +``` + +--- + +## useTrackOnVisible + +Returns a ref that tracks an event when the element becomes visible. + +```tsx +import { useTrackOnVisible } from 'react-fathom' + +function HeroSection() { + const ref = useTrackOnVisible({ + eventName: 'hero-viewed', + }) + + return ( +
+

Welcome

+
+ ) +} +``` + +### Options + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `eventName` | `string` | Yes | Event name to track | +| `threshold` | `number` | No | Visibility threshold (0-1, default: `0.1`) | +| `rootMargin` | `string` | No | IntersectionObserver root margin | +| `callback` | `(entry: IntersectionObserverEntry) => void` | No | Called when visible | +| `_value` | `number` | No | Value in cents | + +### With Threshold + +```tsx +// Track when 50% visible +const ref = useTrackOnVisible({ + eventName: 'section-viewed', + threshold: 0.5, +}) +``` + +### With Callback + +```tsx +const ref = useTrackOnVisible({ + eventName: 'pricing-viewed', + callback: (entry) => { + console.log('Pricing section visible!', entry.intersectionRatio) + }, +}) +``` + +### Implementation Notes + +- Uses `IntersectionObserver` internally +- Only tracks once per mount +- Cleans up observer on unmount +- Falls back gracefully if IntersectionObserver is unavailable diff --git a/docs/content/api/native.mdx b/docs/content/api/native.mdx new file mode 100644 index 0000000..c908fa3 --- /dev/null +++ b/docs/content/api/native.mdx @@ -0,0 +1,185 @@ +--- +title: "Native API" +description: "API reference for React Native-specific exports from react-fathom/native." +--- + +# Native API + +React Native-specific exports from `react-fathom/native`. + +## FathomWebView + +Hidden WebView component that loads the Fathom Analytics script. + +```tsx +import { FathomWebView, type FathomWebViewRef } from 'react-fathom/native' + +const webViewRef = useRef(null) + + console.log('Ready!')} + onError={(err) => console.error(err)} + debug={__DEV__} +/> +``` + +### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `siteId` | `string` | Yes | Your Fathom site ID | +| `loadOptions` | `LoadOptions` | No | Options passed to `fathom.load()` | +| `scriptDomain` | `string` | No | Custom domain (default: `cdn.usefathom.com`) | +| `onReady` | `() => void` | No | Called when script loads | +| `onError` | `(error: string) => void` | No | Called on error | +| `debug` | `boolean` | No | Enable debug logging | + +### Ref Methods (FathomWebViewRef) + +| Method | Description | +|--------|-------------| +| `trackPageview(opts?)` | Track a pageview | +| `trackEvent(eventName, opts?)` | Track a custom event | +| `trackGoal(code, cents)` | Track a goal conversion | +| `blockTrackingForMe()` | Block tracking for user | +| `enableTrackingForMe()` | Enable tracking for user | +| `isReady()` | Check if WebView is ready | + +--- + +## createWebViewClient + +Factory function to create a client that communicates with a FathomWebView. + +```tsx +import { createWebViewClient } from 'react-fathom/native' + +const client = createWebViewClient( + () => webViewRef.current, + { debug: true } +) +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `getWebViewRef` | `() => FathomWebViewRef \| null` | Function returning the WebView ref | +| `options` | `WebViewClientOptions` | Configuration options | + +### Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `debug` | `boolean` | `false` | Enable debug logging | +| `enableQueue` | `boolean` | `true` | Queue commands before WebView ready | +| `maxQueueSize` | `number` | `100` | Maximum queued commands | + +### Return Value (WebViewFathomClient) + +Standard `FathomClient` methods plus: + +| Method | Description | +|--------|-------------| +| `processQueue()` | Manually process queued commands (returns count) | +| `getQueueLength()` | Get current queue length | +| `setWebViewReady()` | Signal WebView is ready (flushes queue) | + +--- + +## useNavigationTracking + +Hook to track React Navigation screen changes as pageviews. + +```tsx +import { useNavigationTracking } from 'react-fathom/native' + +useNavigationTracking({ + navigationRef, + transformRouteName: (name) => `/screens/${name}`, +}) +``` + +### Options + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `navigationRef` | `RefObject` | Yes | React Navigation container ref | +| `transformRouteName` | `(name: string) => string` | No | Transform route names | +| `shouldTrackRoute` | `(name: string, params?: object) => boolean` | No | Filter routes to track | +| `includeParams` | `boolean` | No | Include params in URL (default: `false`) | + +See [Navigation Tracking](/docs/react-native/navigation) for detailed usage. + +--- + +## useAppStateTracking + +Hook to track app foreground/background state changes. + +```tsx +import { useAppStateTracking } from 'react-fathom/native' + +useAppStateTracking({ + foregroundEventName: 'app-resumed', + backgroundEventName: 'app-paused', +}) +``` + +### Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `foregroundEventName` | `string` | `'app-foreground'` | Event for foreground | +| `backgroundEventName` | `string` | `'app-background'` | Event for background | +| `eventOptions` | `EventOptions` | - | Additional event options | +| `onStateChange` | `(state) => void` | - | Callback on state change | + +See [App State Tracking](/docs/react-native/app-state) for detailed usage. + +--- + +## Types + +### FathomClient + +Interface for Fathom client implementations: + +```tsx +interface FathomClient { + load: (siteId: string, options?: LoadOptions) => void + trackPageview: (opts?: PageViewOptions) => void + trackEvent: (eventName: string, opts?: EventOptions) => void + trackGoal: (code: string, cents: number) => void + setSite: (id: string) => void + blockTrackingForMe: () => void + enableTrackingForMe: () => void + isTrackingEnabled: () => boolean +} +``` + +### WebViewFathomClient + +Extends `FathomClient` with queue management: + +```tsx +interface WebViewFathomClient extends FathomClient { + processQueue: () => number + getQueueLength: () => number + setWebViewReady: () => void +} +``` + +### Re-exported Types + +These types are re-exported from `fathom-client` for convenience: + +```tsx +import type { + EventOptions, + LoadOptions, + PageViewOptions, +} from 'react-fathom/native' +``` diff --git a/docs/content/api/providers.mdx b/docs/content/api/providers.mdx new file mode 100644 index 0000000..ac9a3b7 --- /dev/null +++ b/docs/content/api/providers.mdx @@ -0,0 +1,194 @@ +--- +title: "Providers" +description: "API reference for FathomProvider, NextFathomProviderApp, and NativeFathomProvider." +--- + +# Providers + +## FathomProvider + +Main provider component for React apps. Wraps your application and provides the Fathom context. + +```tsx +import { FathomProvider } from 'react-fathom' + + + + +``` + +### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `siteId` | `string` | No* | Your Fathom Analytics site ID | +| `client` | `FathomClient` | No* | Custom Fathom client instance | +| `clientRef` | `MutableRefObject` | No | Ref populated with the client instance | +| `clientOptions` | `LoadOptions` | No | Options passed to `fathom-client` | +| `defaultPageviewOptions` | `PageViewOptions` | No | Default options merged into all `trackPageview` calls | +| `defaultEventOptions` | `EventOptions` | No | Default options merged into all `trackEvent` calls | +| `children` | `ReactNode` | Yes | Child components | + +*Either `siteId` or `client` must be provided. + +### clientOptions + +Options passed to the underlying `fathom-client`: + +| Option | Type | Description | +|--------|------|-------------| +| `auto` | `boolean` | Enable automatic pageview tracking (default: `true`) | +| `canonical` | `boolean` | Use canonical URL for tracking | +| `honorDNT` | `boolean` | Honor Do Not Track browser setting | +| `includedDomains` | `string[]` | Only track on these domains | +| `excludedDomains` | `string[]` | Don't track on these domains | +| `spa` | `'auto' \| 'history' \| 'hash'` | SPA mode for route detection | + +### Example with All Options + +```tsx + + + +``` + +### Nested Providers + +Providers can be nested to override defaults for specific sections: + +```tsx + + {/* Events here use _site_id: 'global' */} + + + {/* Events here use _site_id: 'dashboard' */} + + +``` + +--- + +## NextFathomProviderApp + +Client Component wrapper for Next.js App Router. Combines `FathomProvider` and `NextFathomTrackViewApp`. + +```tsx +import { NextFathomProviderApp } from 'react-fathom/next' + +// app/layout.tsx +export default function RootLayout({ children }) { + return ( + + + + {children} + + + + ) +} +``` + +### Props + +All `FathomProvider` props, plus: + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `disableAutoTrack` | `boolean` | `false` | Disable automatic pageview tracking on route changes | + +--- + +## NextFathomTrackViewApp + +Tracks pageviews for Next.js App Router. Must be used within a `FathomProvider`. + +```tsx +import { FathomProvider } from 'react-fathom' +import { NextFathomTrackViewApp } from 'react-fathom/next' + + + + {children} + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `disableAutoTrack` | `boolean` | `false` | Disable automatic route tracking | + +--- + +## NextFathomTrackViewPages + +Tracks pageviews for Next.js Pages Router. Must be used within a `FathomProvider`. + +```tsx +import { FathomProvider } from 'react-fathom' +import { NextFathomTrackViewPages } from 'react-fathom/next' + +// pages/_app.tsx +function MyApp({ Component, pageProps }) { + return ( + + + + + ) +} +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `disableAutoTrack` | `boolean` | `false` | Disable automatic route tracking | + +--- + +## NativeFathomProvider + +Provider for React Native apps. Manages a hidden WebView with Fathom's tracking script. + +```tsx +import { NativeFathomProvider } from 'react-fathom/native' + + + + +``` + +### Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `siteId` | `string` | Yes | Your Fathom site ID | +| `loadOptions` | `LoadOptions` | No | Options passed to `fathom.load()` | +| `scriptDomain` | `string` | No | Custom domain (default: `cdn.usefathom.com`) | +| `defaultPageviewOptions` | `PageViewOptions` | No | Default pageview options | +| `defaultEventOptions` | `EventOptions` | No | Default event options | +| `trackAppState` | `boolean` | No | Enable automatic app state tracking | +| `debug` | `boolean` | No | Enable debug logging | +| `onReady` | `() => void` | No | Called when Fathom script loads | +| `onError` | `(error: string) => void` | No | Called on script load error | +| `clientRef` | `MutableRefObject` | No | Ref for direct client access | +| `children` | `ReactNode` | Yes | Child components | + +See the [React Native guide](/docs/react-native) for detailed usage. diff --git a/docs/content/changelog.mdx b/docs/content/changelog.mdx new file mode 100644 index 0000000..ea74f5c --- /dev/null +++ b/docs/content/changelog.mdx @@ -0,0 +1,26 @@ +--- +title: Changelog +description: Release history and version changes for react-fathom +--- + +View the full release history on [GitHub Releases](https://github.com/ryanhefner/react-fathom/releases). + +## Recent Releases + +### 0.2.0 + +**January 2026** + +- Add Nextra documentation site and slim down README +- Upgrade dependencies +- Bump globals to 17.0.0 + +### 0.1.0 + +**Initial Release** + +- FathomProvider for React applications +- useFathom hook for tracking events +- Next.js App Router and Pages Router support +- React Native support with navigation tracking +- TypeScript support diff --git a/docs/content/contributing.mdx b/docs/content/contributing.mdx new file mode 100644 index 0000000..fe0897f --- /dev/null +++ b/docs/content/contributing.mdx @@ -0,0 +1,185 @@ +--- +title: "Contributing" +description: "How to contribute to the react-fathom project." +--- + +# Contributing + +Contributions are welcome! Whether it's bug fixes, new features, documentation improvements, or examples. + +## Ways to Contribute + +| Type | Description | +|------|-------------| +| **Bug Reports** | Found a bug? [Open an issue](https://github.com/ryanhefner/react-fathom/issues/new) with reproduction steps | +| **Feature Requests** | Have an idea? Discuss it in an issue first | +| **Bug Fixes** | PRs for documented issues are always welcome | +| **Documentation** | Help improve docs, add examples, fix typos | +| **Tests** | Increase test coverage or add edge case tests | + +## Development Setup + +### Prerequisites + +- Node.js 18+ +- npm 9+ + +### 1. Clone and Install + +```bash +git clone https://github.com/ryanhefner/react-fathom.git +cd react-fathom +npm install +``` + +### 2. Development Workflow + +```bash +# Run tests in watch mode during development +npm run test:watch + +# Run the full test suite +npm test + +# Build the package +npm run build + +# Type check without emitting +npm run typecheck +``` + +## Project Structure + +``` +react-fathom/ +├── src/ +│ ├── index.ts # Main entry point +│ ├── FathomProvider.tsx # Core provider component +│ ├── FathomContext.tsx # React context +│ ├── types.ts # TypeScript definitions +│ ├── hooks/ # React hooks +│ │ ├── useFathom.ts +│ │ ├── useTrackOnClick.ts +│ │ ├── useTrackOnMount.ts +│ │ └── useTrackOnVisible.ts +│ ├── components/ # Declarative tracking components +│ │ ├── TrackClick.tsx +│ │ ├── TrackPageview.tsx +│ │ └── TrackVisible.tsx +│ ├── next/ # Next.js-specific exports +│ │ └── index.ts +│ └── native/ # React Native exports +│ ├── index.ts +│ ├── FathomWebView.tsx +│ ├── createWebViewClient.ts +│ ├── NativeFathomProvider.tsx +│ ├── useNavigationTracking.ts +│ └── useAppStateTracking.ts +├── docs/ # Documentation site (Nextra) +└── dist/ # Built output (generated) +``` + +## Testing Guidelines + +We use [Vitest](https://vitest.dev/) for testing. All new features should include tests. + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage +``` + +### Writing Tests + +```tsx +// src/hooks/useMyHook.test.tsx +import { renderHook } from '@testing-library/react' +import { describe, it, expect, vi } from 'vitest' +import { useMyHook } from './useMyHook' +import { FathomProvider } from '../FathomProvider' + +describe('useMyHook', () => { + it('should track events correctly', () => { + const mockClient = { + load: vi.fn(), + trackPageview: vi.fn(), + trackEvent: vi.fn(), + trackGoal: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }) => ( + {children} + ) + + const { result } = renderHook(() => useMyHook(), { wrapper }) + + result.current.doSomething() + + expect(mockClient.trackEvent).toHaveBeenCalledWith('expected-event', {}) + }) +}) +``` + +## Code Style + +- **TypeScript**: All code should be fully typed +- **Formatting**: We use Prettier (run `npm run format` before committing) +- **Linting**: ESLint catches common issues (run `npm run lint`) +- **Naming**: + - Components: PascalCase (`TrackClick.tsx`) + - Hooks: camelCase with `use` prefix (`useFathom.ts`) + - Types: PascalCase (`FathomClient`) + +## Submitting a Pull Request + +1. **Fork** the repository +2. **Create a branch** from `main`: + ```bash + git checkout -b fix/my-bug-fix + # or + git checkout -b feature/my-new-feature + ``` +3. **Make your changes** with clear, focused commits +4. **Add or update tests** for your changes +5. **Ensure CI passes**: + ```bash + npm run lint + npm test + npm run build + ``` +6. **Submit a PR** with a clear description of what and why + +## Commit Messages + +Use clear, descriptive commit messages: + +``` +feat: add useTrackOnScroll hook for scroll tracking +fix: resolve duplicate pageview tracking in Next.js +docs: add troubleshooting section for ad blockers +test: add tests for native offline queue +refactor: simplify FathomContext default values +``` + +Prefixes: `feat`, `fix`, `docs`, `test`, `refactor`, `chore`, `perf` + +## Documentation + +The docs site uses [Nextra](https://nextra.site) and lives in the `/docs` directory. + +```bash +cd docs +npm install +npm run dev +``` + +When adding new features, please also update the relevant documentation pages. diff --git a/docs/content/gatsby/index.mdx b/docs/content/gatsby/index.mdx new file mode 100644 index 0000000..4ebea4d --- /dev/null +++ b/docs/content/gatsby/index.mdx @@ -0,0 +1,321 @@ +--- +title: "Gatsby" +description: "Integrate react-fathom with Gatsby for automatic pageview tracking." +--- + +# Gatsby Integration + +This guide covers integrating `react-fathom` with Gatsby applications for automatic pageview tracking. + +## Installation + +Install the required packages: + +```bash npm2yarn +npm install react-fathom fathom-client +``` + +## Setup Options + +There are two ways to integrate react-fathom with Gatsby: + +1. **Component-based** (recommended) — Use `GatsbyFathomTrackView` in your layout +2. **gatsby-browser.js** — Use helpers in your gatsby-browser.js file + +## Option 1: Component-Based (Recommended) + +This approach uses a React component in your layout file, consistent with other react-fathom integrations. + +### Layout Setup + +```tsx +// src/components/Layout.tsx +import React from 'react' +import { FathomProvider } from 'react-fathom' +import { GatsbyFathomTrackView } from 'react-fathom/gatsby' + +interface LayoutProps { + children: React.ReactNode +} + +export function Layout({ children }: LayoutProps) { + return ( + + + {children} + + ) +} +``` + +### Wrap Pages with Layout + +Use the layout in your pages: + +```tsx +// src/pages/index.tsx +import React from 'react' +import { Layout } from '../components/Layout' + +export default function HomePage() { + return ( + +

Welcome to my Gatsby site

+
+ ) +} +``` + +Or use `gatsby-browser.js` to wrap all pages: + +```js +// gatsby-browser.js +import React from 'react' +import { FathomProvider } from 'react-fathom' +import { GatsbyFathomTrackView } from 'react-fathom/gatsby' + +export const wrapRootElement = ({ element }) => ( + + + {element} + +) +``` + +## Option 2: gatsby-browser.js Helpers + +For users who prefer configuring analytics entirely in `gatsby-browser.js`, react-fathom provides helper functions. + +### Using createGatsbyFathomPlugins + +```js +// gatsby-browser.js +import { createGatsbyFathomPlugins } from 'react-fathom/gatsby' + +const fathomPlugins = createGatsbyFathomPlugins({ + siteId: 'YOUR_SITE_ID', + // Optional configuration + includedDomains: ['yourdomain.com'], + excludedDomains: ['staging.yourdomain.com'], + spa: 'auto', + honorDNT: false, +}) + +export const onClientEntry = fathomPlugins.onClientEntry +export const onRouteUpdate = fathomPlugins.onRouteUpdate +``` + +### Manual Configuration + +For more control, use `trackGatsbyPageview` directly: + +```js +// gatsby-browser.js +import Fathom from 'fathom-client' +import { trackGatsbyPageview } from 'react-fathom/gatsby' + +export const onClientEntry = () => { + Fathom.load('YOUR_SITE_ID', { + includedDomains: ['yourdomain.com'], + }) +} + +export const onRouteUpdate = ({ location }) => { + trackGatsbyPageview(Fathom, location, { + includeSearchParams: true, + includeHash: false, + }) +} +``` + +## GatsbyFathomTrackView Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `disableAutoTrack` | `boolean` | `false` | Disable automatic pageview tracking | +| `includeSearchParams` | `boolean` | `true` | Include query parameters in tracked URLs | +| `includeHash` | `boolean` | `false` | Include URL hash in tracked URLs | +| `transformUrl` | `(url: string) => string \| null` | — | Transform URL before tracking; return `null` to skip | + +## Tracking Custom Events + +Use the `useFathom` hook in any component: + +```tsx +import React from 'react' +import { useFathom } from 'react-fathom' + +function NewsletterForm() { + const { trackEvent } = useFathom() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + trackEvent('newsletter-signup') + // Submit form... + } + + return ( +
+ + +
+ ) +} +``` + +## URL Transformation + +### Sanitizing Dynamic Routes + +Remove dynamic segments from URLs for cleaner analytics: + +```tsx + { + // /blog/post-123 → /blog/[slug] + return url.replace(/\/blog\/[^/]+/, '/blog/[slug]') + }} +/> +``` + +### Excluding Routes + +Skip tracking for specific routes: + +```tsx + { + // Don't track admin pages + if (url.includes('/admin')) { + return null + } + return url + }} +/> +``` + +## Environment Variables + +Store your site ID in environment variables: + +```bash +# .env.development +GATSBY_FATHOM_SITE_ID=YOUR_SITE_ID +``` + +```tsx + +``` + +## Local Development + +Enable tracking on localhost: + +```tsx + +``` + +Or with gatsby-browser.js helpers: + +```js +const fathomPlugins = createGatsbyFathomPlugins({ + siteId: 'YOUR_SITE_ID', + includedDomains: ['localhost', 'yourdomain.com'], +}) +``` + +## TypeScript Support + +All Gatsby exports are fully typed. Import types as needed: + +```tsx +import type { GatsbyFathomTrackViewProps } from 'react-fathom/gatsby' +``` + +## How It Works + +Gatsby uses `@reach/router` internally. The `GatsbyFathomTrackView` component listens to route changes via `@reach/router`'s `globalHistory`: + +1. On mount, tracks the initial pageview +2. Listens for `PUSH` and `POP` history actions +3. Constructs the full URL from `window.location.origin` and the route path +4. Calls `trackPageview` with the constructed URL + +The `gatsby-browser.js` helpers use Gatsby's built-in `onRouteUpdate` API, which fires after each client-side navigation. + +## Complete Example + +Here's a full Gatsby setup with react-fathom: + +```tsx +// src/components/Layout.tsx +import React from 'react' +import { FathomProvider } from 'react-fathom' +import { GatsbyFathomTrackView } from 'react-fathom/gatsby' + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + { + // Normalize blog post URLs + return url.replace(/\/blog\/[^/]+$/, '/blog/[slug]') + }} + /> +
...
+
{children}
+
...
+
+ ) +} +``` + +```tsx +// src/pages/index.tsx +import React from 'react' +import { useFathom } from 'react-fathom' +import { Layout } from '../components/Layout' + +export default function HomePage() { + const { trackEvent } = useFathom() + + return ( + +

Welcome

+ +
+ ) +} +``` + +## Troubleshooting + +### Events not appearing in Fathom? + +1. Verify your site ID matches your [Fathom dashboard](https://app.usefathom.com) +2. Check for ad blockers (test in incognito mode) +3. Add `localhost` to `includedDomains` for local testing +4. Ensure `FathomProvider` wraps your entire app + +### Duplicate pageviews? + +If using both the component approach and gatsby-browser.js helpers, you may get duplicate tracking. Use only one approach. + +### Route changes not tracking? + +Ensure `GatsbyFathomTrackView` is rendered inside `FathomProvider` and that the provider persists across route changes (use gatsby-browser.js `wrapRootElement`). diff --git a/docs/content/getting-started.mdx b/docs/content/getting-started.mdx new file mode 100644 index 0000000..2119675 --- /dev/null +++ b/docs/content/getting-started.mdx @@ -0,0 +1,113 @@ +--- +title: "Getting Started" +description: "Install and set up react-fathom in your application." +--- + +# Getting Started + +## Installation + +Install `react-fathom` and its peer dependency `fathom-client`: + +```bash npm2yarn +npm install react-fathom fathom-client +``` + +## Peer Dependencies + +| Package | Version | Required for | +|---------|---------|--------------| +| `react` | >= 16.8 | All | +| `react-dom` | >= 16.8 | Web only | +| `fathom-client` | >= 3.0.0 | Web only (not needed for React Native) | +| `next` | >= 10.0.0 | Next.js providers only | +| `react-router-dom` | >= 6.0.0 | React Router / Remix only | +| `@reach/router` | >= 1.3.0 | Gatsby only | +| `gatsby` | >= 4.0.0 | Gatsby only | +| `@tanstack/react-router` | >= 1.0.0 | TanStack Router only | +| `react-native` | >= 0.60.0 | React Native only | +| `react-native-webview` | >= 11.0.0 | React Native only | + +## Basic Setup + +### 1. Get Your Site ID + +Your Fathom site ID is an 8-character alphanumeric string (e.g., `ABCD1234`). Find it in your [Fathom dashboard](https://app.usefathom.com) under **Settings → Site ID**. + +### 2. Add the Provider + +Wrap your application with `FathomProvider`: + +```tsx +import { FathomProvider } from 'react-fathom' + +function App() { + return ( + + + + ) +} +``` + +### 3. Track Events + +Use the `useFathom` hook to track custom events: + +```tsx +import { useFathom } from 'react-fathom' + +function SignUpButton() { + const { trackEvent } = useFathom() + + return ( + + ) +} +``` + +Pageviews are tracked automatically by default. + +## Next Steps + +- [React usage guide](/docs/react) — Hooks and declarative components +- [React Router integration](/docs/react-router) — Automatic pageview tracking for React Router and Remix +- [Gatsby integration](/docs/gatsby) — Automatic pageview tracking for Gatsby +- [TanStack Router integration](/docs/tanstack-router) — Type-safe routing with automatic tracking +- [Next.js integration](/docs/nextjs) — App Router and Pages Router +- [React Native setup](/docs/react-native) — Mobile apps with offline queuing +- [API Reference](/docs/api/providers) — Full provider props and options + +## Environment Variables + +For production apps, store your site ID in an environment variable: + +```tsx + +``` + +For Next.js, use the `NEXT_PUBLIC_` prefix: + +```bash +# .env.local +NEXT_PUBLIC_FATHOM_SITE_ID=YOUR_SITE_ID +``` + +```tsx + +``` + +## Local Development + +By default, Fathom only tracks events from configured domains. To track during local development: + +```tsx + +``` diff --git a/docs/content/guides/_meta.ts b/docs/content/guides/_meta.ts new file mode 100644 index 0000000..8ba2fd5 --- /dev/null +++ b/docs/content/guides/_meta.ts @@ -0,0 +1,6 @@ +export default { + 'default-options': 'Default Options', + 'custom-client': 'Custom Client', + testing: 'Testing', + 'custom-domains': 'Custom Domains', +} diff --git a/docs/content/guides/custom-client.mdx b/docs/content/guides/custom-client.mdx new file mode 100644 index 0000000..0a4c967 --- /dev/null +++ b/docs/content/guides/custom-client.mdx @@ -0,0 +1,187 @@ +--- +title: "Custom Client" +description: "Provide a custom Fathom client implementation for testing, SSR, or custom analytics pipelines." +--- + +# Custom Client + +Provide a custom Fathom client implementation for testing, SSR, or custom analytics pipelines. + +## FathomClient Interface + +Your custom client must implement the `FathomClient` interface: + +```tsx +import type { FathomClient, EventOptions, LoadOptions, PageViewOptions } from 'react-fathom' + +const myCustomClient: FathomClient = { + load: (siteId: string, options?: LoadOptions) => { + // Initialize your tracking + }, + trackPageview: (opts?: PageViewOptions) => { + // Track pageview + }, + trackEvent: (eventName: string, opts?: EventOptions) => { + // Track custom event + }, + trackGoal: (code: string, cents: number) => { + // Track goal conversion + }, + setSite: (id: string) => { + // Change site ID + }, + blockTrackingForMe: () => { + // Block tracking + }, + enableTrackingForMe: () => { + // Enable tracking + }, + isTrackingEnabled: () => { + // Return tracking status + return true + }, +} +``` + +## Using a Custom Client + +Pass your client to `FathomProvider`: + +```tsx + + + +``` + +When using a custom client, you don't need to provide `siteId` (unless your client needs it). + +## Use Cases + +### Console Logger (Development) + +Log all tracking calls to the console: + +```tsx +const consoleClient: FathomClient = { + load: (id, opts) => console.log('[Fathom] load:', id, opts), + trackPageview: (opts) => console.log('[Fathom] pageview:', opts), + trackEvent: (name, opts) => console.log('[Fathom] event:', name, opts), + trackGoal: (code, cents) => console.log('[Fathom] goal:', code, cents), + setSite: (id) => console.log('[Fathom] setSite:', id), + blockTrackingForMe: () => console.log('[Fathom] blocked'), + enableTrackingForMe: () => console.log('[Fathom] enabled'), + isTrackingEnabled: () => true, +} + +// Use in development + +``` + +### No-op Client (SSR) + +Prevent errors during server-side rendering: + +```tsx +const noopClient: FathomClient = { + load: () => {}, + trackPageview: () => {}, + trackEvent: () => {}, + trackGoal: () => {}, + setSite: () => {}, + blockTrackingForMe: () => {}, + enableTrackingForMe: () => {}, + isTrackingEnabled: () => false, +} + +// Use on server +const client = typeof window === 'undefined' ? noopClient : undefined + + +``` + +### Analytics Aggregator + +Forward events to multiple analytics services: + +```tsx +import * as fathom from 'fathom-client' +import mixpanel from 'mixpanel-browser' + +const aggregatorClient: FathomClient = { + load: (siteId, options) => { + fathom.load(siteId, options) + mixpanel.init('YOUR_MIXPANEL_TOKEN') + }, + trackPageview: (opts) => { + fathom.trackPageview(opts) + mixpanel.track('Page View', { url: opts?.url }) + }, + trackEvent: (name, opts) => { + fathom.trackEvent(name, opts) + mixpanel.track(name, opts) + }, + trackGoal: (code, cents) => { + fathom.trackGoal(code, cents) + mixpanel.track('Goal', { code, value: cents / 100 }) + }, + setSite: fathom.setSite, + blockTrackingForMe: fathom.blockTrackingForMe, + enableTrackingForMe: fathom.enableTrackingForMe, + isTrackingEnabled: fathom.isTrackingEnabled, +} +``` + +### Event Transformer + +Transform events before sending: + +```tsx +import * as fathom from 'fathom-client' + +const transformerClient: FathomClient = { + load: fathom.load, + trackPageview: fathom.trackPageview, + trackEvent: (name, opts) => { + // Prefix all event names + const prefixedName = `app_${name}` + + // Add timestamp to all events + const enrichedOpts = { + ...opts, + _timestamp: Date.now(), + } + + fathom.trackEvent(prefixedName, enrichedOpts) + }, + trackGoal: fathom.trackGoal, + setSite: fathom.setSite, + blockTrackingForMe: fathom.blockTrackingForMe, + enableTrackingForMe: fathom.enableTrackingForMe, + isTrackingEnabled: fathom.isTrackingEnabled, +} +``` + +## Accessing the Client + +Use `clientRef` to access the resolved client from a parent component: + +```tsx +import { useRef } from 'react' +import { FathomProvider, FathomClient } from 'react-fathom' + +function App() { + const clientRef = useRef(null) + + const handleExternalEvent = () => { + // Access client directly without hooks + clientRef.current?.trackEvent('external-event') + } + + return ( + + + + + ) +} +``` diff --git a/docs/content/guides/custom-domains.mdx b/docs/content/guides/custom-domains.mdx new file mode 100644 index 0000000..f7abaf6 --- /dev/null +++ b/docs/content/guides/custom-domains.mdx @@ -0,0 +1,133 @@ +# Custom Domains + +Use Fathom's custom domains feature to bypass ad blockers and improve tracking accuracy. + +## What Are Custom Domains? + +[Fathom's custom domains](https://usefathom.com/docs/script/custom-domains) let you serve the tracking script from your own domain (e.g., `stats.yourdomain.com`). This helps avoid ad blockers that target `cdn.usefathom.com`. + +## Setup in Fathom + +1. Go to your [Fathom dashboard](https://app.usefathom.com) +2. Navigate to **Settings → Custom Domains** +3. Add your custom domain (e.g., `stats.yourdomain.com`) +4. Configure DNS as instructed +5. Wait for SSL certificate provisioning + +## Using Custom Domains + +### React / Next.js + +Pass the custom domain in `clientOptions`: + +```tsx + +``` + +### React Native + +Use the `scriptDomain` prop: + +```tsx + + + +``` + +Or with manual WebView setup: + +```tsx + +``` + +## Verifying Setup + +### Check Script Loading + +Open your browser's Network tab and verify the Fathom script loads from your custom domain: + +``` +https://stats.yourdomain.com/script.js +``` + +### Check Tracking Requests + +Tracking requests should also go to your custom domain: + +``` +https://stats.yourdomain.com/... +``` + +## Troubleshooting + +### Script Not Loading + +1. Verify DNS is configured correctly +2. Check SSL certificate is active in Fathom dashboard +3. Ensure the domain matches exactly (no trailing slash) + +### CORS Errors + +Custom domains handle CORS automatically. If you see CORS errors: + +1. Verify the custom domain is fully provisioned +2. Check you're using HTTPS +3. Clear browser cache + +### React Native Not Working + +Ensure the WebView can access your custom domain: + +1. Check network connectivity +2. Verify no firewall blocking the domain +3. Use `debug={true}` to see detailed logs + +```tsx + console.error('Load error:', err)} +> +``` + +## Multiple Environments + +Use environment variables to switch domains: + +```tsx +// React / Next.js + +``` + +```bash +# .env.local +NEXT_PUBLIC_FATHOM_SITE_ID=ABCD1234 +NEXT_PUBLIC_FATHOM_SCRIPT_URL=https://stats.yourdomain.com/script.js +``` + +```tsx +// React Native + +``` diff --git a/docs/content/guides/default-options.mdx b/docs/content/guides/default-options.mdx new file mode 100644 index 0000000..92f8bc7 --- /dev/null +++ b/docs/content/guides/default-options.mdx @@ -0,0 +1,142 @@ +--- +title: "Default Options" +description: "Set app-wide defaults that automatically merge into all tracking calls." +--- + +# Default Options + +Set app-wide defaults that automatically merge into all tracking calls. + +## How It Works + +Default options are spread first, then any options you pass to individual tracking calls override them: + +```tsx + + {/* All trackEvent calls include _site_id: 'my-app' unless overridden */} + +``` + +```tsx +const { trackEvent } = useFathom() + +// Uses default: { _site_id: 'my-app' } +trackEvent('button-click') + +// Merges with default: { _site_id: 'my-app', _value: 100 } +trackEvent('purchase', { _value: 100 }) + +// Overrides default: { _site_id: 'custom-site', _value: 50 } +trackEvent('special-event', { _site_id: 'custom-site', _value: 50 }) +``` + +## Default Pageview Options + +Set defaults for all `trackPageview` calls: + +```tsx + +``` + +## Default Event Options + +Set defaults for all `trackEvent` calls: + +```tsx + +``` + +## Nested Providers + +When nesting providers, child providers inherit defaults from parents but can override them: + +```tsx + + {/* Events here use _site_id: 'global' */} + + + + {/* Events here use _site_id: 'dashboard' */} + + + + + {/* Events here use _site_id: 'settings' */} + + + +``` + +## Use Cases + +### Multi-tenant Apps + +Track which tenant generated each event: + +```tsx +function TenantProvider({ tenantId, children }) { + return ( + + {children} + + ) +} +``` + +### A/B Testing + +Include experiment variant in all events: + +```tsx +function ExperimentProvider({ variant, children }) { + return ( + + {children} + + ) +} +``` + +### Consistent Referrer + +Set a consistent referrer for all pageviews: + +```tsx + +``` + +## Accessing Current Defaults + +The current defaults are available from `useFathom`: + +```tsx +const { defaultPageviewOptions, defaultEventOptions } = useFathom() + +console.log('Current event defaults:', defaultEventOptions) +``` diff --git a/docs/content/guides/testing.mdx b/docs/content/guides/testing.mdx new file mode 100644 index 0000000..88df6a0 --- /dev/null +++ b/docs/content/guides/testing.mdx @@ -0,0 +1,232 @@ +--- +title: "Testing" +description: "Use mock clients to test tracking behavior without sending real analytics." +--- + +# Testing + +Use mock clients to test tracking behavior without sending real analytics. + +## Mock Client Setup + +Create a mock client with Jest or Vitest: + +```tsx +import { FathomProvider, type FathomClient } from 'react-fathom' + +const mockClient: FathomClient = { + load: vi.fn(), + trackPageview: vi.fn(), + trackEvent: vi.fn(), + trackGoal: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), +} +``` + +## Testing Components + +### Testing Event Tracking + +```tsx +import { render, screen, fireEvent } from '@testing-library/react' +import { FathomProvider, type FathomClient } from 'react-fathom' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { SignUpButton } from './SignUpButton' + +describe('SignUpButton', () => { + let mockClient: FathomClient + + beforeEach(() => { + mockClient = { + load: vi.fn(), + trackPageview: vi.fn(), + trackEvent: vi.fn(), + trackGoal: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + }) + + it('tracks click event', () => { + render( + + + + ) + + fireEvent.click(screen.getByRole('button')) + + expect(mockClient.trackEvent).toHaveBeenCalledWith('signup-click', {}) + }) + + it('tracks event with value', () => { + render( + + + + ) + + fireEvent.click(screen.getByRole('button')) + + expect(mockClient.trackEvent).toHaveBeenCalledWith('purchase', { _value: 2999 }) + }) +}) +``` + +### Testing Pageview Tracking + +```tsx +import { render } from '@testing-library/react' +import { FathomProvider } from 'react-fathom' +import { describe, it, expect, vi } from 'vitest' +import { Dashboard } from './Dashboard' + +describe('Dashboard', () => { + it('tracks pageview on mount', () => { + const mockClient = { + load: vi.fn(), + trackPageview: vi.fn(), + trackEvent: vi.fn(), + trackGoal: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + + ) + + expect(mockClient.trackPageview).toHaveBeenCalledWith({ url: '/dashboard' }) + }) +}) +``` + +## Test Utilities + +### Create a Reusable Wrapper + +```tsx +// test-utils.tsx +import { ReactNode } from 'react' +import { FathomProvider, type FathomClient } from 'react-fathom' +import { vi } from 'vitest' + +export function createMockClient(): FathomClient { + return { + load: vi.fn(), + trackPageview: vi.fn(), + trackEvent: vi.fn(), + trackGoal: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } +} + +export function createWrapper(client: FathomClient) { + return function Wrapper({ children }: { children: ReactNode }) { + return ( + + {children} + + ) + } +} +``` + +### Usage with renderHook + +```tsx +import { renderHook } from '@testing-library/react' +import { useFathom } from 'react-fathom' +import { createMockClient, createWrapper } from './test-utils' + +describe('useFathom', () => { + it('provides tracking methods', () => { + const mockClient = createMockClient() + + const { result } = renderHook(() => useFathom(), { + wrapper: createWrapper(mockClient), + }) + + result.current.trackEvent('test-event') + + expect(mockClient.trackEvent).toHaveBeenCalledWith('test-event', {}) + }) +}) +``` + +## Testing Default Options + +```tsx +it('merges default options with provided options', () => { + const mockClient = createMockClient() + + render( + + + + ) + + fireEvent.click(screen.getByRole('button')) + + expect(mockClient.trackEvent).toHaveBeenCalledWith('click', { + _site_id: 'test-app', + _value: 100, + }) +}) +``` + +## Testing TrackVisible + +For components that track on visibility, you'll need to mock IntersectionObserver: + +```tsx +import { vi } from 'vitest' + +// Mock IntersectionObserver +const mockIntersectionObserver = vi.fn() +mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, +}) +window.IntersectionObserver = mockIntersectionObserver + +describe('TrackVisible', () => { + it('observes the element', () => { + const mockClient = createMockClient() + + render( + + +
Content
+
+
+ ) + + expect(mockIntersectionObserver).toHaveBeenCalled() + }) +}) +``` + +## Snapshot Testing + +Don't snapshot test analytics calls—they're implementation details. Instead, test that: + +1. The correct events are fired +2. Events include the expected data +3. Events fire at the right times (mount, click, visibility) diff --git a/docs/content/index.mdx b/docs/content/index.mdx new file mode 100644 index 0000000..a6b4926 --- /dev/null +++ b/docs/content/index.mdx @@ -0,0 +1,82 @@ +--- +title: "react-fathom" +description: "Privacy-focused analytics for React, Next.js, and React Native." +--- + +# react-fathom + +**Privacy-focused analytics for React, Next.js, and React Native.** + +Easily integrate [Fathom Analytics](https://usefathom.com/ref/EKONBS) into your applications with automatic pageview tracking, custom event tracking, and full TypeScript support. + +## Why Fathom? + +[Fathom Analytics](https://usefathom.com/ref/EKONBS) is a privacy-focused alternative to Google Analytics. Unlike traditional analytics platforms: + +- **No cookies required** — GDPR, CCPA, and PECR compliant out of the box +- **No personal data collection** — Respects user privacy by design +- **No consent banners needed** — Simplified compliance for your websites +- **Fast and lightweight** — Won't slow down your site + +**New to Fathom?** Get a **$10 credit** when you sign up using [this referral link](https://usefathom.com/ref/EKONBS). + +## Why react-fathom? + +The official `fathom-client` works, but react-fathom provides a better developer experience: + +| Problem | react-fathom solution | +|---------|----------------------| +| Web only—no React Native | **Full React Native support** with offline event queuing | +| Next.js App Router requires boilerplate | **`NextFathomProviderApp`** works directly in Server Component layouts | +| Imperative API only | **Hooks** (`useFathom`, `useTrackOnMount`, `useTrackOnVisible`) and **declarative components** (``, ``) | +| No tree-shaking | **Fully tree-shakeable**—bundle only what you use | + +## Features + +- **Zero-config** Fathom Analytics integration for React +- **Tree-shakeable** — Only bundle what you use +- **Automatic pageview tracking** for Next.js (Pages Router & App Router) +- **React Native support** with offline queuing and navigation tracking +- **Full TypeScript** support with type definitions +- **Flexible** — Works with any React app, Next.js, or React Native +- **Lightweight** — Minimal bundle size impact + +## Quick Start + +```bash +npm install react-fathom fathom-client +``` + +```tsx +import { FathomProvider } from 'react-fathom' + +function App() { + return ( + + + + ) +} +``` + +```tsx +import { useFathom } from 'react-fathom' + +function MyComponent() { + const { trackEvent } = useFathom() + + return ( + + ) +} +``` + +That's it! Pageviews are tracked automatically. + +## Platform Guides + +- [React](/docs/react) — Basic React setup with hooks and components +- [Next.js](/docs/nextjs) — App Router and Pages Router integration +- [React Native](/docs/react-native) — Mobile apps with offline queuing diff --git a/docs/content/nextjs/_meta.ts b/docs/content/nextjs/_meta.ts new file mode 100644 index 0000000..c22058c --- /dev/null +++ b/docs/content/nextjs/_meta.ts @@ -0,0 +1,5 @@ +export default { + index: 'Overview', + 'app-router': 'App Router', + 'pages-router': 'Pages Router', +} diff --git a/docs/content/nextjs/app-router.mdx b/docs/content/nextjs/app-router.mdx new file mode 100644 index 0000000..497cc21 --- /dev/null +++ b/docs/content/nextjs/app-router.mdx @@ -0,0 +1,220 @@ +--- +title: App Router +description: Integrating react-fathom with Next.js App Router +--- + +# App Router + +This guide covers integrating react-fathom with Next.js 13.4+ using the App Router (`app/` directory). + +## Recommended Setup + +Use `NextFathomProviderApp` for the simplest setup. It combines the provider and automatic route tracking: + +```tsx filename="app/layout.tsx" +import { NextFathomProviderApp } from 'react-fathom/next' + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + + ) +} +``` + +That's it! Pageviews are now tracked automatically on every route change. + +## How It Works + +`NextFathomProviderApp` is a Client Component (marked with `'use client'`) that: + +1. Wraps your app with `FathomProvider` +2. Includes `NextFathomTrackViewApp` for automatic route tracking +3. Can be used directly in Server Component layouts + +## Configuration Options + +```tsx filename="app/layout.tsx" + + {children} + +``` + +### Props + +| Prop | Type | Description | +|------|------|-------------| +| `siteId` | `string` | Your Fathom site ID | +| `clientOptions` | `LoadOptions` | Options passed to `fathom-client` | +| `defaultPageviewOptions` | `PageViewOptions` | Default options for all pageviews | +| `defaultEventOptions` | `EventOptions` | Default options for all events | +| `disableAutoTrack` | `boolean` | Disable automatic route tracking | +| `children` | `ReactNode` | Your app content | + +## Alternative: Manual Setup + +If you need more control, use `FathomProvider` with `NextFathomTrackViewApp` separately: + +```tsx filename="app/layout.tsx" +import { FathomProvider } from 'react-fathom' +import { NextFathomTrackViewApp } from 'react-fathom/next' + +export default function RootLayout({ children }) { + return ( + + + + + {children} + + + + ) +} +``` + + + Since `FathomProvider` uses React hooks, you may need to wrap it in your own Client Component when using this approach directly in a Server Component layout. + + +## Custom Client Component Wrapper + +For advanced setups, create your own client component: + +```tsx filename="components/AnalyticsProvider.tsx" +'use client' + +import { FathomProvider } from 'react-fathom' +import { NextFathomTrackViewApp } from 'react-fathom/next' + +export function AnalyticsProvider({ children }) { + return ( + + + {children} + + ) +} +``` + +```tsx filename="app/layout.tsx" +import { AnalyticsProvider } from '@/components/AnalyticsProvider' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ) +} +``` + +## Tracking Events + +Use hooks and components in any Client Component: + +```tsx filename="components/SignUpButton.tsx" +'use client' + +import { useFathom } from 'react-fathom' + +export function SignUpButton() { + const { trackEvent } = useFathom() + + return ( + + ) +} +``` + +Or use declarative components: + +```tsx filename="components/CTASection.tsx" +'use client' + +import { TrackClick, TrackVisible } from 'react-fathom' + +export function CTASection() { + return ( + +
+

Ready to get started?

+ + + +
+
+ ) +} +``` + +## Disabling Auto-Tracking + +To handle pageview tracking manually: + +```tsx + + {children} + +``` + +Then track pageviews where needed: + +```tsx +'use client' + +import { useFathom } from 'react-fathom' +import { usePathname } from 'next/navigation' +import { useEffect } from 'react' + +function CustomTracker() { + const { trackPageview } = useFathom() + const pathname = usePathname() + + useEffect(() => { + // Custom logic before tracking + if (pathname !== '/excluded-page') { + trackPageview({ url: pathname }) + } + }, [pathname, trackPageview]) + + return null +} +``` + +## Avoiding Duplicate Pageviews + +If you see duplicate pageviews, you likely have multiple tracking sources: + +```tsx +// WRONG: Both auto tracking AND fathom-client's auto + + {/* NextFathomTrackViewApp tracks pageviews */} + {/* AND clientOptions.auto defaults to true */} + + +// CORRECT: Disable fathom-client's built-in auto tracking + + {children} + +``` diff --git a/docs/content/nextjs/index.mdx b/docs/content/nextjs/index.mdx new file mode 100644 index 0000000..a5c8f3a --- /dev/null +++ b/docs/content/nextjs/index.mdx @@ -0,0 +1,64 @@ +--- +title: Next.js +description: First-class Next.js support with automatic pageview tracking +--- + +# Next.js + +react-fathom provides first-class support for Next.js with automatic pageview tracking on route changes. + +## Which Router Are You Using? + +Next.js has two routing systems. Choose the guide that matches your project: + + + + Next.js 13.4+ with the `app/` directory. Uses Server Components and the new routing system. + + + Traditional Next.js with the `pages/` directory. Works with all Next.js versions. + + + +## Quick Comparison + +| Feature | App Router | Pages Router | +|---------|-----------|--------------| +| Provider | `NextFathomProviderApp` | `FathomProvider` | +| Route tracking | `NextFathomTrackViewApp` (included) | `NextFathomTrackViewPages` | +| Setup location | `app/layout.tsx` | `pages/_app.tsx` | +| Server Components | Yes (provider is a Client Component) | N/A | + +## Import Path + +All Next.js-specific exports come from `react-fathom/next`: + +```tsx +import { + NextFathomProviderApp, + NextFathomTrackViewApp, + NextFathomTrackViewPages, +} from 'react-fathom/next' +``` + +The base `FathomProvider`, hooks, and components come from `react-fathom`: + +```tsx +import { FathomProvider, useFathom, TrackClick } from 'react-fathom' +``` + +## Environment Variables + +Store your site ID in an environment variable for production: + +```bash +NEXT_PUBLIC_FATHOM_SITE_ID=YOUR_SITE_ID +``` + +```tsx + +``` + + + Environment variables must be prefixed with `NEXT_PUBLIC_` to be available on the client side. + diff --git a/docs/content/nextjs/pages-router.mdx b/docs/content/nextjs/pages-router.mdx new file mode 100644 index 0000000..90dabd9 --- /dev/null +++ b/docs/content/nextjs/pages-router.mdx @@ -0,0 +1,208 @@ +--- +title: "Pages Router" +description: "Integrate react-fathom with Next.js Pages Router." +--- + +# Pages Router + +This guide covers integrating react-fathom with Next.js using the Pages Router (`pages/` directory). + +## Setup + +Add the provider and route tracker to your `_app.tsx`: + +```tsx filename="pages/_app.tsx" +import { FathomProvider } from 'react-fathom' +import { NextFathomTrackViewPages } from 'react-fathom/next' + +function MyApp({ Component, pageProps }) { + return ( + + + + + ) +} + +export default MyApp +``` + +Pageviews are now tracked automatically on every route change. + +## Configuration Options + +```tsx filename="pages/_app.tsx" +import { FathomProvider } from 'react-fathom' +import { NextFathomTrackViewPages } from 'react-fathom/next' + +function MyApp({ Component, pageProps }) { + return ( + + + + + ) +} + +export default MyApp +``` + +### FathomProvider Props + +| Prop | Type | Description | +|------|------|-------------| +| `siteId` | `string` | Your Fathom site ID | +| `clientOptions` | `LoadOptions` | Options passed to `fathom-client` | +| `defaultPageviewOptions` | `PageViewOptions` | Default options for all pageviews | +| `defaultEventOptions` | `EventOptions` | Default options for all events | + +### NextFathomTrackViewPages Props + +| Prop | Type | Description | +|------|------|-------------| +| `disableAutoTrack` | `boolean` | Disable automatic route tracking | + +## Tracking Events + +Use hooks in any component: + +```tsx filename="components/SignUpButton.tsx" +import { useFathom } from 'react-fathom' + +export function SignUpButton() { + const { trackEvent } = useFathom() + + return ( + + ) +} +``` + +Or use declarative components: + +```tsx filename="components/PricingSection.tsx" +import { TrackClick, TrackVisible } from 'react-fathom' + +export function PricingSection() { + return ( + +
+

Pricing

+ + + +
+
+ ) +} +``` + +## Page-Specific Tracking + +Track custom data on specific pages: + +```tsx filename="pages/product/[id].tsx" +import { useTrackOnMount } from 'react-fathom' +import { useRouter } from 'next/router' + +export default function ProductPage({ product }) { + const router = useRouter() + + useTrackOnMount({ + url: `/product/${router.query.id}`, + referrer: document.referrer, + }) + + return ( +
+

{product.name}

+ {/* ... */} +
+ ) +} +``` + +## Disabling Auto-Tracking + +To handle pageview tracking manually: + +```tsx filename="pages/_app.tsx" + + + + +``` + +Then implement your own tracking logic: + +```tsx filename="components/CustomTracker.tsx" +import { useFathom } from 'react-fathom' +import { useRouter } from 'next/router' +import { useEffect } from 'react' + +export function CustomTracker() { + const { trackPageview } = useFathom() + const router = useRouter() + + useEffect(() => { + const handleRouteChange = (url) => { + // Custom logic before tracking + if (!url.startsWith('/admin')) { + trackPageview({ url }) + } + } + + router.events.on('routeChangeComplete', handleRouteChange) + return () => { + router.events.off('routeChangeComplete', handleRouteChange) + } + }, [router.events, trackPageview]) + + return null +} +``` + +## Avoiding Duplicate Pageviews + +If you see duplicate pageviews, disable `fathom-client`'s built-in auto tracking: + +```tsx + + + + +``` + +## TypeScript + +The Pages Router setup is fully typed: + +```tsx filename="pages/_app.tsx" +import type { AppProps } from 'next/app' +import { FathomProvider } from 'react-fathom' +import { NextFathomTrackViewPages } from 'react-fathom/next' + +function MyApp({ Component, pageProps }: AppProps) { + return ( + + + + + ) +} + +export default MyApp +``` diff --git a/docs/content/react-native/_meta.ts b/docs/content/react-native/_meta.ts new file mode 100644 index 0000000..649ef4e --- /dev/null +++ b/docs/content/react-native/_meta.ts @@ -0,0 +1,6 @@ +export default { + index: 'Overview', + navigation: 'Navigation Tracking', + 'app-state': 'App State Tracking', + advanced: 'Advanced Setup', +} diff --git a/docs/content/react-native/advanced.mdx b/docs/content/react-native/advanced.mdx new file mode 100644 index 0000000..1cb1115 --- /dev/null +++ b/docs/content/react-native/advanced.mdx @@ -0,0 +1,175 @@ +--- +title: Advanced Setup +description: Manual WebView configuration and advanced use cases +--- + +# Advanced Setup + +For advanced use cases, you can manually control the WebView and client. + +## Manual WebView Setup + +If you need full control over the WebView lifecycle: + +```tsx +import { useRef, useMemo, useCallback } from 'react' +import { + FathomWebView, + createWebViewClient, + FathomProvider, + type FathomWebViewRef, +} from 'react-fathom/native' + +function App() { + const webViewRef = useRef(null) + + const client = useMemo( + () => createWebViewClient(() => webViewRef.current, { debug: __DEV__ }), + [] + ) + + const handleReady = useCallback(() => { + client.setWebViewReady() + }, [client]) + + return ( + + + + + ) +} +``` + +## FathomWebView Props + +| Prop | Type | Description | +|------|------|-------------| +| `siteId` | `string` | Your Fathom site ID (required) | +| `loadOptions` | `LoadOptions` | Options passed to `fathom.load()` | +| `scriptDomain` | `string` | Custom domain (default: `cdn.usefathom.com`) | +| `onReady` | `() => void` | Called when script loads | +| `onError` | `(error: string) => void` | Called on error | +| `debug` | `boolean` | Enable debug logging | + +## FathomWebView Ref Methods + +Access these methods via the ref: + +| Method | Description | +|--------|-------------| +| `trackPageview(opts?)` | Track a pageview | +| `trackEvent(name, opts?)` | Track a custom event | +| `trackGoal(code, cents)` | Track a goal conversion | +| `blockTrackingForMe()` | Block tracking for user | +| `enableTrackingForMe()` | Enable tracking for user | +| `isReady()` | Check if WebView is ready | + +## createWebViewClient Options + +```tsx +const client = createWebViewClient(getWebViewRef, { + debug: true, // Enable debug logging + enableQueue: true, // Queue commands before ready (default: true) + maxQueueSize: 100, // Max queued commands (default: 100) +}) +``` + +### Client Methods + +The client includes additional methods for queue management: + +| Method | Description | +|--------|-------------| +| `processQueue()` | Manually process queued commands | +| `getQueueLength()` | Get current queue length | +| `setWebViewReady()` | Signal that WebView is ready | + +## Custom Domains + +If you use [Fathom's custom domains](https://usefathom.com/docs/script/custom-domains): + +```tsx + + + +``` + +Or with manual setup: + +```tsx + +``` + +## Parent Access via clientRef + +Access the client directly from a parent component: + +```tsx +import { useRef } from 'react' +import { NativeFathomProvider, WebViewFathomClient } from 'react-fathom/native' + +function App() { + const clientRef = useRef(null) + + const handleDeepLink = (url: string) => { + // Track from parent before provider is mounted in children + clientRef.current?.trackEvent('deep_link', { _url: url }) + + // Check queue status + console.log('Queued events:', clientRef.current?.getQueueLength()) + } + + return ( + + + + ) +} +``` + +## Debugging + +Enable debug mode to see all tracking activity: + +```tsx + console.log('WebView ready')} + onError={(err) => console.error('WebView error:', err)} +> +``` + +Debug mode logs: +- When events are queued +- When the queue is flushed +- All tracking calls +- Any errors + +## Offline Behavior + +Events are automatically queued when: +- The WebView hasn't loaded yet +- Network is unavailable (events stay in queue) + +The queue: +- Has a max size (default: 100 events) +- Is processed in order when WebView becomes ready +- Persists only in memory (not across app restarts) + + + The queue does not persist across app restarts. Events queued when the app is killed will be lost. + diff --git a/docs/content/react-native/app-state.mdx b/docs/content/react-native/app-state.mdx new file mode 100644 index 0000000..83d4722 --- /dev/null +++ b/docs/content/react-native/app-state.mdx @@ -0,0 +1,119 @@ +--- +title: "App State Tracking" +description: "Track when users foreground or background your React Native app." +--- + +# App State Tracking + +Track when users foreground or background your app. + +## Automatic Tracking + +Enable with the `trackAppState` prop: + +```tsx + + + +``` + +This tracks: +- `app-foreground` when the app becomes active +- `app-background` when the app goes to background + +## Custom Hook + +For more control, use the `useAppStateTracking` hook: + +```tsx +import { useAppStateTracking } from 'react-fathom/native' + +function AppStateTracker() { + useAppStateTracking({ + foregroundEventName: 'app-resumed', + backgroundEventName: 'app-paused', + onStateChange: (state) => { + console.log('App state changed:', state) + }, + }) + + return null +} +``` + +## Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `foregroundEventName` | `string` | `'app-foreground'` | Event name for foreground | +| `backgroundEventName` | `string` | `'app-background'` | Event name for background | +| `eventOptions` | `EventOptions` | `undefined` | Additional options for events | +| `onStateChange` | `(state) => void` | `undefined` | Callback on state change | + +## State Values + +The `onStateChange` callback receives one of: + +| State | Description | +|-------|-------------| +| `'active'` | App is in the foreground and interactive | +| `'background'` | App is in the background | +| `'inactive'` | App is transitioning (iOS only) | + +## Examples + +### Track with value + +```tsx +useAppStateTracking({ + foregroundEventName: 'session-resumed', + eventOptions: { _value: 1 }, +}) +``` + +### Custom logic on state change + +```tsx +useAppStateTracking({ + onStateChange: (state) => { + if (state === 'background') { + // Save draft, pause audio, etc. + saveDraft() + } + if (state === 'active') { + // Refresh data + refetchData() + } + }, +}) +``` + +### Conditional tracking + +```tsx +function ConditionalAppStateTracker({ isLoggedIn }) { + useAppStateTracking({ + foregroundEventName: isLoggedIn ? 'user-resumed' : 'visitor-resumed', + backgroundEventName: isLoggedIn ? 'user-paused' : 'visitor-paused', + }) + + return null +} +``` + +## Combining with Provider + +You can use both the provider's `trackAppState` and the hook together if you want different event names: + +```tsx +// Provider tracks default events + + {/* Hook tracks custom events */} + + +``` + +However, this will fire two events per state change. Usually you want one or the other. diff --git a/docs/content/react-native/index.mdx b/docs/content/react-native/index.mdx new file mode 100644 index 0000000..b520960 --- /dev/null +++ b/docs/content/react-native/index.mdx @@ -0,0 +1,98 @@ +--- +title: React Native +description: Full React Native support with offline event queuing +--- + +# React Native + +react-fathom provides full React Native support with a hidden WebView that loads Fathom's official tracking script, ensuring compatibility with Fathom Analytics. + + + React Native support uses a WebView-based approach. Events are queued until the WebView is ready, then sent automatically. + + +## Installation + +Install react-fathom and the required WebView dependency: + +```bash +npm install react-fathom react-native-webview +``` + +For iOS, install CocoaPods dependencies: + +```bash +cd ios && pod install +``` + +## Basic Setup + +```tsx +import { NativeFathomProvider } from 'react-fathom/native' + +function App() { + return ( + console.log('Fathom ready!')} + onError={(err) => console.error('Fathom error:', err)} + > + + + ) +} +``` + +## How It Works + +The `NativeFathomProvider`: + +1. Renders a hidden WebView (0x0 pixels) that loads the Fathom script +2. Queues all tracking calls until the WebView is ready +3. Automatically flushes the queue when the script loads +4. Provides the same `useFathom` hook API as web + +## Tracking Events + +Use the same hooks as web: + +```tsx +import { useFathom } from 'react-fathom/native' + +function PurchaseButton({ price }) { + const { trackEvent, trackGoal } = useFathom() + + const handlePurchase = () => { + trackEvent('purchase-tap', { _value: price }) + trackGoal('PURCHASE', price) + } + + return ( + + Buy Now + + ) +} +``` + +## Provider Props + +| Prop | Type | Description | +|------|------|-------------| +| `siteId` | `string` | Your Fathom site ID (required) | +| `debug` | `boolean` | Enable debug logging | +| `trackAppState` | `boolean` | Auto-track app foreground/background | +| `loadOptions` | `LoadOptions` | Options passed to `fathom.load()` | +| `scriptDomain` | `string` | Custom domain (default: `cdn.usefathom.com`) | +| `defaultPageviewOptions` | `PageViewOptions` | Default pageview options | +| `defaultEventOptions` | `EventOptions` | Default event options | +| `onReady` | `() => void` | Called when Fathom script loads | +| `onError` | `(error: string) => void` | Called on script load error | +| `clientRef` | `MutableRefObject` | Ref for direct client access | + +## Next Steps + +- [Navigation Tracking](/docs/react-native/navigation) — Track React Navigation screen changes +- [App State Tracking](/docs/react-native/app-state) — Track foreground/background +- [Advanced Setup](/docs/react-native/advanced) — Custom WebView configuration diff --git a/docs/content/react-native/navigation.mdx b/docs/content/react-native/navigation.mdx new file mode 100644 index 0000000..45a60fd --- /dev/null +++ b/docs/content/react-native/navigation.mdx @@ -0,0 +1,166 @@ +--- +title: "Navigation Tracking" +description: "Track React Navigation screen changes as pageviews in React Native." +--- + +# Navigation Tracking + +Track React Navigation screen changes as pageviews. + +## Setup + +```tsx +import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native' +import { NativeFathomProvider, useNavigationTracking } from 'react-fathom/native' + +function App() { + const navigationRef = useNavigationContainerRef() + + return ( + + + + + + + ) +} + +function NavigationTracker({ navigationRef }) { + useNavigationTracking({ + navigationRef, + }) + return null +} +``` + +## Options + +```tsx +useNavigationTracking({ + navigationRef, + + // Transform route names before tracking + transformRouteName: (name) => `/screens/${name.toLowerCase()}`, + + // Filter which routes to track + shouldTrackRoute: (name, params) => !name.startsWith('Modal'), + + // Include route params in tracked URL + includeParams: true, +}) +``` + +### Option Reference + +| Option | Type | Description | +|--------|------|-------------| +| `navigationRef` | `RefObject` | React Navigation container ref (required) | +| `transformRouteName` | `(name: string) => string` | Transform route names before tracking | +| `shouldTrackRoute` | `(name: string, params?: object) => boolean` | Filter which routes to track | +| `includeParams` | `boolean` | Include route params in URL (default: `false`) | + +## Transform Examples + +### Prefix with app name + +```tsx +transformRouteName: (name) => `/myapp/${name}` + +// "Home" → "/myapp/Home" +// "Settings" → "/myapp/Settings" +``` + +### Convert to kebab-case URLs + +```tsx +transformRouteName: (name) => { + return '/' + name + .replace(/([a-z])([A-Z])/g, '$1-$2') + .toLowerCase() +} + +// "UserProfile" → "/user-profile" +// "SettingsScreen" → "/settings-screen" +``` + +### Add hierarchy from nested navigators + +```tsx +transformRouteName: (name, params) => { + const tab = params?.tab || 'main' + return `/${tab}/${name.toLowerCase()}` +} + +// With params { tab: 'profile' } +// "EditProfile" → "/profile/editprofile" +``` + +## Filtering Routes + +### Skip modal screens + +```tsx +shouldTrackRoute: (name) => !name.includes('Modal') +``` + +### Only track specific screens + +```tsx +const trackedScreens = ['Home', 'Profile', 'Settings', 'Product'] + +shouldTrackRoute: (name) => trackedScreens.includes(name) +``` + +### Skip screens with certain params + +```tsx +shouldTrackRoute: (name, params) => { + // Don't track preview mode + if (params?.preview) return false + return true +} +``` + +## Full Example + +```tsx +import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native' +import { createNativeStackNavigator } from '@react-navigation/native-stack' +import { NativeFathomProvider, useNavigationTracking } from 'react-fathom/native' + +const Stack = createNativeStackNavigator() + +function NavigationTracker({ navigationRef }) { + useNavigationTracking({ + navigationRef, + transformRouteName: (name) => `/app/${name.toLowerCase()}`, + shouldTrackRoute: (name) => { + // Skip auth screens and modals + const skipScreens = ['Login', 'Register', 'ForgotPassword'] + return !skipScreens.includes(name) && !name.includes('Modal') + }, + }) + return null +} + +export default function App() { + const navigationRef = useNavigationContainerRef() + + return ( + + + + + + + + + + + ) +} +``` diff --git a/docs/content/react-router/index.mdx b/docs/content/react-router/index.mdx new file mode 100644 index 0000000..6d440db --- /dev/null +++ b/docs/content/react-router/index.mdx @@ -0,0 +1,231 @@ +--- +title: "React Router" +description: "Using react-fathom with React Router and Remix for automatic pageview tracking." +--- + +# React Router + +This guide covers integrating react-fathom with React Router v6+ and Remix applications for automatic pageview tracking on route changes. + +## Installation + +Make sure you have both `react-fathom` and `react-router-dom` installed: + +```bash +npm install react-fathom react-router-dom fathom-client +``` + +## Quick Start + +Import `ReactRouterFathomTrackView` from `react-fathom/react-router` and add it inside your `FathomProvider`: + +```tsx +import { BrowserRouter } from 'react-router-dom' +import { FathomProvider } from 'react-fathom' +import { ReactRouterFathomTrackView } from 'react-fathom/react-router' + +function App() { + return ( + + + + + } /> + } /> + } /> + + + + ) +} +``` + +The component will automatically track: +- Initial pageview on app load +- Subsequent pageviews on every route change + +## Remix Integration + +For Remix applications, add the component to your root layout: + +```tsx +// app/root.tsx +import { Outlet } from '@remix-run/react' +import { FathomProvider } from 'react-fathom' +import { ReactRouterFathomTrackView } from 'react-fathom/react-router' + +export default function App() { + return ( + + + + + + + + + + + + + + ) +} +``` + +## Component Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `disableAutoTrack` | `boolean` | `false` | Disable automatic pageview tracking | +| `includeSearchParams` | `boolean` | `true` | Include query parameters in tracked URL | +| `includeHash` | `boolean` | `false` | Include hash fragment in tracked URL | +| `transformUrl` | `(url: string) => string \| null` | — | Transform URL before tracking | + +## Configuration Options + +### Including/Excluding URL Parts + +By default, search parameters are included but hash fragments are not: + +```tsx +// Track /products?category=shoes (includes search params) + + +// Track /products (excludes search params) + + +// Track /docs#installation (includes hash) + +``` + +### URL Transformation + +Use `transformUrl` to modify URLs before tracking. This is useful for: +- Removing sensitive parameters (tokens, session IDs) +- Normalizing URLs +- Skipping certain routes + +```tsx +// Remove sensitive parameters + { + const urlObj = new URL(url) + urlObj.searchParams.delete('token') + urlObj.searchParams.delete('session') + return urlObj.toString() + }} +/> + +// Normalize dynamic IDs to patterns + { + return url.replace(/\/users\/\d+/, '/users/:id') + }} +/> + +// Skip tracking certain routes (return null) + { + if (url.includes('/admin')) { + return null // Skip tracking admin pages + } + return url + }} +/> +``` + +### Disabling Auto-Tracking + +If you need manual control over pageview tracking: + +```tsx +import { useFathom } from 'react-fathom' +import { ReactRouterFathomTrackView } from 'react-fathom/react-router' + +function App() { + return ( + + {/* Disable auto-tracking, handle manually */} + + ... + + ) +} + +// Manual tracking in a specific component +function SpecialPage() { + const { trackPageview } = useFathom() + + useEffect(() => { + // Custom tracking logic + trackPageview({ url: '/special-page-custom' }) + }, []) + + return
Special Page
+} +``` + +## Combining with Event Tracking + +Use alongside other react-fathom features for comprehensive analytics: + +```tsx +import { useFathom, TrackClick, TrackVisible } from 'react-fathom' +import { ReactRouterFathomTrackView } from 'react-fathom/react-router' + +function App() { + return ( + + + {/* Automatic pageview tracking */} + + + + } /> + } /> + + + + ) +} + +function Pricing() { + const { trackEvent } = useFathom() + + return ( +
+ {/* Track when pricing section becomes visible */} + +

Pricing

+
+ + {/* Track CTA clicks */} + + + +
+ ) +} +``` + +## TypeScript + +The component is fully typed. Import types if needed: + +```tsx +import type { ReactRouterFathomTrackViewProps } from 'react-fathom/react-router' + +const config: ReactRouterFathomTrackViewProps = { + includeSearchParams: true, + includeHash: false, + transformUrl: (url) => url.toLowerCase(), +} +``` + +## Next Steps + +- [React Guide](/docs/react) — Hooks and components for React +- [API Reference: Hooks](/docs/api/hooks) — Full hook documentation +- [API Reference: Components](/docs/api/components) — Full component documentation +- [Testing Guide](/docs/guides/testing) — Mock clients for testing diff --git a/docs/content/react.mdx b/docs/content/react.mdx new file mode 100644 index 0000000..80f5778 --- /dev/null +++ b/docs/content/react.mdx @@ -0,0 +1,222 @@ +--- +title: "React" +description: "Using react-fathom hooks and components in standard React applications." +--- + +# React + +This guide covers using react-fathom in standard React applications. For Next.js or React Native, see the dedicated guides. + +## Setup + +```tsx +import { FathomProvider } from 'react-fathom' + +function App() { + return ( + + + + ) +} +``` + +## Using the Hook + +The `useFathom` hook provides access to all tracking methods: + +```tsx +import { useFathom } from 'react-fathom' + +function MyComponent() { + const { trackPageview, trackEvent, trackGoal } = useFathom() + + const handleSignUp = () => { + trackEvent('signup-click', { _value: 100 }) // Optional: value in cents + } + + const handlePurchase = () => { + trackGoal('GOALCODE', 2999) // $29.99 in cents + } + + return ( + <> + + + + ) +} +``` + +### Available Methods + +| Method | Description | +|--------|-------------| +| `trackPageview(options?)` | Track a pageview | +| `trackEvent(eventName, options?)` | Track a custom event | +| `trackGoal(code, cents)` | Track a goal conversion | +| `load(siteId, options?)` | Load Fathom with a site ID | +| `setSite(siteId)` | Change the site ID | +| `blockTrackingForMe()` | Block tracking for current user | +| `enableTrackingForMe()` | Enable tracking for current user | +| `isTrackingEnabled()` | Check if tracking is enabled | + +## Convenience Hooks + +### useTrackOnMount + +Track a pageview when a component mounts: + +```tsx +import { useTrackOnMount } from 'react-fathom' + +function LandingPage() { + useTrackOnMount({ url: '/landing' }) + + return
Welcome!
+} +``` + +### useTrackOnClick + +Returns a click handler that tracks an event: + +```tsx +import { useTrackOnClick } from 'react-fathom' + +function CTAButton() { + const handleClick = useTrackOnClick({ + eventName: 'cta-click', + _value: 100, // Optional: value in cents + callback: (e) => { + console.log('Tracked!', e) + }, + }) + + return +} +``` + +### useTrackOnVisible + +Returns a ref that tracks an event when the element becomes visible: + +```tsx +import { useTrackOnVisible } from 'react-fathom' + +function HeroSection() { + const ref = useTrackOnVisible({ + eventName: 'hero-viewed', + threshold: 0.5, // 50% visible + callback: (entry) => { + console.log('Hero section visible!', entry) + }, + }) + + return ( +
+

Welcome to Our App

+
+ ) +} +``` + +## Declarative Components + +For a more declarative approach, use tracking components: + +### TrackPageview + +Track a pageview when the component mounts: + +```tsx +import { TrackPageview } from 'react-fathom' + +function Dashboard() { + return ( + +
Dashboard content
+
+ ) +} +``` + +### TrackClick + +Wrap any clickable element to track clicks: + +```tsx +import { TrackClick } from 'react-fathom' + +function SignUpSection() { + return ( + + + + ) +} +``` + +### TrackVisible + +Track when an element becomes visible (using IntersectionObserver): + +```tsx +import { TrackVisible } from 'react-fathom' + +function PricingSection() { + return ( + +
+

Pricing

+ {/* pricing content */} +
+
+ ) +} +``` + +You can customize the wrapper element: + +```tsx + +

Footer content

+
+``` + +## Combining Approaches + +Mix hooks and components based on your needs: + +```tsx +import { useFathom, TrackVisible } from 'react-fathom' + +function ProductPage({ product }) { + const { trackEvent } = useFathom() + + const handleAddToCart = () => { + trackEvent('add-to-cart', { _value: product.price }) + // ... add to cart logic + } + + return ( +
+ +

{product.name}

+

{product.description}

+
+ + +
+ ) +} +``` + +## Next Steps + +- [API Reference: Hooks](/docs/api/hooks) — Full hook options +- [API Reference: Components](/docs/api/components) — Full component props +- [Default Options Guide](/docs/guides/default-options) — Set app-wide defaults +- [Testing Guide](/docs/guides/testing) — Mock clients for testing diff --git a/docs/content/tanstack-router/index.mdx b/docs/content/tanstack-router/index.mdx new file mode 100644 index 0000000..4c503d6 --- /dev/null +++ b/docs/content/tanstack-router/index.mdx @@ -0,0 +1,266 @@ +--- +title: "TanStack Router" +description: "Integrate react-fathom with TanStack Router for automatic pageview tracking." +--- + +# TanStack Router Integration + +This guide covers integrating `react-fathom` with TanStack Router applications for automatic pageview tracking. + +## Installation + +Install the required packages: + +```bash npm2yarn +npm install react-fathom fathom-client @tanstack/react-router +``` + +## Setup + +Add `TanStackRouterFathomTrackView` inside your `FathomProvider`, typically in your root route component. + +### Root Route Setup + +```tsx +// src/routes/__root.tsx +import { createRootRoute, Outlet } from '@tanstack/react-router' +import { FathomProvider } from 'react-fathom' +import { TanStackRouterFathomTrackView } from 'react-fathom/tanstack-router' + +export const Route = createRootRoute({ + component: () => ( + + + + + ), +}) +``` + +### Alternative: App-Level Setup + +If you prefer to keep analytics configuration separate from routes: + +```tsx +// src/App.tsx +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { FathomProvider } from 'react-fathom' +import { TanStackRouterFathomTrackView } from 'react-fathom/tanstack-router' +import { routeTree } from './routeTree.gen' + +const router = createRouter({ routeTree }) + +function InnerApp() { + return ( + <> + + {/* Your app content */} + + ) +} + +export function App() { + return ( + + + + ) +} +``` + +> Note: When using this approach, ensure `TanStackRouterFathomTrackView` is rendered within the router context (inside a route component). + +## TanStackRouterFathomTrackView Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `disableAutoTrack` | `boolean` | `false` | Disable automatic pageview tracking | +| `includeSearchParams` | `boolean` | `true` | Include query parameters in tracked URLs | +| `includeHash` | `boolean` | `false` | Include URL hash in tracked URLs | +| `transformUrl` | `(url: string) => string \| null` | — | Transform URL before tracking; return `null` to skip | + +## Tracking Custom Events + +Use the `useFathom` hook in any component: + +```tsx +import { useFathom } from 'react-fathom' + +function CheckoutButton() { + const { trackEvent } = useFathom() + + const handleClick = () => { + trackEvent('checkout-started') + // Navigate to checkout... + } + + return +} +``` + +## URL Transformation + +### Sanitizing Dynamic Routes + +Remove dynamic segments from URLs for cleaner analytics: + +```tsx + { + // /users/123 → /users/$userId + return url.replace(/\/users\/\d+/, '/users/$userId') + }} +/> +``` + +### Excluding Routes + +Skip tracking for specific routes: + +```tsx + { + // Don't track admin pages + if (url.includes('/admin')) { + return null + } + return url + }} +/> +``` + +### Stripping Sensitive Parameters + +Remove sensitive data from tracked URLs: + +```tsx + { + const urlObj = new URL(url) + urlObj.searchParams.delete('token') + urlObj.searchParams.delete('session') + return urlObj.toString() + }} +/> +``` + +## Environment Variables + +Store your site ID in environment variables: + +```bash +# .env +VITE_FATHOM_SITE_ID=YOUR_SITE_ID +``` + +```tsx + +``` + +## Local Development + +Enable tracking on localhost: + +```tsx + +``` + +## TypeScript Support + +All TanStack Router exports are fully typed. Import types as needed: + +```tsx +import type { TanStackRouterFathomTrackViewProps } from 'react-fathom/tanstack-router' +``` + +## How It Works + +TanStack Router provides location state through the `useRouterState` hook. The `TanStackRouterFathomTrackView` component: + +1. Uses `useRouterState` to watch for location changes +2. On mount, tracks the initial pageview +3. When `pathname`, `searchStr`, or `hash` change, tracks a new pageview +4. Constructs the full URL from `window.location.origin` and the route path + +## File-Based Routing Example + +If you're using TanStack Router's file-based routing: + +```tsx +// src/routes/__root.tsx +import { createRootRoute, Outlet } from '@tanstack/react-router' +import { FathomProvider } from 'react-fathom' +import { TanStackRouterFathomTrackView } from 'react-fathom/tanstack-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + { + // Normalize user profile URLs + return url.replace(/\/users\/[^/]+$/, '/users/$userId') + }} + /> +
...
+
+ +
+
...
+
+ ) +} +``` + +```tsx +// src/routes/index.tsx +import { createFileRoute } from '@tanstack/react-router' +import { useFathom } from 'react-fathom' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + const { trackEvent } = useFathom() + + return ( +
+

Welcome

+ +
+ ) +} +``` + +## Troubleshooting + +### Events not appearing in Fathom? + +1. Verify your site ID matches your [Fathom dashboard](https://app.usefathom.com) +2. Check for ad blockers (test in incognito mode) +3. Add `localhost` to `includedDomains` for local testing +4. Ensure `TanStackRouterFathomTrackView` is within both `FathomProvider` and router context + +### Route changes not tracking? + +Ensure `TanStackRouterFathomTrackView` is rendered inside `FathomProvider` and within the TanStack Router context. The component must be inside a route component to access the router state. + +### Search params not being tracked? + +TanStack Router uses `searchStr` for the serialized search string. This is handled automatically by the component. If you have custom search param serialization, make sure it's producing the expected format. diff --git a/docs/content/troubleshooting.mdx b/docs/content/troubleshooting.mdx new file mode 100644 index 0000000..591979c --- /dev/null +++ b/docs/content/troubleshooting.mdx @@ -0,0 +1,230 @@ +--- +title: "Troubleshooting" +description: "Common issues and solutions for react-fathom." +--- + +# Troubleshooting + +Common issues and solutions. + +## Events Not Appearing + +### 1. Verify Your Site ID + +Your site ID should match exactly what's in your [Fathom dashboard](https://app.usefathom.com). It's an 8-character alphanumeric string like `ABCD1234`. + +```tsx +// Double-check this value + +``` + +### 2. Check for Ad Blockers + +Many ad blockers and privacy extensions block analytics scripts. To test: + +- Open an incognito/private window with extensions disabled +- Or temporarily whitelist your development domain + +Consider using [custom domains](/docs/guides/custom-domains) to improve tracking reliability. + +### 3. Domain Restrictions + +Fathom only tracks events from configured domains. For local development: + +```tsx + +``` + +### 4. Inspect Network Requests + +Open your browser's Network tab and look for requests to `cdn.usefathom.com`: + +| Symptom | Cause | +|---------|-------| +| No requests | Script isn't loading—check provider setup | +| Blocked requests | Ad blocker interference | +| Failed requests | Check site ID and domain configuration | + +--- + +## Duplicate Pageviews + +If you see double pageviews, you have multiple tracking sources: + +```tsx +// WRONG: NextFathomTrackViewApp tracks AND clientOptions.auto defaults to true + + + + +// CORRECT: Disable fathom-client's built-in auto tracking + + + +``` + +Same applies to `NextFathomProviderApp`: + +```tsx + + {children} + +``` + +--- + +## Next.js Issues + +### "use client" Errors + +Server Components can't use hooks directly. Use the pre-configured client component: + +```tsx +// app/layout.tsx +import { NextFathomProviderApp } from 'react-fathom/next' + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + + ) +} +``` + +Or create your own client component wrapper: + +```tsx +// components/AnalyticsProvider.tsx +'use client' +import { FathomProvider } from 'react-fathom' + +export function AnalyticsProvider({ children }) { + return ( + + {children} + + ) +} +``` + +### Environment Variables Not Loading + +Ensure your variable is prefixed with `NEXT_PUBLIC_`: + +```bash +# .env.local +NEXT_PUBLIC_FATHOM_SITE_ID=YOUR_SITE_ID # ✓ Works client-side +FATHOM_SITE_ID=YOUR_SITE_ID # ✗ Server-only +``` + +--- + +## React Native Issues + +### Events Not Sending + +#### 1. Verify react-native-webview is installed + +```bash +npm install react-native-webview + +# For iOS +cd ios && pod install +``` + +#### 2. Check WebView is ready + +Events are queued until the WebView loads: + +```tsx + console.log('Fathom WebView ready!')} + onError={(err) => console.error('Fathom error:', err)} +> +``` + +#### 3. Verify network connectivity + +The WebView needs network access to load the Fathom script from `cdn.usefathom.com` (or your custom domain). + +#### 4. Check for WebView restrictions + +Some enterprise MDM solutions or app configurations block WebViews from loading external scripts. + +### Queue Not Flushing + +If events stay queued: + +1. Check `onReady` is being called +2. Verify no `onError` is firing +3. Use `debug={true}` to see queue activity + +```tsx +const clientRef = useRef(null) + + + {/* Check queue length */} + {console.log('Queue:', clientRef.current?.getQueueLength())} + +``` + +--- + +## Debugging Tips + +### Enable Debug Logging + +For React Native: + +```tsx + +``` + +### Use a Console Client + +Replace the real client with one that logs everything: + +```tsx +const debugClient = { + load: (id, opts) => console.log('load:', id, opts), + trackPageview: (opts) => console.log('pageview:', opts), + trackEvent: (name, opts) => console.log('event:', name, opts), + trackGoal: (code, cents) => console.log('goal:', code, cents), + setSite: (id) => console.log('setSite:', id), + blockTrackingForMe: () => console.log('blocked'), + enableTrackingForMe: () => console.log('enabled'), + isTrackingEnabled: () => true, +} + + +``` + +### Verify Real-time in Dashboard + +Fathom's dashboard updates in real-time. Open it alongside your app to see events as they're tracked. + +--- + +## Getting Help + +- [Open an issue](https://github.com/ryanhefner/react-fathom/issues) on GitHub +- [Search existing issues](https://github.com/ryanhefner/react-fathom/issues?q=is%3Aissue) for solutions +- [Fathom Analytics docs](https://usefathom.com/docs) for platform-specific questions diff --git a/docs/lib/changelog.ts b/docs/lib/changelog.ts new file mode 100644 index 0000000..ecd7868 --- /dev/null +++ b/docs/lib/changelog.ts @@ -0,0 +1,74 @@ +import { execSync } from 'child_process' + +export interface ChangelogEntry { + version: string + date: string + commits: { + hash: string + message: string + author: string + }[] +} + +export function getChangelog(): ChangelogEntry[] { + try { + // Get all tags sorted by version + const tagsOutput = execSync('git tag --sort=-version:refname', { + encoding: 'utf-8', + cwd: process.cwd(), + }).trim() + + if (!tagsOutput) { + return [] + } + + const tags = tagsOutput.split('\n').filter(Boolean) + const changelog: ChangelogEntry[] = [] + + for (let i = 0; i < tags.length; i++) { + const tag = tags[i] + const prevTag = tags[i + 1] + + // Get tag date + const dateOutput = execSync(`git log -1 --format=%cI ${tag}`, { + encoding: 'utf-8', + cwd: process.cwd(), + }).trim() + + // Get commits between this tag and the previous one + const range = prevTag ? `${prevTag}..${tag}` : tag + const commitsOutput = execSync( + `git log ${range} --format="%H|%s|%an" --no-merges`, + { encoding: 'utf-8', cwd: process.cwd() } + ).trim() + + const commits = commitsOutput + .split('\n') + .filter(Boolean) + .map((line) => { + const [hash, message, author] = line.split('|') + return { hash: hash.slice(0, 7), message, author } + }) + + changelog.push({ + version: tag, + date: dateOutput, + commits, + }) + } + + return changelog + } catch (error) { + console.error('Failed to get changelog:', error) + return [] + } +} + +export function formatChangelogDate(dateString: string): string { + const date = new Date(dateString) + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) +} diff --git a/docs/lib/docs.ts b/docs/lib/docs.ts new file mode 100644 index 0000000..03c7e99 --- /dev/null +++ b/docs/lib/docs.ts @@ -0,0 +1,364 @@ +import fs from 'fs' +import path from 'path' +import { execSync } from 'child_process' +import matter from 'gray-matter' + +export interface NavItem { + title: string + href?: string + children?: NavItem[] +} + +export interface TOCItem { + id: string + title: string + level: number + children?: TOCItem[] +} + +export interface Frontmatter { + title?: string + description?: string + [key: string]: unknown +} + +export interface AdjacentPages { + prev?: { title: string; href: string } + next?: { title: string; href: string } +} + +export interface BreadcrumbItem { + title: string + href?: string +} + +export interface DocPage { + content: string + frontmatter: Frontmatter + slug: string[] +} + +// Meta file structure: { [filename]: title | { title, ... } } +type MetaValue = string | { title: string; [key: string]: unknown } +type MetaFile = Record + +const CONTENT_DIR = path.join(process.cwd(), 'content') +const DOCS_BASE_PATH = '/docs' + +function loadMeta(dir: string): MetaFile | null { + const metaPath = path.join(dir, '_meta.ts') + const metaJsonPath = path.join(dir, '_meta.json') + + // Try _meta.ts first (as exported default) + if (fs.existsSync(metaPath)) { + try { + // Read the file and extract the default export + const content = fs.readFileSync(metaPath, 'utf-8') + // Simple parsing for `export default { ... }` + const match = content.match(/export\s+default\s+(\{[\s\S]*\})/) + if (match) { + // Use Function constructor to evaluate (safe for static config) + const fn = new Function(`return ${match[1]}`) + return fn() as MetaFile + } + } catch { + // Fall through to JSON + } + } + + // Try _meta.json + if (fs.existsSync(metaJsonPath)) { + try { + return JSON.parse(fs.readFileSync(metaJsonPath, 'utf-8')) as MetaFile + } catch { + return null + } + } + + return null +} + +function getTitleFromMeta(meta: MetaFile | null, key: string): string | null { + if (!meta || !(key in meta)) return null + const value = meta[key] + return typeof value === 'string' ? value : value.title +} + +function getTitleFromFrontmatter(filePath: string): string | null { + try { + const content = fs.readFileSync(filePath, 'utf-8') + const { data } = matter(content) + return data.title || null + } catch { + return null + } +} + +function formatTitle(name: string): string { + return name + .replace(/-/g, ' ') + .replace(/\b\w/g, (c) => c.toUpperCase()) +} + +function buildNavFromDir(dir: string, basePath: string = ''): NavItem[] { + const items: NavItem[] = [] + const meta = loadMeta(dir) + + // Get all files and directories + const entries = fs.readdirSync(dir, { withFileTypes: true }) + + // Separate files and directories + const files: string[] = [] + const dirs: string[] = [] + + for (const entry of entries) { + if (entry.name.startsWith('_')) continue // Skip meta files + if (entry.isDirectory()) { + dirs.push(entry.name) + } else if (entry.name.endsWith('.mdx')) { + files.push(entry.name.replace('.mdx', '')) + } + } + + // Get order from meta file, or use alphabetical + const allKeys = [...new Set([...Object.keys(meta || {}), ...files, ...dirs])] + const orderedKeys = meta ? Object.keys(meta) : allKeys.sort() + + // Add remaining items not in meta + for (const key of allKeys) { + if (!orderedKeys.includes(key)) { + orderedKeys.push(key) + } + } + + for (const key of orderedKeys) { + const dirPath = path.join(dir, key) + const filePath = path.join(dir, `${key}.mdx`) + const isDir = dirs.includes(key) + const isFile = files.includes(key) + + if (isDir) { + // It's a directory - check for index.mdx + const indexPath = path.join(dirPath, 'index.mdx') + const hasIndex = fs.existsSync(indexPath) + const children = buildNavFromDir(dirPath, `${basePath}/${key}`) + + // Get title from meta, index frontmatter, or format from name + const title = getTitleFromMeta(meta, key) + || (hasIndex ? getTitleFromFrontmatter(indexPath) : null) + || formatTitle(key) + + if (hasIndex) { + // Directory with index - link to index, children are sub-pages + items.push({ + title, + href: `${basePath}/${key}`, + children: children.length > 0 ? children : undefined, + }) + } else if (children.length > 0) { + // Directory without index - just a group + items.push({ + title, + children, + }) + } + } else if (isFile && key !== 'index') { + // Regular file (not index) + const title = getTitleFromMeta(meta, key) + || getTitleFromFrontmatter(filePath) + || formatTitle(key) + + items.push({ + title, + href: `${basePath}/${key}`, + }) + } + } + + return items +} + +// Generate navigation from file structure +export function getDocsNav(): NavItem[] { + // Handle root index separately + const indexPath = path.join(CONTENT_DIR, 'index.mdx') + const rootItems: NavItem[] = [] + + if (fs.existsSync(indexPath)) { + const title = getTitleFromFrontmatter(indexPath) || 'Introduction' + rootItems.push({ title, href: DOCS_BASE_PATH }) + } + + // Build from directory structure + const dirItems = buildNavFromDir(CONTENT_DIR, DOCS_BASE_PATH) + + // Combine - put index first, then dir items + return [...rootItems, ...dirItems] +} + +export function getDocBySlug(slug: string[]): DocPage | null { + const slugPath = slug.length === 0 ? 'index' : slug.join('/') + let filePath = path.join(CONTENT_DIR, `${slugPath}.mdx`) + + // Try direct file first + if (!fs.existsSync(filePath)) { + // Try as directory with index.mdx + filePath = path.join(CONTENT_DIR, slugPath, 'index.mdx') + if (!fs.existsSync(filePath)) { + return null + } + } + + const fileContent = fs.readFileSync(filePath, 'utf-8') + const { data, content } = matter(fileContent) + + return { + content, + frontmatter: data as Frontmatter, + slug, + } +} + +export function getAllDocSlugs(): string[][] { + const slugs: string[][] = [] + + function walkDir(dir: string, prefix: string[] = []) { + const files = fs.readdirSync(dir) + + for (const file of files) { + if (file.startsWith('_')) continue // Skip meta files + + const filePath = path.join(dir, file) + const stat = fs.statSync(filePath) + + if (stat.isDirectory()) { + walkDir(filePath, [...prefix, file]) + } else if (file.endsWith('.mdx')) { + const name = file.replace('.mdx', '') + if (name === 'index') { + slugs.push(prefix.length === 0 ? [] : prefix) + } else { + slugs.push([...prefix, name]) + } + } + } + } + + walkDir(CONTENT_DIR) + return slugs +} + +export function extractTOC(content: string): TOCItem[] { + const headingRegex = /^(#{2,3})\s+(.+)$/gm + const toc: TOCItem[] = [] + + let match + while ((match = headingRegex.exec(content)) !== null) { + const level = match[1].length + const title = match[2].trim() + const id = title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, '') + + toc.push({ id, title, level }) + } + + return toc +} + +export function getAdjacentPages(slug: string[]): AdjacentPages { + const nav = getDocsNav() + const flatNav: { title: string; href: string }[] = [] + + function flatten(items: NavItem[]) { + for (const item of items) { + if (item.href) { + flatNav.push({ title: item.title, href: item.href }) + } + if (item.children) { + flatten(item.children) + } + } + } + + flatten(nav) + + const currentHref = slug.length === 0 ? DOCS_BASE_PATH : `${DOCS_BASE_PATH}/${slug.join('/')}` + const currentIndex = flatNav.findIndex((item) => item.href === currentHref) + + return { + prev: currentIndex > 0 ? flatNav[currentIndex - 1] : undefined, + next: currentIndex < flatNav.length - 1 ? flatNav[currentIndex + 1] : undefined, + } +} + +export function getLastUpdated(slug: string[]): string | null { + const slugPath = slug.length === 0 ? 'index' : slug.join('/') + let filePath = path.join(CONTENT_DIR, `${slugPath}.mdx`) + + // Try direct file first + if (!fs.existsSync(filePath)) { + // Try as directory with index.mdx + filePath = path.join(CONTENT_DIR, slugPath, 'index.mdx') + if (!fs.existsSync(filePath)) { + return null + } + } + + try { + // Get the last commit date for this file + const result = execSync( + `git log -1 --format=%cI -- "${filePath}"`, + { encoding: 'utf-8', cwd: process.cwd() } + ).trim() + + if (!result) return null + + return result + } catch { + return null + } +} + +export function getBreadcrumbs(slug: string[]): BreadcrumbItem[] { + if (slug.length === 0) { + return [{ title: 'Docs', href: DOCS_BASE_PATH }] + } + + const nav = getDocsNav() + const breadcrumbs: BreadcrumbItem[] = [{ title: 'Docs', href: DOCS_BASE_PATH }] + + // Find the path through the navigation + function findPath(items: NavItem[], targetPath: string, currentPath: BreadcrumbItem[] = []): BreadcrumbItem[] | null { + for (const item of items) { + if (item.href === targetPath) { + return [...currentPath, { title: item.title, href: item.href }] + } + if (item.children) { + const result = findPath(item.children, targetPath, [...currentPath, { title: item.title, href: item.href }]) + if (result) return result + } + } + return null + } + + const targetHref = `${DOCS_BASE_PATH}/${slug.join('/')}` + const foundPath = findPath(nav, targetHref) + + if (foundPath) { + return [...breadcrumbs, ...foundPath] + } + + // Fallback: build breadcrumbs from slug segments + let href = DOCS_BASE_PATH + for (const segment of slug) { + href += `/${segment}` + breadcrumbs.push({ + title: segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' '), + href: href, + }) + } + + return breadcrumbs +} diff --git a/docs/lib/fathom-next.ts b/docs/lib/fathom-next.ts new file mode 100644 index 0000000..7a906ed --- /dev/null +++ b/docs/lib/fathom-next.ts @@ -0,0 +1,2 @@ +// Re-export from source files to ensure single context instance +export * from '../../src/next/index' diff --git a/docs/lib/fathom.ts b/docs/lib/fathom.ts new file mode 100644 index 0000000..4e432e6 --- /dev/null +++ b/docs/lib/fathom.ts @@ -0,0 +1,7 @@ +// Re-export from source files to ensure single context instance +// This avoids module resolution issues with linked packages + +export * from '../../src/index' +export { useFathom } from '../../src/hooks/useFathom' +export { useDebugSubscription } from '../../src/hooks/useDebugSubscription' +export type { DebugEvent } from '../../src/types' diff --git a/docs/next.config.mjs b/docs/next.config.mjs new file mode 100644 index 0000000..a0761aa --- /dev/null +++ b/docs/next.config.mjs @@ -0,0 +1,39 @@ +import path from 'path' +import { fileURLToPath } from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.resolve(__dirname, '..') + +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', + reactStrictMode: true, + pageExtensions: ['js', 'jsx', 'ts', 'tsx'], + // Transpile linked packages to ensure context works correctly + transpilePackages: ['react-fathom'], + typescript: { + // Allow build to succeed even with TypeScript errors + ignoreBuildErrors: false, + }, + // Turbopack configuration (Next.js 16+ default) + turbopack: { + root: rootDir, + resolveAlias: { + 'react-fathom': '../dist/es/index.js', + 'react-fathom/next': '../dist/es/next/index.js', + 'react-fathom/debug': '../dist/es/debug/index.js', + }, + }, + // Webpack fallback for production builds + webpack: (config) => { + config.resolve.alias = { + ...config.resolve.alias, + 'react-fathom': path.resolve(__dirname, '../dist/es'), + 'react-fathom/next': path.resolve(__dirname, '../dist/es/next'), + 'react-fathom/debug': path.resolve(__dirname, '../dist/es/debug'), + } + return config + }, +} + +export default nextConfig diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..29bed3d --- /dev/null +++ b/docs/package.json @@ -0,0 +1,31 @@ +{ + "name": "react-fathom-docs", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "postbuild": "pagefind --site .next/server/app --output-path public/pagefind", + "start": "next start" + }, + "dependencies": { + "@chakra-ui/react": "^3.2.0", + "@emotion/react": "^11.13.0", + "fathom-client": "^3.6.0", + "gray-matter": "^4.0.3", + "next": "^16.1.4", + "next-mdx-remote": "^5.0.0", + "next-themes": "^0.4.0", + "pagefind": "^1.3.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-fathom": "file:..", + "reading-time": "^1.5.0", + "rehype-pretty-code": "^0.14.0", + "shiki": "^3.21.0" + }, + "devDependencies": { + "@types/node": "^25.0.10", + "typescript": "^5.7.0" + } +} diff --git a/docs/public/logos/gatsby.svg b/docs/public/logos/gatsby.svg new file mode 100644 index 0000000..9fe8b7b --- /dev/null +++ b/docs/public/logos/gatsby.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/public/logos/nextjs.svg b/docs/public/logos/nextjs.svg new file mode 100644 index 0000000..69ae296 --- /dev/null +++ b/docs/public/logos/nextjs.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/docs/public/logos/react-native.svg b/docs/public/logos/react-native.svg new file mode 100644 index 0000000..1ab815a --- /dev/null +++ b/docs/public/logos/react-native.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/public/logos/react-router.svg b/docs/public/logos/react-router.svg new file mode 100644 index 0000000..1e5c93e --- /dev/null +++ b/docs/public/logos/react-router.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs/public/logos/react.svg b/docs/public/logos/react.svg new file mode 100644 index 0000000..1ab815a --- /dev/null +++ b/docs/public/logos/react.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/public/logos/tanstack.svg b/docs/public/logos/tanstack.svg new file mode 100644 index 0000000..0891f51 --- /dev/null +++ b/docs/public/logos/tanstack.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/docs/public/og-image.svg b/docs/public/og-image.svg new file mode 100644 index 0000000..322e6f1 --- /dev/null +++ b/docs/public/og-image.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + react-fathom + + + + + Privacy-focused analytics for React + + + + + + React + + + Next.js + + + React Router + + + Gatsby + + + TanStack + + + React Native + + diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000..ff87ced --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/docs/vercel.json b/docs/vercel.json new file mode 100644 index 0000000..84a2d9b --- /dev/null +++ b/docs/vercel.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "buildCommand": "cd .. && npm run build && cd docs && npm run build", + "installCommand": "cd .. && npm install && cd docs && npm install", + "outputDirectory": "out" +} diff --git a/docs/yarn.lock b/docs/yarn.lock new file mode 100644 index 0000000..d462200 --- /dev/null +++ b/docs/yarn.lock @@ -0,0 +1,3027 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ark-ui/react@^5.29.1": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@ark-ui/react/-/react-5.30.0.tgz#57f82e5f91e7f3af6adc1b470b8deda322d135af" + integrity sha512-MIWgj6uWTuG42DGaXUQARObvuQJymm+/1wsdGEDrIHtSv0a2PFQO4svwMvMFwfFbL1jVkJzzBU6JDAH0xKbvyw== + dependencies: + "@internationalized/date" "3.10.0" + "@zag-js/accordion" "1.31.1" + "@zag-js/anatomy" "1.31.1" + "@zag-js/angle-slider" "1.31.1" + "@zag-js/async-list" "1.31.1" + "@zag-js/auto-resize" "1.31.1" + "@zag-js/avatar" "1.31.1" + "@zag-js/bottom-sheet" "1.31.1" + "@zag-js/carousel" "1.31.1" + "@zag-js/checkbox" "1.31.1" + "@zag-js/clipboard" "1.31.1" + "@zag-js/collapsible" "1.31.1" + "@zag-js/collection" "1.31.1" + "@zag-js/color-picker" "1.31.1" + "@zag-js/color-utils" "1.31.1" + "@zag-js/combobox" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/date-picker" "1.31.1" + "@zag-js/date-utils" "1.31.1" + "@zag-js/dialog" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/editable" "1.31.1" + "@zag-js/file-upload" "1.31.1" + "@zag-js/file-utils" "1.31.1" + "@zag-js/floating-panel" "1.31.1" + "@zag-js/focus-trap" "1.31.1" + "@zag-js/highlight-word" "1.31.1" + "@zag-js/hover-card" "1.31.1" + "@zag-js/i18n-utils" "1.31.1" + "@zag-js/image-cropper" "1.31.1" + "@zag-js/json-tree-utils" "1.31.1" + "@zag-js/listbox" "1.31.1" + "@zag-js/marquee" "1.31.1" + "@zag-js/menu" "1.31.1" + "@zag-js/navigation-menu" "1.31.1" + "@zag-js/number-input" "1.31.1" + "@zag-js/pagination" "1.31.1" + "@zag-js/password-input" "1.31.1" + "@zag-js/pin-input" "1.31.1" + "@zag-js/popover" "1.31.1" + "@zag-js/presence" "1.31.1" + "@zag-js/progress" "1.31.1" + "@zag-js/qr-code" "1.31.1" + "@zag-js/radio-group" "1.31.1" + "@zag-js/rating-group" "1.31.1" + "@zag-js/react" "1.31.1" + "@zag-js/scroll-area" "1.31.1" + "@zag-js/select" "1.31.1" + "@zag-js/signature-pad" "1.31.1" + "@zag-js/slider" "1.31.1" + "@zag-js/splitter" "1.31.1" + "@zag-js/steps" "1.31.1" + "@zag-js/switch" "1.31.1" + "@zag-js/tabs" "1.31.1" + "@zag-js/tags-input" "1.31.1" + "@zag-js/timer" "1.31.1" + "@zag-js/toast" "1.31.1" + "@zag-js/toggle" "1.31.1" + "@zag-js/toggle-group" "1.31.1" + "@zag-js/tooltip" "1.31.1" + "@zag-js/tour" "1.31.1" + "@zag-js/tree-view" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7" + integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/generator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1" + integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw== + dependencies: + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-module-imports@^7.16.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/parser@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" + integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== + dependencies: + "@babel/types" "^7.28.6" + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== + +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e" + integrity sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/generator" "^7.28.6" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.6" + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" + debug "^4.3.1" + +"@babel/types@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" + integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@chakra-ui/react@^3.2.0": + version "3.31.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react/-/react-3.31.0.tgz#8328f6c57c22c2e8b5511927664a306f954b5ca3" + integrity sha512-puvrZOfnfMA+DckDcz0UxO20l7TVhwsdQ9ksCv4nIUB430yuWzon0yo9fM10lEr3hd7BhjZARpMCVw5u280clw== + dependencies: + "@ark-ui/react" "^5.29.1" + "@emotion/is-prop-valid" "^1.4.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + "@pandacss/is-valid-prop" "^1.4.2" + csstype "^3.2.3" + +"@emnapi/runtime@^1.7.0": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.8.1.tgz#550fa7e3c0d49c5fb175a116e8cd70614f9a22a5" + integrity sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg== + dependencies: + tslib "^2.4.0" + +"@emotion/babel-plugin@^11.13.5": + version "11.13.5" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz#eab8d65dbded74e0ecfd28dc218e75607c4e7bc0" + integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.3.3" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.14.0": + version "11.14.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.14.0.tgz#ee44b26986eeb93c8be82bb92f1f7a9b21b2ed76" + integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@emotion/is-prop-valid@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz#e9ad47adff0b5c94c72db3669ce46de33edf28c0" + integrity sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw== + dependencies: + "@emotion/memoize" "^0.9.0" + +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.13.0": + version "11.14.0" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.14.0.tgz#cfaae35ebc67dd9ef4ea2e9acc6cd29e157dd05d" + integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/cache" "^11.14.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.3.tgz#d291531005f17d704d0463a032fe679f376509e8" + integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== + dependencies: + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.2" + csstype "^3.0.2" + +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== + +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== + +"@emotion/use-insertion-effect-with-fallbacks@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz#8a8cb77b590e09affb960f4ff1e9a89e532738bf" + integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== + +"@emotion/utils@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.2.tgz#6df6c45881fcb1c412d6688a311a98b7f59c1b52" + integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== + +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== + +"@floating-ui/core@^1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7" + integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w== + dependencies: + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/dom@1.7.4": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77" + integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA== + dependencies: + "@floating-ui/core" "^1.7.3" + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/utils@^0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" + integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== + +"@img/colour@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311" + integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw== + +"@img/sharp-darwin-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86" + integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.2.4" + +"@img/sharp-darwin-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b" + integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.2.4" + +"@img/sharp-libvips-darwin-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43" + integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g== + +"@img/sharp-libvips-darwin-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc" + integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg== + +"@img/sharp-libvips-linux-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318" + integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw== + +"@img/sharp-libvips-linux-arm@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d" + integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A== + +"@img/sharp-libvips-linux-ppc64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7" + integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA== + +"@img/sharp-libvips-linux-riscv64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de" + integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA== + +"@img/sharp-libvips-linux-s390x@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec" + integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ== + +"@img/sharp-libvips-linux-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce" + integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw== + +"@img/sharp-libvips-linuxmusl-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06" + integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw== + +"@img/sharp-libvips-linuxmusl-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75" + integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg== + +"@img/sharp-linux-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc" + integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.2.4" + +"@img/sharp-linux-arm@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d" + integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.2.4" + +"@img/sharp-linux-ppc64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813" + integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA== + optionalDependencies: + "@img/sharp-libvips-linux-ppc64" "1.2.4" + +"@img/sharp-linux-riscv64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60" + integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw== + optionalDependencies: + "@img/sharp-libvips-linux-riscv64" "1.2.4" + +"@img/sharp-linux-s390x@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7" + integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.2.4" + +"@img/sharp-linux-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8" + integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.2.4" + +"@img/sharp-linuxmusl-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086" + integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + +"@img/sharp-linuxmusl-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f" + integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + +"@img/sharp-wasm32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0" + integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw== + dependencies: + "@emnapi/runtime" "^1.7.0" + +"@img/sharp-win32-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a" + integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g== + +"@img/sharp-win32-ia32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de" + integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg== + +"@img/sharp-win32-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8" + integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw== + +"@internationalized/date@3.10.0": + version "3.10.0" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.10.0.tgz#056db64a4facdf48c6937ad498a882a8151d640a" + integrity sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw== + dependencies: + "@swc/helpers" "^0.5.0" + +"@internationalized/number@3.6.5": + version "3.6.5" + resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.6.5.tgz#1103f2832ca8d9dd3e4eecf95733d497791dbbbe" + integrity sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g== + dependencies: + "@swc/helpers" "^0.5.0" + +"@jridgewell/gen-mapping@^0.3.12": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@mdx-js/mdx@^3.0.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-3.1.1.tgz#c5ffd991a7536b149e17175eee57a1a2a511c6d1" + integrity sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdx" "^2.0.0" + acorn "^8.0.0" + collapse-white-space "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + estree-util-scope "^1.0.0" + estree-walker "^3.0.0" + hast-util-to-jsx-runtime "^2.0.0" + markdown-extensions "^2.0.0" + recma-build-jsx "^1.0.0" + recma-jsx "^1.0.0" + recma-stringify "^1.0.0" + rehype-recma "^1.0.0" + remark-mdx "^3.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + source-map "^0.7.0" + unified "^11.0.0" + unist-util-position-from-estree "^2.0.0" + unist-util-stringify-position "^4.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +"@mdx-js/react@^3.0.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.1.1.tgz#24bda7fffceb2fe256f954482123cda1be5f5fef" + integrity sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw== + dependencies: + "@types/mdx" "^2.0.0" + +"@next/env@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-16.1.4.tgz#1f5155b16bad9825432b5e398b83df687b7b86f9" + integrity sha512-gkrXnZyxPUy0Gg6SrPQPccbNVLSP3vmW8LU5dwEttEEC1RwDivk8w4O+sZIjFvPrSICXyhQDCG+y3VmjlJf+9A== + +"@next/swc-darwin-arm64@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.4.tgz#2d5ee68da80c9b822edd06caa360aef1917d0f37" + integrity sha512-T8atLKuvk13XQUdVLCv1ZzMPgLPW0+DWWbHSQXs0/3TjPrKNxTmUIhOEaoEyl3Z82k8h/gEtqyuoZGv6+Ugawg== + +"@next/swc-darwin-x64@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.4.tgz#2f8d4462f48d4cb3c927de1962ca7a7b2f8a5b03" + integrity sha512-AKC/qVjUGUQDSPI6gESTx0xOnOPQ5gttogNS3o6bA83yiaSZJek0Am5yXy82F1KcZCx3DdOwdGPZpQCluonuxg== + +"@next/swc-linux-arm64-gnu@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.4.tgz#79fecac25ad4a0ee1081110f4c8863b87e754943" + integrity sha512-POQ65+pnYOkZNdngWfMEt7r53bzWiKkVNbjpmCt1Zb3V6lxJNXSsjwRuTQ8P/kguxDC8LRkqaL3vvsFrce4dMQ== + +"@next/swc-linux-arm64-musl@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.4.tgz#e9a99b1ea9a68908c3d36a847a6fe367b4fc3855" + integrity sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q== + +"@next/swc-linux-x64-gnu@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.4.tgz#4804de5f42ac8333e0049ab538473cbd996507f6" + integrity sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ== + +"@next/swc-linux-x64-musl@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.4.tgz#4aa01e59b0e0fd19ab493ee239e3904c42419ca6" + integrity sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw== + +"@next/swc-win32-arm64-msvc@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.4.tgz#67652a5c57889f44c11e145d49f777ac2e6cde58" + integrity sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw== + +"@next/swc-win32-x64-msvc@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.4.tgz#3c51597eb64a96b8fcade74ab3f21ef3ad278a33" + integrity sha512-JSVlm9MDhmTXw/sO2PE/MRj+G6XOSMZB+BcZ0a7d6KwVFZVpkHcb2okyoYFBaco6LeiL53BBklRlOrDDbOeE5w== + +"@pagefind/darwin-arm64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/darwin-arm64/-/darwin-arm64-1.4.0.tgz#0315030e6a89bec3121273b1851f7aadf0b12425" + integrity sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ== + +"@pagefind/darwin-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/darwin-x64/-/darwin-x64-1.4.0.tgz#671e1fe0f0733350a3eb244ace2675166186793e" + integrity sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A== + +"@pagefind/freebsd-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/freebsd-x64/-/freebsd-x64-1.4.0.tgz#3419701ce810e7ec050bbf4786b1c3bee78ec51b" + integrity sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q== + +"@pagefind/linux-arm64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/linux-arm64/-/linux-arm64-1.4.0.tgz#ba2a5c8d10d5273fe61a8d230b546b08d22cb676" + integrity sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw== + +"@pagefind/linux-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/linux-x64/-/linux-x64-1.4.0.tgz#1e56bb3c91fd0128be84e98897c9785c489fbbb7" + integrity sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg== + +"@pagefind/windows-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/windows-x64/-/windows-x64-1.4.0.tgz#ba68fd609621132e8e314a89e2d2d52516f61723" + integrity sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g== + +"@pandacss/is-valid-prop@^1.4.2": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@pandacss/is-valid-prop/-/is-valid-prop-1.8.1.tgz#8d23dd897a006c21f5ce0bcb66759b1b625d3b9a" + integrity sha512-gf2HTBCOboc65Jlb9swAjbffXSIv+A4vzSQ9iHyTCDLMcXTHYjPOQNliI36WkuQgR0pNXggBbQXGNaT9wKcrAw== + +"@shikijs/core@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-3.21.0.tgz#9641d09865c43612b28e7931f9af68c8a62edd90" + integrity sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA== + dependencies: + "@shikijs/types" "3.21.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + hast-util-to-html "^9.0.5" + +"@shikijs/engine-javascript@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz#f04554fe87bed39d00ba4b140894b41cd207f5cb" + integrity sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ== + dependencies: + "@shikijs/types" "3.21.0" + "@shikijs/vscode-textmate" "^10.0.2" + oniguruma-to-es "^4.3.4" + +"@shikijs/engine-oniguruma@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz#0e666454a03fd85d6c634d9dbe70a63f007a6323" + integrity sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ== + dependencies: + "@shikijs/types" "3.21.0" + "@shikijs/vscode-textmate" "^10.0.2" + +"@shikijs/langs@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.21.0.tgz#da33400a85c7cba75fc9f4a6b9feb69a6c39c800" + integrity sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA== + dependencies: + "@shikijs/types" "3.21.0" + +"@shikijs/themes@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.21.0.tgz#1955d642ea37d70d1137e6cf47da7dc9c34ff4c0" + integrity sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw== + dependencies: + "@shikijs/types" "3.21.0" + +"@shikijs/types@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.21.0.tgz#510d6ddbea65add27980a6ca36cc7bdabc7afe90" + integrity sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA== + dependencies: + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224" + integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== + +"@swc/helpers@0.5.15": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" + integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== + dependencies: + tslib "^2.8.0" + +"@swc/helpers@^0.5.0": + version "0.5.18" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.18.tgz#feeeabea0d10106ee25aaf900165df911ab6d3b1" + integrity sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ== + dependencies: + tslib "^2.8.0" + +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/hast@^3.0.0", "@types/hast@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + +"@types/mdx@^2.0.0": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd" + integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/node@^25.0.10": + version "25.0.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7" + integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg== + dependencies: + undici-types "~7.16.0" + +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + +"@ungap/structured-clone@^1.0.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +"@zag-js/accordion@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/accordion/-/accordion-1.31.1.tgz#ea16303036696bb49cf528b131e4bbe3c54437fc" + integrity sha512-3sGi4EZpGBz/O1IVkk9dzzWzP5vVVOj4Li6C+jHOnrgaWPouA/mBTP5L9HEL8qtFsECFZwpNo486eqiCmeHoGw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/anatomy@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/anatomy/-/anatomy-1.31.1.tgz#415564763b6e6b2f77ff3c2dac9c8683531bc5d3" + integrity sha512-BhIhf3Q0tRA0Jugd7AJfUBzeAb/iATBsw7KyYThMGcPWmrWssL7KWr5AB6RufzGKU7+DCb1QEhlqd4NSOJaYxQ== + +"@zag-js/angle-slider@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/angle-slider/-/angle-slider-1.31.1.tgz#fba6c241868ac8de3ee2318e9ac9a893dfb771c5" + integrity sha512-SfWrgnM0zMLX82rsIJOqWk430UnPA17UFGcDqMDRwXy1Wx4yptmx0aFAsSXnRnw4Ee7WaulF2RWBli6O6iYRCA== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/rect-utils" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/aria-hidden@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/aria-hidden/-/aria-hidden-1.31.1.tgz#5dfb1e9702a02f0964684ebace3592e935cf756c" + integrity sha512-SoNt4S2LkHNWPglQczWN0E5vAV15MT1GoK9MksZzbkMhl+pkDTdLytpXsQ1IgalC1YUng0XNps/Wt6P3uDuzTA== + dependencies: + "@zag-js/dom-query" "1.31.1" + +"@zag-js/async-list@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/async-list/-/async-list-1.31.1.tgz#db46419798d856a85a3de9b4907e502ba181cfd5" + integrity sha512-BDZEmr4KKh3JASgkXouOwoTWRS1UPE3gdZYZ7Sk7SJ1i8+Pk6zUQ4FnxaoF/cSAdCXyjSSr92Kns2bTk/QuNkQ== + dependencies: + "@zag-js/core" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/auto-resize@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/auto-resize/-/auto-resize-1.31.1.tgz#882d6313b71b6546b905aa718b6aa6e7b4ff46e4" + integrity sha512-qzWHibjBekSmFweG+EWY8g0lRzKtok7o9XtQ+JFlOu3s6x4D02z2YDzjDdfSLmS7j0NxISnwQkinWiDAZEYHog== + dependencies: + "@zag-js/dom-query" "1.31.1" + +"@zag-js/avatar@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/avatar/-/avatar-1.31.1.tgz#31f103b0984bc1e0b03704d14cbfd145fc9a4f38" + integrity sha512-Grosi2hRn4wfDYlPd8l+d4GCIFMsoj6ZFqii+1k14AqTDiCUJ/J0jCvOrRHkvkpEqektjuSD7e/GCX+yawqkuQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/bottom-sheet@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/bottom-sheet/-/bottom-sheet-1.31.1.tgz#9fe9b295bff3661b0a32a758df6d9c1be2f218c5" + integrity sha512-ZBbIpYyZX2zQeqW36aODVi9/I4J3zS1XmIHUjeXmfmf6TlQUA1ydgYl7ipREfmCzNWX2LEA5ZnPJQw0UBcrB8w== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/aria-hidden" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/focus-trap" "1.31.1" + "@zag-js/remove-scroll" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/carousel@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/carousel/-/carousel-1.31.1.tgz#4cf83807ae28b14b94947471bf260ccbe3855685" + integrity sha512-228Ol86G/lg8crcomy5cALkUYdOHCHcvJnSOQzeUj80JNjlELzrjBpaAj4lx8dZocfwou2Sg4NyZJ+mISSc+Dg== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/scroll-snap" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/checkbox@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/checkbox/-/checkbox-1.31.1.tgz#6547280b733b14c97cfb1da2d253f9dae91a8374" + integrity sha512-oLS8bqhimckLl6coCNmKPPUmB8wIbVhtkpLwLPLgz4vhhUe7gnpB5dea14Ow2JTBnmug8bMh/bJDtuPa9qQuTw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/focus-visible" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/clipboard@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/clipboard/-/clipboard-1.31.1.tgz#6f1803ec184440d61ca280dd6f21539f24d2ddc0" + integrity sha512-pv/gOmD9DMg+YmSMjahyd5oSp7/v9K0uQ3att6fPeaNMjB42b3tnY1S1GNVy5Ltf/qHDab6WVwlEN+1zKHXaYw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/collapsible@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/collapsible/-/collapsible-1.31.1.tgz#46082b9827d1d5893d46bdb71c396e7d8defae2a" + integrity sha512-eCC5G6bBZUwF8z2XULQXUNRxqte9I2Sv+WJ2brycPn1a68uYD76RzFBmLQ2er95VbshUdeo8nRuX8MooAFuYzg== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/collection@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/collection/-/collection-1.31.1.tgz#26724e1ffa359097f1f2836f9d66a95b56bd45ff" + integrity sha512-ecpfyfCj8Y0/GUPuHYsLxexIrx10VuR3Wd0H+lamcki3lYgQxZrpLRFMwgTqmI/m7t3zhm5QeEvMUJ1H14YMLA== + dependencies: + "@zag-js/utils" "1.31.1" + +"@zag-js/color-picker@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/color-picker/-/color-picker-1.31.1.tgz#547d2f354adf37f2dd47e551872c979452cc04a8" + integrity sha512-AWNZth49iEDxqh1DBZNSKpfEM/FF+MjL5bgUHVctnHdkpFsZLynJorWQQ4hNXNDFEc/I5w10KSxVCcO6tsPGFw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/color-utils" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/color-utils@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/color-utils/-/color-utils-1.31.1.tgz#444efc5c0fc0064b85915404980916137e8dde12" + integrity sha512-HdjTRU8C0tO6hK+PBVlu8iQH1MJaAnJAEdq2FcD97mq0PiPhrSj6iOftnrvPsE4CRieVFjnJWOvaubWFc4VmHA== + dependencies: + "@zag-js/utils" "1.31.1" + +"@zag-js/combobox@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/combobox/-/combobox-1.31.1.tgz#6719fe141ae01459814f8a242add2c56d127927f" + integrity sha512-IT0getSAGzngdRL20iX/iAh2d7DzVoMDDppOsOFBG2owKAgLpj8uLvUhy+lcrm6N8yxYOya89D6Aef7V5KdwlQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/aria-hidden" "1.31.1" + "@zag-js/collection" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/core@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/core/-/core-1.31.1.tgz#a8419dee9bb5e9b021009aba4b2aa44d6a48fda8" + integrity sha512-RaMJeqtjxG6k7iFD3WQnlyFJVT3yfQN+pJygAHH37GsMtiNzQQJOoesjb0LV9T27jwMXeNUzrh3MSDr1/0yVcQ== + dependencies: + "@zag-js/dom-query" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/date-picker@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/date-picker/-/date-picker-1.31.1.tgz#e9ec0419edd7e300279aa858e113e0038bb1b792" + integrity sha512-AOWN/IskGidVQt5g+uE9cILqJBTclE6OG1GC9WSWuyP/y4F+PdP/781SgYpYCZg/6pMGbL01PFKKb7xOOCeZAg== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/date-utils" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/live-region" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/date-utils@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/date-utils/-/date-utils-1.31.1.tgz#42633d8163d4d5aa9c50b0cc00f8d28ba77b977c" + integrity sha512-+Aq9g/rqLeiRmnazgdZMc59gAxqxbw3GGy8AngrtNipgRtMhPlzGa3S4Qsq1yau6OKaHZ13uckUS+MhLNbBY+Q== + +"@zag-js/dialog@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/dialog/-/dialog-1.31.1.tgz#a87edd3d67ce0fc6e66c036bbb15723920724ff9" + integrity sha512-iaWlYQ6TYoVjM/X5+UZVZzKiMboE50GnEzGUpbhbeRNRiLqSu5dODSFzior1G4kde/ns5eN+BTf/Tm6AT4N2og== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/aria-hidden" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/focus-trap" "1.31.1" + "@zag-js/remove-scroll" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/dismissable@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/dismissable/-/dismissable-1.31.1.tgz#93c56e85b2e0c71e0714da14d0600254da6c614e" + integrity sha512-jCdJwQmEkG6PlrN13fUk2l7ZclSu54FZwmT4xOtQpEbaiAiESm5KI5oyFh5jDPY47Goa28UJkEjWXVgKXKWb0g== + dependencies: + "@zag-js/dom-query" "1.31.1" + "@zag-js/interact-outside" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/dom-query@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/dom-query/-/dom-query-1.31.1.tgz#9b7e8ee3441dcb1b6c48f042177ffe293334d479" + integrity sha512-2tCZLwSfoXm62gwl0neiAN6u5VnzUhy5wHtKbX+klqGFatnca3Bm++H9+4PHMrwUWRbPg3H5N151lKFEOQhBfQ== + dependencies: + "@zag-js/types" "1.31.1" + +"@zag-js/editable@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/editable/-/editable-1.31.1.tgz#6bc4500a9c0acff1408a9ae83707d0f96e824159" + integrity sha512-JMICHw4/x0YqDy/n+I+TeaXlFbTA0j9w3UqOWMwUFQ+dAsq4JLXeqZDXu19MQN6yaTFdOpG1EFw4FEVTsu+d3Q== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/interact-outside" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/file-upload@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/file-upload/-/file-upload-1.31.1.tgz#2e6d063b68eb4b86d768dc543695cbc7e5cd35d5" + integrity sha512-cp7qMiXKrIcTfDamOz9wlnJLeBF8gucTI7Y+iKaP+hiIW+OG254GElfQiqXNDad3HUmD+Dt8Tx6uAzL/mw3sbQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/file-utils" "1.31.1" + "@zag-js/i18n-utils" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/file-utils@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/file-utils/-/file-utils-1.31.1.tgz#69a4f5142f7c2b6af7e1f1b42773af25e2df002f" + integrity sha512-MDDz52IdPh/mPUYrqUXvh7qDckJHs+mt5gjfx0N89qh2JNXuRU14zPotOKTzIKM4o+HFZkAT6BAfMpr9CX/0ug== + dependencies: + "@zag-js/i18n-utils" "1.31.1" + +"@zag-js/floating-panel@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/floating-panel/-/floating-panel-1.31.1.tgz#cfe2b6cc0e6ec12f3ea07b46f14cf4aa4dbc2788" + integrity sha512-Pjgd/wjdglZ90dtq/LC4o5sc6w0m+RehhPmJcIzq9T+E/Xrb6qrhf06QhxB9LwSj4DG/gIv87gmD2qF1VH7cRQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/rect-utils" "1.31.1" + "@zag-js/store" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/focus-trap@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/focus-trap/-/focus-trap-1.31.1.tgz#2417ce97dc6d28040378b152ddb22027e14f2492" + integrity sha512-omgUhAz1r81pYAujqYIIavdTKJzDRExioSiqhnx/xq10a6Q/xavMFflq8w7edMc9JHkTOnr9E5qh9abCVJjhpQ== + dependencies: + "@zag-js/dom-query" "1.31.1" + +"@zag-js/focus-visible@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/focus-visible/-/focus-visible-1.31.1.tgz#4cc4e8a391aab71f1be4141c741236a8ecf15fad" + integrity sha512-GC59A3yd7tj8aKhzvhrM+CEZZraXm5y/SpfIjz1J7kGV6eeXbUtjkbe75g99Ve8iJYfQVQlAj2GyN3oniHc5Zw== + dependencies: + "@zag-js/dom-query" "1.31.1" + +"@zag-js/highlight-word@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/highlight-word/-/highlight-word-1.31.1.tgz#d6554f7747f203075f33d3fb6b2510f65baca86e" + integrity sha512-nQw7t8LgWXW+6Z5E/p6T+OST0DDXp35mrFCzrkJL54aVTZ3GuLyIP2p0/HGQr2hE/KKLbZEs5i6UcXF84tiI4g== + +"@zag-js/hover-card@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/hover-card/-/hover-card-1.31.1.tgz#41b110332108ed2ed35bbe765b5b72460fb156fb" + integrity sha512-R74kz2wPgGwB3jKQeD91kdtlvVKpffWBJHqw8yCBd95GXGVmhym+BPoCToJzcqiemP8+0EtSuVPU9IHaSuJnSg== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/i18n-utils@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/i18n-utils/-/i18n-utils-1.31.1.tgz#8cb701774b7235260872419b421f217cea481336" + integrity sha512-SARkFuo1+Q0WcNv4jqvxp5hjCOqu/gBa7p6BTh7v5Bo00QhKRM/bCvVt0EB6V+h2oejrZfkwZ0MwbpQiL6L2aQ== + dependencies: + "@zag-js/dom-query" "1.31.1" + +"@zag-js/image-cropper@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/image-cropper/-/image-cropper-1.31.1.tgz#96b79d9af49023a456049c8563a2cd56fdbd9306" + integrity sha512-hFuy4I3jIJ/iyJsnfbLX1l/cJtN42j7lwhw8TeWVX8Y+hHxFPMSKx7AQirt/hALUbyy7QsQgAd5IslpsYq1Nlg== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/interact-outside@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/interact-outside/-/interact-outside-1.31.1.tgz#280bdcfb6f83aacca3b143197156cbdb98db62d6" + integrity sha512-oxBAlBqcatlxGUmhwUCRYTADIBrVoyxM1YrFzR1R8jhvVR/QCaxoLAyKwcA3mWXlZ8+NlXb7n5ELE11BZb/rEg== + dependencies: + "@zag-js/dom-query" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/json-tree-utils@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/json-tree-utils/-/json-tree-utils-1.31.1.tgz#54c495131dae7cdda09e77a38ad870e3f0b04fd2" + integrity sha512-wrNek2UBE69FWpo2f0E2MxiboBS+Uop79LeQU2jNDujA1o3x6b1Lp2r7Fl1sfnUWMdKVVQb44oqfIj2g3CTEmQ== + +"@zag-js/listbox@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/listbox/-/listbox-1.31.1.tgz#f83d8489d071187f1d4eb3d9e36af865df1c687d" + integrity sha512-LcTIr4I9eN4MR1nSRfQfseWgj4ybOXXAY2o5dBpEBL67dnCSX3swNb/4LQO+ebj077BViQb66pBb1KSoeHGkEQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/collection" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/focus-visible" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/live-region@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/live-region/-/live-region-1.31.1.tgz#6fbd62e5f2559f77756f0136d8f4ee1ef02888fa" + integrity sha512-RBx8jk1dgvkEUuFs77SBZn0WwvEkeZgVawVu6XUAy4ENfhP0D/qkvwNk+Els8InKmr1gWKajD7sh+g8M40Ex6A== + +"@zag-js/marquee@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/marquee/-/marquee-1.31.1.tgz#670222a46ea629f677321b3d017d1cd0f0eb8a13" + integrity sha512-Rt7+zy7CDOxXm0PqaTcmuWxcrZOPOpZY4T6IxOZk4ZcOXJQ2v7CkF3EK0pdI9PyI6Zpk/YIwQkENjidT55db0A== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/menu@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/menu/-/menu-1.31.1.tgz#bdfc59e9931e3e2e6336a94acdaad0a4d94753d0" + integrity sha512-eJPRM8tlauRTsAoJXchDBzMzL2RhXYSHmHak2IJCDMApCV51p0MqGYP8Er3DbMSQTPUFuTq779uUIarDqW+zmA== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/rect-utils" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/navigation-menu@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/navigation-menu/-/navigation-menu-1.31.1.tgz#68f05a2cbad76eaee597c514fdc32b0a0b2d07ae" + integrity sha512-xS4aynqmB9NYicPbEW8lPPakAfDfSgIDL1pRVSD6f1+VXkHD6LgNn6jUNDNbFt65mGhLpA2IczbvLCxv0g/ISQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/number-input@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/number-input/-/number-input-1.31.1.tgz#efad93bb62d1cc04c0b8aef148cf25160f957cf6" + integrity sha512-vn+BXEZ2/g2CMIFFyjjye/SbCeW3I/rlszL8EyBmhMcuA1l51OX2WKry6HeQNiU41uMyFg2rb1pb5KVw1gJsCg== + dependencies: + "@internationalized/number" "3.6.5" + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/pagination@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/pagination/-/pagination-1.31.1.tgz#60b34682e5e887d6cd7c8ea05ff331f82f12474a" + integrity sha512-icW6FNzIKNz7iXU+prlQWpMFJedDrhmCKzzI39SY+dv5g1Gnrlc0b44PxvNl5PWFLSkB5KBT/R1WCqd8Kh4cCA== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/password-input@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/password-input/-/password-input-1.31.1.tgz#d6dded0eef022487b27f0315f1c8836aae6a70d9" + integrity sha512-AivOeNO14a39xhxVMB2TVmIjmQ89OwVz0+2IjX3JjLS2Pmia+gg9xnVd2kBIcKfnqUN4MBnzmk7t46YWJMQVVQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/pin-input@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/pin-input/-/pin-input-1.31.1.tgz#0301f4eb724d173d656ff877b5e3d461f4d89b7d" + integrity sha512-k3ESoX5ve5sbWBLTCPYAzgLjRU7mVNEUiqAOhRgazOcBGV5wjGh398zWb1jr0FMxPnoAMrXDN/CQwJTmJcMKrg== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/popover@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/popover/-/popover-1.31.1.tgz#a6b6066b4898783929f21789cfad494d156a7d09" + integrity sha512-uCFJP3DFBkEBAre6lgGLw2xWS2ZIuT/DLeajIXb+8BmC9KCF0wY4c9qojx9F3rGMJQxcGl+WUoXENkOvkTaVhQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/aria-hidden" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/focus-trap" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/remove-scroll" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/popper@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/popper/-/popper-1.31.1.tgz#fb6292df602dd5146e6b238393e8c62c53bbf1c9" + integrity sha512-wLXcEqzn9MK1rGbsgnDH26o5ZWqR4oeb6ZepKKy0gcuJl/1S5/dr1VBvxJNMZlf9d6etvYklG5LRnIVkXCbrjA== + dependencies: + "@floating-ui/dom" "1.7.4" + "@zag-js/dom-query" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/presence@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/presence/-/presence-1.31.1.tgz#fa5b6be77b687fecae040f50261171c0b12b0cfa" + integrity sha512-tv+WsBnA0abIlDuEfZMh0lRPF4cMs6kWJosNkGBwzeXnGds+KXjzpL2KDtwDgbJgN3sI0xHPMYjRy2v3ZamcDA== + dependencies: + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + +"@zag-js/progress@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/progress/-/progress-1.31.1.tgz#d4c9e9c59df095d718f7ce4661016e49afb1015a" + integrity sha512-f9lIDHCRcFAG14LVEKOAPTdqPzphwIIraC6fTr9AwmNlYI6/qFDkz3jOlYVSyk5VsJAIFM/777x/CdqjliiOqg== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/qr-code@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/qr-code/-/qr-code-1.31.1.tgz#058f34c9b2ecb2b37991cb0e1bc6898ccac3c277" + integrity sha512-Rxh+HF12SgUp5rvTelp1qyLK3xkn37h2fT/L4eBQ0f8OUEo8wfowEbs36+1i61d6UuH7PJt4q/07eIf6vNVevA== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + proxy-memoize "3.0.1" + uqr "0.1.2" + +"@zag-js/radio-group@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/radio-group/-/radio-group-1.31.1.tgz#ef20d1267d1e2365c719bd586b2d2b0d9846296c" + integrity sha512-OfKIdEtSG0EuHM+cFVqcR+04yzZmcDRgG3j0QhoJsyS1my63ZHbwC2HNAtfPFh4U4sJx9yUexwSzPGZ6pOzIdw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/focus-visible" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/rating-group@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/rating-group/-/rating-group-1.31.1.tgz#b68a8d6dfc68cbdcd5d6348da096014e162a2a64" + integrity sha512-BkQUglKm4a+KXYPACYvIvBJSuEyzV0YQqjjiucwJ5UiOlK72C66VBvyGN+DqJRDnkU1K5azt6E1Ja5ANk3fgsg== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/react@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/react/-/react-1.31.1.tgz#7589c927e779848f4c2b46f9167173db2b93ff72" + integrity sha512-a7uYH+tcw1UYbcovyVBzlh6X8KztK/b1+s8sMs4Srhd24M+hZMetV94Z0bM1Km5aNAnoS4gkH3gtJjH0OphquQ== + dependencies: + "@zag-js/core" "1.31.1" + "@zag-js/store" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/rect-utils@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/rect-utils/-/rect-utils-1.31.1.tgz#c6dcbc96d76f4ebb8bad618b02eba6e4a31c466a" + integrity sha512-lBFheAnz8+3aGDFjqlkw0Iew/F03lFjiIf26hkkcFSZu0ltNZUMG/X3XLHUnHxdfbdBguc8ons6mr2MkVvisng== + +"@zag-js/remove-scroll@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/remove-scroll/-/remove-scroll-1.31.1.tgz#d9f3347dd28d1fc6369d736f479350cca62080ae" + integrity sha512-gVVJuFKaCjo652RmajYmkjXKgjJWLQ5ZhZLTaLUKWM1mAarvlqnLui8jrHEHLxqpfsjQylfdhJKkWmyF8NAgTA== + dependencies: + "@zag-js/dom-query" "1.31.1" + +"@zag-js/scroll-area@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/scroll-area/-/scroll-area-1.31.1.tgz#37d7412cdc9506ebb4ae417811aff51c74f75d86" + integrity sha512-GBXd1K3U0AHwWlJaqAMKQMZyeoxuBO6XYrVgdvzgiftQbJrZs5fuYOFyDvPLDWHTLYxaHso44/f+9EmAUAiytw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/scroll-snap@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/scroll-snap/-/scroll-snap-1.31.1.tgz#13f43987259fed9b7388f4bcc8b18c284f0d7d7e" + integrity sha512-YWsfhcQqiffu2X9HuB0fMnEQAu6rEOfGcvQYinvB6pjWPOvIJGxGMi/dYyy21XQDNJ9K1IcWRIo/yuaajoJyQQ== + dependencies: + "@zag-js/dom-query" "1.31.1" + +"@zag-js/select@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/select/-/select-1.31.1.tgz#aa514e8668e5d720a41bbadf10c4f6720247bc48" + integrity sha512-vKWb8BiRY83Y3HkDNnimf6cr1yvzJh1HwZlzXFz0y47zEvlikQaf+r96obR78RgTtMjNTTV15tTXdc1/WFoYkw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/collection" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/signature-pad@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/signature-pad/-/signature-pad-1.31.1.tgz#b48bac5dd2c38eb689886b822da1936df4dfad39" + integrity sha512-bz3WtLuIZoLrJDKcdS7fPAdD/Qi9wKiKACl5cu+ftv9zg8w+qqYNLtjH9HxeUFbCtQRKqcdXjO/UZ8iL07hgsQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + perfect-freehand "^1.2.2" + +"@zag-js/slider@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/slider/-/slider-1.31.1.tgz#af9ab16b79d65f7d6a7f36d18cbc3333ee902484" + integrity sha512-FILbLTMd3BnyclZ28+ippfyqzYPGK60qZapxtTERmWDC75Okf8AFnTCQf84Y8jRmBKCS1yhjF+IOtkFAENeB6w== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/splitter@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/splitter/-/splitter-1.31.1.tgz#29d84f3d689eb8e2ae26c18a1653178ddcdefd46" + integrity sha512-7SGBT2/xKsOzeSQEg+Otn1XV3RHrAz3jTySjBRKoEmdxubhfREqbKotbGVG65aTve11fQnmJ3Oyt3GJOeraxLA== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/steps@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/steps/-/steps-1.31.1.tgz#66dd823ffda8446999903c7182eb1f14bbe87073" + integrity sha512-KsBH38V3tH9/q8CDgx4sUSXLYwFdcp1crZy8hTIcN0RUiZ55PmqYKkN2znzBjTbaCW9yhP8kXsbuo2s8OIU5lQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/store@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/store/-/store-1.31.1.tgz#aceafec78957bcf5d8f94db8fa56f7a38f113834" + integrity sha512-d5ZTRciTuXOGQ3nML15kQLaTiR1wJPxT1Fu1nN659X6Rl8DPtubYaRCZ3RCk9Kyiyg2z5HxeVqDswaDvGbM9Rg== + dependencies: + proxy-compare "3.0.1" + +"@zag-js/switch@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/switch/-/switch-1.31.1.tgz#4198ca6edc54828959194a2e3f9daf889944e989" + integrity sha512-Jii3OSqSa9sQux+hvSRvp9dirzUF09+PAjrLjCQs+BT08EZ0XqeGvVzM0Wqf9LFy07HdLZntai3IUaXLF6byBw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/focus-visible" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/tabs@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/tabs/-/tabs-1.31.1.tgz#69bbbff16ae554e470ed4001565030b14872902c" + integrity sha512-QBq4ngpBNMNEI7Wuaq8llwHOqgcVbNHHEDC5zHg60Bf7MY5ltP8wSq6Kldu0zZRVwrLzanYoMELDUyf9H0vtnw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/tags-input@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/tags-input/-/tags-input-1.31.1.tgz#21b807d8c9aa457601bd5c4a4bd60e62cfba3e3c" + integrity sha512-V4lJe/aMIs7WVoXYfszU6E3iARLLRQFMiycu76/slb8NWJiLrkSIaMQ4FAe2pqkodgCWXA83tuaeAZRq7ouTFg== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/auto-resize" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/interact-outside" "1.31.1" + "@zag-js/live-region" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/timer@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/timer/-/timer-1.31.1.tgz#c4d8976da52530d16360c38c887de931f2eaa897" + integrity sha512-bXfeSbneWGOBKlD5dYq06T8CSY9Ky+qb1yIfJAFsRF4n34mpUYRdtfwpNQYyddGpkLD7oH4VibajeZXB7HaL0g== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/toast@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/toast/-/toast-1.31.1.tgz#5370c81d90550cf4d36cecb8e7de2d8e86a44ada" + integrity sha512-MueHEei9ol3H6tWBruLxF7yEUpV3vsJ8brTQVRRtPr/6pqBs5kGzfL4YskhQ2tiwO6egay8YrkbaS3xJfpKt4w== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/toggle-group@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/toggle-group/-/toggle-group-1.31.1.tgz#1490a114f6a4b515e12398a485fdf7495311eb77" + integrity sha512-Mojc7mex01/gvwXfrUIIThzT7HOktZoMge9rrb6+P7rQX7ulyNXYPjQrW2tay+t54GOJ3xODo9dU7PpRzXeHbw== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/toggle@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/toggle/-/toggle-1.31.1.tgz#3ed4d3fb602179bcfab094c98abe6291528b7cf7" + integrity sha512-HbFBuGfdyYkNvOp3cEB8Civ4E92finT4u3e4LKysB4/LboqKA0cJvFhSnHyThbROONTx06W/3CxwoSFR4o8IhA== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/tooltip@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/tooltip/-/tooltip-1.31.1.tgz#db7d0c063a674c1d8f7361365f08a32a90217cfe" + integrity sha512-pWEU5XhEPpnyl2VLrGJlyjj7+p+X0UX3Fld+WGhc/hCaWiuW2ZzD/ewDRhSOZu4/TzAO3axrPqG1YhW4fhogKQ== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/focus-visible" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/tour@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/tour/-/tour-1.31.1.tgz#0f0e3823316fb239cf168045b5d1103b17b53afb" + integrity sha512-ZmcAevXxoENHmHG0xwdIt1oCLe2/DW1CEBFPr7YuGKc+FU3QbBVZMzcBHrJCe0nkKXhUKzHOHM78bOHD/gM76w== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dismissable" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/focus-trap" "1.31.1" + "@zag-js/interact-outside" "1.31.1" + "@zag-js/popper" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/tree-view@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/tree-view/-/tree-view-1.31.1.tgz#3d1755d8729468b778ae48dec32fdcd64aee0000" + integrity sha512-Q+VSQz7X1XR8gT7ICWXlQOJIvzTWw/9BlF7B073UpEgAKRFlD11FmERka5y/BYqj8uE0vazcbSEA3Vc2dgCMJA== + dependencies: + "@zag-js/anatomy" "1.31.1" + "@zag-js/collection" "1.31.1" + "@zag-js/core" "1.31.1" + "@zag-js/dom-query" "1.31.1" + "@zag-js/types" "1.31.1" + "@zag-js/utils" "1.31.1" + +"@zag-js/types@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/types/-/types-1.31.1.tgz#a42e7059a33b81bd2d63e4625d45a3a413eafd77" + integrity sha512-mKw5DoeBjFykfUHv3ifCRjcogFTqp0aCCsmqQMfnf+J/mg2aXpAx76AXT1PYXAVVhxdP6qGXNd0mOQZDVrIlSQ== + dependencies: + csstype "3.2.3" + +"@zag-js/utils@1.31.1": + version "1.31.1" + resolved "https://registry.yarnpkg.com/@zag-js/utils/-/utils-1.31.1.tgz#4dfdc7d43eec354b585d9f5d9886681e3e1cbbe1" + integrity sha512-KLm0pmOtf4ydALbaVLboL7W98TDVxwVVLvSuvtRgV53XTjlsVopTRA5/Xmzq2NhWujDZAXv7bRV603NDgDcjSw== + +acorn-jsx@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.0.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +astring@^1.8.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/astring/-/astring-1.9.0.tgz#cc73e6062a7eb03e7d19c22d8b0b3451fd9bfeef" + integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg== + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + +baseline-browser-mapping@^2.8.3: + version "2.9.17" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz#9d6019766cd7eba738cb5f32c84b9f937cc87780" + integrity sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +caniuse-lite@^1.0.30001579: + version "1.0.30001765" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz#4a78d8a797fd4124ebaab2043df942eb091648ee" + integrity sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + +collapse-white-space@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz#640257174f9f42c740b40f3b55ee752924feefca" + integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw== + +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +csstype@3.2.3, csstype@^3.0.2, csstype@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +debug@^4.0.0, debug@^4.3.1: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decode-named-character-reference@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz#3e40603760874c2e5867691b599d73a7da25b53f" + integrity sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q== + dependencies: + character-entities "^2.0.0" + +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +detect-libc@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + +error-ex@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + +esast-util-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz#8d1cfb51ad534d2f159dc250e604f3478a79f1ad" + integrity sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + unist-util-position-from-estree "^2.0.0" + +esast-util-from-js@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz#5147bec34cc9da44accf52f87f239a40ac3e8225" + integrity sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw== + dependencies: + "@types/estree-jsx" "^1.0.0" + acorn "^8.0.0" + esast-util-from-estree "^2.0.0" + vfile-message "^4.0.0" + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estree-util-attach-comments@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz#344bde6a64c8a31d15231e5ee9e297566a691c2d" + integrity sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw== + dependencies: + "@types/estree" "^1.0.0" + +estree-util-build-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz#b6d0bced1dcc4f06f25cf0ceda2b2dcaf98168f1" + integrity sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + estree-walker "^3.0.0" + +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== + +estree-util-scope@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/estree-util-scope/-/estree-util-scope-1.0.0.tgz#9cbdfc77f5cb51e3d9ed4ad9c4adbff22d43e585" + integrity sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + +estree-util-to-js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz#10a6fb924814e6abb62becf0d2bc4dea51d04f17" + integrity sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg== + dependencies: + "@types/estree-jsx" "^1.0.0" + astring "^1.8.0" + source-map "^0.7.0" + +estree-util-visit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-2.0.0.tgz#13a9a9f40ff50ed0c022f831ddf4b58d05446feb" + integrity sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/unist" "^3.0.0" + +estree-walker@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fathom-client@^3.6.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/fathom-client/-/fathom-client-3.7.2.tgz#819920316935974a200509dcf07774a3599676a9" + integrity sha512-sWtaNivhg7uwp/q1bUuIiNj4LeQZMEZ5NXXFFpZ8le4uDedAfQG84gPOdYehtVXbl+1yX2s8lmXZ2+IQ9a/xxA== + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gray-matter@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" + integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== + dependencies: + js-yaml "^3.13.1" + kind-of "^6.0.2" + section-matter "^1.0.0" + strip-bom-string "^1.0.0" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hast-util-from-html@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz#485c74785358beb80c4ba6346299311ac4c49c82" + integrity sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.1.0" + hast-util-from-parse5 "^8.0.0" + parse5 "^7.0.0" + vfile "^6.0.0" + vfile-message "^4.0.0" + +hast-util-from-parse5@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz#830a35022fff28c3fea3697a98c2f4cc6b835a2e" + integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^9.0.0" + property-information "^7.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-to-estree@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz#e654c1c9374645135695cc0ab9f70b8fcaf733d7" + integrity sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-attach-comments "^3.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + style-to-js "^1.0.0" + unist-util-position "^5.0.0" + zwitch "^2.0.0" + +hast-util-to-html@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz#ccc673a55bb8e85775b08ac28380f72d47167005" + integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + +hast-util-to-jsx-runtime@^2.0.0: + version "2.3.6" + resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz#ff31897aae59f62232e21594eac7ef6b63333e98" + integrity sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + style-to-js "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + +hast-util-to-string@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz#a4f15e682849326dd211c97129c94b0c3e76527c" + integrity sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + +hastscript@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff" + integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + +hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +inline-style-parser@0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz#b1fc68bfc0313b8685745e4464e37f9376b9c909" + integrity sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA== + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + +is-extendable@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0" + integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + +markdown-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" + integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== + +mdast-util-from-markdown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz#43f0abac9adc756e2086f63822a38c8d3c3a5096" + integrity sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-jsx@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz#fd04c67a2a7499efb905a8a5c578dddc9fdada0d" + integrity sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdx@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz#792f9cf0361b46bee1fdf1ef36beac424a099c41" + integrity sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w== + dependencies: + mdast-util-from-markdown "^2.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.2.1" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz#d7ff84ca499a57e2c060ae67548ad950e689a053" + integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + +micromark-core-commonmark@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" + integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-mdx-expression@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz#43d058d999532fb3041195a3c3c05c46fa84543b" + integrity sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-mdx-jsx@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz#ffc98bdb649798902fa9fc5689f67f9c1c902044" + integrity sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdx-md@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz#1d252881ea35d74698423ab44917e1f5b197b92d" + integrity sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-mdxjs-esm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz#de21b2b045fd2059bd00d36746081de38390d54a" + integrity sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdxjs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz#b5a2e0ed449288f3f6f6c544358159557549de18" + integrity sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ== + dependencies: + acorn "^8.0.0" + acorn-jsx "^5.0.0" + micromark-extension-mdx-expression "^3.0.0" + micromark-extension-mdx-jsx "^3.0.0" + micromark-extension-mdx-md "^2.0.0" + micromark-extension-mdxjs-esm "^3.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-mdx-expression@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz#bb09988610589c07d1c1e4425285895041b3dfa9" + integrity sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" + integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-events-to-acorn@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz#e7a8a6b55a47e5a06c720d5a1c4abae8c37c98f3" + integrity sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg== + dependencies: + "@types/estree" "^1.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" + +micromark-util-html-tag-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" + integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== + +micromark@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" + integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.6: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +next-mdx-remote@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/next-mdx-remote/-/next-mdx-remote-5.0.0.tgz#028a2cf5cf7f814d988d7ab11a401bed0f31b4ee" + integrity sha512-RNNbqRpK9/dcIFZs/esQhuLA8jANqlH694yqoDBK8hkVdJUndzzGmnPHa2nyi90N4Z9VmzuSWNRpr5ItT3M7xQ== + dependencies: + "@babel/code-frame" "^7.23.5" + "@mdx-js/mdx" "^3.0.1" + "@mdx-js/react" "^3.0.1" + unist-util-remove "^3.1.0" + vfile "^6.0.1" + vfile-matter "^5.0.0" + +next-themes@^0.4.0: + version "0.4.6" + resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6" + integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA== + +next@^16.1.4: + version "16.1.4" + resolved "https://registry.yarnpkg.com/next/-/next-16.1.4.tgz#d024bace2d52a2bea1dec33149b8bbd0852632c5" + integrity sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ== + dependencies: + "@next/env" "16.1.4" + "@swc/helpers" "0.5.15" + baseline-browser-mapping "^2.8.3" + caniuse-lite "^1.0.30001579" + postcss "8.4.31" + styled-jsx "5.1.6" + optionalDependencies: + "@next/swc-darwin-arm64" "16.1.4" + "@next/swc-darwin-x64" "16.1.4" + "@next/swc-linux-arm64-gnu" "16.1.4" + "@next/swc-linux-arm64-musl" "16.1.4" + "@next/swc-linux-x64-gnu" "16.1.4" + "@next/swc-linux-x64-musl" "16.1.4" + "@next/swc-win32-arm64-msvc" "16.1.4" + "@next/swc-win32-x64-msvc" "16.1.4" + sharp "^0.34.4" + +oniguruma-parser@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz#82ba2208d7a2b69ee344b7efe0ae930c627dcc4a" + integrity sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w== + +oniguruma-to-es@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz#0b909d960faeb84511c979b1f2af64e9bc37ce34" + integrity sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA== + dependencies: + oniguruma-parser "^0.12.1" + regex "^6.0.1" + regex-recursion "^6.0.2" + +pagefind@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pagefind/-/pagefind-1.4.0.tgz#0154b0a44b5ef9ef55c156824a3244bfc0c4008d" + integrity sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g== + optionalDependencies: + "@pagefind/darwin-arm64" "1.4.0" + "@pagefind/darwin-x64" "1.4.0" + "@pagefind/freebsd-x64" "1.4.0" + "@pagefind/linux-arm64" "1.4.0" + "@pagefind/linux-x64" "1.4.0" + "@pagefind/windows-x64" "1.4.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-entities@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== + dependencies: + "@types/unist" "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse-numeric-range@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3" + integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== + +parse5@^7.0.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== + dependencies: + entities "^6.0.0" + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +perfect-freehand@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.2.2.tgz#292f65b72df0c7f57a89c4b346b50d7139014172" + integrity sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ== + +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +property-information@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== + +proxy-compare@3.0.1, proxy-compare@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-3.0.1.tgz#3262cff3a25a6dedeaa299f6cf2369d6f7588a94" + integrity sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q== + +proxy-memoize@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/proxy-memoize/-/proxy-memoize-3.0.1.tgz#75eed518778b282abb0bc55e748995214b7f74a9" + integrity sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g== + dependencies: + proxy-compare "^3.0.0" + +react-dom@^19.0.0: + version "19.2.3" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17" + integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg== + dependencies: + scheduler "^0.27.0" + +"react-fathom@file:..": + version "0.2.0" + +react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react@^19.0.0: + version "19.2.3" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8" + integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA== + +reading-time@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" + integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== + +recma-build-jsx@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz#c02f29e047e103d2fab2054954e1761b8ea253c4" + integrity sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew== + dependencies: + "@types/estree" "^1.0.0" + estree-util-build-jsx "^3.0.0" + vfile "^6.0.0" + +recma-jsx@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/recma-jsx/-/recma-jsx-1.0.1.tgz#58e718f45e2102ed0bf2fa994f05b70d76801a1a" + integrity sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w== + dependencies: + acorn-jsx "^5.0.0" + estree-util-to-js "^2.0.0" + recma-parse "^1.0.0" + recma-stringify "^1.0.0" + unified "^11.0.0" + +recma-parse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-parse/-/recma-parse-1.0.0.tgz#c351e161bb0ab47d86b92a98a9d891f9b6814b52" + integrity sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ== + dependencies: + "@types/estree" "^1.0.0" + esast-util-from-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +recma-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-stringify/-/recma-stringify-1.0.0.tgz#54632030631e0c7546136ff9ef8fde8e7b44f130" + integrity sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g== + dependencies: + "@types/estree" "^1.0.0" + estree-util-to-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +regex-recursion@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/regex-recursion/-/regex-recursion-6.0.2.tgz#a0b1977a74c87f073377b938dbedfab2ea582b33" + integrity sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg== + dependencies: + regex-utilities "^2.3.0" + +regex-utilities@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/regex-utilities/-/regex-utilities-2.3.0.tgz#87163512a15dce2908cf079c8960d5158ff43280" + integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== + +regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/regex/-/regex-6.1.0.tgz#d7ce98f8ee32da7497c13f6601fca2bc4a6a7803" + integrity sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg== + dependencies: + regex-utilities "^2.3.0" + +rehype-parse@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-9.0.1.tgz#9993bda129acc64c417a9d3654a7be38b2a94c20" + integrity sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag== + dependencies: + "@types/hast" "^3.0.0" + hast-util-from-html "^2.0.0" + unified "^11.0.0" + +rehype-pretty-code@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/rehype-pretty-code/-/rehype-pretty-code-0.14.1.tgz#603d2ddccb11f1cf95e42a10b37b842beb8ad78e" + integrity sha512-IpG4OL0iYlbx78muVldsK86hdfNoht0z63AP7sekQNW2QOTmjxB7RbTO+rhIYNGRljgHxgVZoPwUl6bIC9SbjA== + dependencies: + "@types/hast" "^3.0.4" + hast-util-to-string "^3.0.0" + parse-numeric-range "^1.3.0" + rehype-parse "^9.0.0" + unified "^11.0.5" + unist-util-visit "^5.0.0" + +rehype-recma@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rehype-recma/-/rehype-recma-1.0.0.tgz#d68ef6344d05916bd96e25400c6261775411aa76" + integrity sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + hast-util-to-estree "^3.0.0" + +remark-mdx@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-3.1.1.tgz#047f97038bc7ec387aebb4b0a4fe23779999d845" + integrity sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg== + dependencies: + mdast-util-mdx "^3.0.0" + micromark-extension-mdxjs "^3.0.0" + +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" + +remark-rehype@^11.0.0: + version "11.1.2" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.1.2.tgz#2addaadda80ca9bd9aa0da763e74d16327683b37" + integrity sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.19.0: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== + +section-matter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" + integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== + dependencies: + extend-shallow "^2.0.1" + kind-of "^6.0.0" + +semver@^7.7.3: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + +sharp@^0.34.4: + version "0.34.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0" + integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg== + dependencies: + "@img/colour" "^1.0.0" + detect-libc "^2.1.2" + semver "^7.7.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.34.5" + "@img/sharp-darwin-x64" "0.34.5" + "@img/sharp-libvips-darwin-arm64" "1.2.4" + "@img/sharp-libvips-darwin-x64" "1.2.4" + "@img/sharp-libvips-linux-arm" "1.2.4" + "@img/sharp-libvips-linux-arm64" "1.2.4" + "@img/sharp-libvips-linux-ppc64" "1.2.4" + "@img/sharp-libvips-linux-riscv64" "1.2.4" + "@img/sharp-libvips-linux-s390x" "1.2.4" + "@img/sharp-libvips-linux-x64" "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + "@img/sharp-linux-arm" "0.34.5" + "@img/sharp-linux-arm64" "0.34.5" + "@img/sharp-linux-ppc64" "0.34.5" + "@img/sharp-linux-riscv64" "0.34.5" + "@img/sharp-linux-s390x" "0.34.5" + "@img/sharp-linux-x64" "0.34.5" + "@img/sharp-linuxmusl-arm64" "0.34.5" + "@img/sharp-linuxmusl-x64" "0.34.5" + "@img/sharp-wasm32" "0.34.5" + "@img/sharp-win32-arm64" "0.34.5" + "@img/sharp-win32-ia32" "0.34.5" + "@img/sharp-win32-x64" "0.34.5" + +shiki@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-3.21.0.tgz#64686fe6bfc6b2b602d209eb6c8cdbc79d0eff22" + integrity sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w== + dependencies: + "@shikijs/core" "3.21.0" + "@shikijs/engine-javascript" "3.21.0" + "@shikijs/engine-oniguruma" "3.21.0" + "@shikijs/langs" "3.21.0" + "@shikijs/themes" "3.21.0" + "@shikijs/types" "3.21.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +source-map-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +source-map@^0.7.0: + version "0.7.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.6.tgz#a3658ab87e5b6429c8a1f3ba0083d4c61ca3ef02" + integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +strip-bom-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" + integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== + +style-to-js@^1.0.0: + version "1.1.21" + resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.21.tgz#2908941187f857e79e28e9cd78008b9a0b3e0e8d" + integrity sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ== + dependencies: + style-to-object "1.0.14" + +style-to-object@1.0.14: + version "1.0.14" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.14.tgz#1d22f0e7266bb8c6d8cae5caf4ec4f005e08f611" + integrity sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw== + dependencies: + inline-style-parser "0.2.7" + +styled-jsx@5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499" + integrity sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA== + dependencies: + client-only "0.0.1" + +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + +tslib@^2.4.0, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +typescript@^5.7.0: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + +unified@^11.0.0, unified@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== + dependencies: + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" + +unist-util-is@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" + integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-is@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.1.tgz#d0a3f86f2dd0db7acd7d8c2478080b5c67f9c6a9" + integrity sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz#d94da4df596529d1faa3de506202f0c9a23f2200" + integrity sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-remove@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/unist-util-remove/-/unist-util-remove-3.1.1.tgz#8bfa181aff916bd32a4ed30b3ed76d0c21c077df" + integrity sha512-kfCqZK5YVY5yEa89tvpl7KnBBHu2c6CzMkqHUrlOqaRgGOMp0sMvwWOVrbAtj03KhovQB7i96Gda72v/EFE0vw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^5.0.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" + integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz#777df7fb98652ce16b4b7cd999d0a1a40efa3a02" + integrity sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.1.0.tgz#9a2a28b0aa76a15e0da70a08a5863a2f060e2468" + integrity sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +uqr@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/uqr/-/uqr-0.1.2.tgz#5c6cd5dcff9581f9bb35b982cb89e2c483a41d7d" + integrity sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA== + +vfile-location@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3" + integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + +vfile-matter@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vfile-matter/-/vfile-matter-5.0.1.tgz#3f701840dde13c68d72d5c5ebd9cf233dff84419" + integrity sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw== + dependencies: + vfile "^6.0.0" + yaml "^2.0.0" + +vfile-message@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" + integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0, vfile@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yaml@^2.0.0: + version "2.8.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5" + integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A== + +zwitch@^2.0.0, zwitch@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/examples/README.md b/examples/README.md index 81f9dc9..4bb9817 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,43 +2,63 @@ This directory contains example applications demonstrating how to integrate `react-fathom` for privacy-focused analytics in your React projects. +## Live Demos + +Each example is hosted as a subdomain of react-fathom.com: + +| Example | Live Demo | Description | +|---------|-----------|-------------| +| React | [react.react-fathom.com](https://react.react-fathom.com) | Standard React with Vite and React Router | +| Next.js App Router | [next-app.react-fathom.com](https://next-app.react-fathom.com) | Modern Next.js with Server Components | +| Next.js Pages Router | [next-pages.react-fathom.com](https://next-pages.react-fathom.com) | Traditional Next.js routing | +| TanStack Router | [tanstack.react-fathom.com](https://tanstack.react-fathom.com) | TanStack Router with file-based routing | +| Gatsby | [gatsby.react-fathom.com](https://gatsby.react-fathom.com) | Gatsby static site generator | + ## Available Examples | Example | Framework | Router | Description | |---------|-----------|--------|-------------| +| [react](./react/) | React + Vite | React Router | Standard React SPA | | [next-app](./next-app/) | Next.js 13+ | App Router | Modern Next.js with React Server Components | | [next-pages](./next-pages/) | Next.js | Pages Router | Traditional Next.js routing | +| [tanstack-router](./tanstack-router/) | React + Vite | TanStack Router | Type-safe file-based routing | +| [gatsby](./gatsby/) | Gatsby | @reach/router | Static site generation | ## Which Example Should I Use? -- **Starting a new Next.js project?** Use [next-app](./next-app/) - the App Router is the recommended approach for new Next.js applications -- **Existing Next.js project with Pages Router?** Use [next-pages](./next-pages/) - works with the traditional `pages/` directory structure -- **Plain React or other frameworks?** Check the main [README](../README.md) for generic React setup instructions +- **Standard React application?** Use [react](./react/) - Vite + React Router setup +- **Starting a new Next.js project?** Use [next-app](./next-app/) - the App Router is the recommended approach +- **Existing Next.js with Pages Router?** Use [next-pages](./next-pages/) - works with the traditional `pages/` directory +- **Using TanStack Router?** Use [tanstack-router](./tanstack-router/) - type-safe file-based routing +- **Building with Gatsby?** Use [gatsby](./gatsby/) - static site generation with @reach/router ## Getting Started ### 1. Navigate to an example ```bash -cd examples/next-app # or next-pages +cd examples/react # or next-app, next-pages ``` ### 2. Install dependencies ```bash npm install -# or -yarn install -# or -pnpm install ``` ### 3. Configure your Fathom site ID -Create a `.env.local` file: +Create a `.env` or `.env.local` file: ```bash +# For React / TanStack Router (Vite) +VITE_FATHOM_SITE_ID=your-site-id-here + +# For Next.js NEXT_PUBLIC_FATHOM_SITE_ID=your-site-id-here + +# For Gatsby +GATSBY_FATHOM_SITE_ID=your-site-id-here ``` Get your site ID from [Fathom Analytics](https://app.usefathom.com). New to Fathom? [Sign up here](https://usefathom.com/ref/EKONBS) and get $10 credit. @@ -51,27 +71,20 @@ npm run dev ### 5. Open your browser -Navigate to [http://localhost:3000](http://localhost:3000) and explore the example. +Navigate to the local URL shown in the terminal. ## What's Demonstrated -Each example demonstrates: +Each example includes self-documenting documentation and demonstrates: - **Automatic pageview tracking** - Navigate between pages to see tracking in action - **Manual event tracking** - Click buttons to track custom events -- **Goal tracking** - Track conversions and goals +- **Declarative components** - Use `` for click tracking - **TypeScript integration** - Full type safety - -## Development Notes - -These examples use `workspace:*` to reference the local `react-fathom` package, making them ideal for: - -- Testing changes to the library -- Debugging integration issues -- Exploring different configuration options +- **Revenue tracking** - Track events with monetary values ## Learn More -- [react-fathom Documentation](../README.md) +- [react-fathom Documentation](https://react-fathom.com/docs) - [Fathom Analytics Documentation](https://usefathom.com/docs) - [Next.js Documentation](https://nextjs.org/docs) diff --git a/examples/gatsby/.env.example b/examples/gatsby/.env.example new file mode 100644 index 0000000..f5ea6b3 --- /dev/null +++ b/examples/gatsby/.env.example @@ -0,0 +1,2 @@ +# Get your site ID from https://usefathom.com +GATSBY_FATHOM_SITE_ID=YOUR_SITE_ID diff --git a/examples/gatsby/.gitignore b/examples/gatsby/.gitignore new file mode 100644 index 0000000..4d8b539 --- /dev/null +++ b/examples/gatsby/.gitignore @@ -0,0 +1,4 @@ +node_modules +.cache +public +.env diff --git a/examples/gatsby/gatsby-config.ts b/examples/gatsby/gatsby-config.ts new file mode 100644 index 0000000..ab0ee8c --- /dev/null +++ b/examples/gatsby/gatsby-config.ts @@ -0,0 +1,13 @@ +import type { GatsbyConfig } from 'gatsby' + +const config: GatsbyConfig = { + siteMetadata: { + title: 'Gatsby Example - react-fathom', + description: + 'Example Gatsby application demonstrating react-fathom analytics integration', + siteUrl: 'https://example.com', + }, + plugins: ['gatsby-plugin-react-helmet'], +} + +export default config diff --git a/examples/gatsby/package.json b/examples/gatsby/package.json new file mode 100644 index 0000000..b6b3b40 --- /dev/null +++ b/examples/gatsby/package.json @@ -0,0 +1,28 @@ +{ + "name": "react-fathom-gatsby-example", + "version": "0.1.0", + "private": true, + "scripts": { + "develop": "gatsby develop", + "build": "gatsby build", + "serve": "gatsby serve", + "clean": "gatsby clean" + }, + "dependencies": { + "@chakra-ui/react": "^3.2.0", + "@emotion/react": "^11.13.0", + "@reach/router": "^1.3.4", + "fathom-client": "^3.6.0", + "gatsby": "^5.14.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-fathom": "file:../..", + "react-helmet": "^6.1.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@types/react-helmet": "^6.1.0", + "typescript": "^5.7.0" + } +} diff --git a/examples/gatsby/src/components/Layout.tsx b/examples/gatsby/src/components/Layout.tsx new file mode 100644 index 0000000..cef93a8 --- /dev/null +++ b/examples/gatsby/src/components/Layout.tsx @@ -0,0 +1,90 @@ +import React from 'react' +import { Helmet } from 'react-helmet' +import { ChakraProvider, defaultSystem, Box, Container, Flex, HStack, Link, Text } from '@chakra-ui/react' +import { FathomProvider } from 'react-fathom' +import { GatsbyFathomTrackView } from 'react-fathom/gatsby' + +import { Navbar } from './Navbar' + +interface LayoutProps { + children: React.ReactNode +} + +const siteId = process.env.GATSBY_FATHOM_SITE_ID || 'DEMO' + +export function Layout({ children }: LayoutProps) { + return ( + + + + + + + + + + + + + + + + + {children} + + + + + + + © {new Date().getFullYear()} —{' '} + + react-fathom + + + + + Docs + + + GitHub + + + Fathom + + + + + + + + + ) +} diff --git a/examples/gatsby/src/components/Navbar.tsx b/examples/gatsby/src/components/Navbar.tsx new file mode 100644 index 0000000..b5a3670 --- /dev/null +++ b/examples/gatsby/src/components/Navbar.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { Link } from 'gatsby' +import { Box, Flex, Text, HStack, Link as ChakraLink } from '@chakra-ui/react' + +export function Navbar() { + return ( + + + + + react-fathom + + + — Gatsby + + + + + Home + + + About + + + Contact + + + + + ) +} diff --git a/examples/gatsby/src/pages/about.tsx b/examples/gatsby/src/pages/about.tsx new file mode 100644 index 0000000..f4bb19f --- /dev/null +++ b/examples/gatsby/src/pages/about.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import type { HeadFC, PageProps } from 'gatsby' +import { Box, Heading, Text, VStack } from '@chakra-ui/react' + +import { Layout } from '../components/Layout' + +const AboutPage: React.FC = () => { + return ( + + + + + About + + + This page demonstrates automatic pageview tracking. When you + navigate here, react-fathom automatically tracks the pageview. + + + + + + How It Works + + + The GatsbyFathomTrackView component uses Gatsby's + @reach/router to listen for route changes and automatically sends + pageview events to Fathom Analytics. + + + Open your browser's developer console to see the debug output, or + check the debug panel if visible. + + + + + ) +} + +export default AboutPage + +export const Head: HeadFC = () => ( + <> + About - Gatsby Example - react-fathom + + +) diff --git a/examples/gatsby/src/pages/contact.tsx b/examples/gatsby/src/pages/contact.tsx new file mode 100644 index 0000000..b23ffa1 --- /dev/null +++ b/examples/gatsby/src/pages/contact.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import type { HeadFC, PageProps } from 'gatsby' +import { Box, Heading, Text, VStack, Input, Button } from '@chakra-ui/react' +import { useFathom } from 'react-fathom' + +import { Layout } from '../components/Layout' + +const ContactPage: React.FC = () => { + const { trackEvent } = useFathom() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + trackEvent('contact_form_submit') + alert('Form submitted! Check the console to see the tracked event.') + } + + return ( + + + + + Contact + + + This page demonstrates form tracking. Submit the form to see custom + event tracking in action. + + + + + + + + Name + + + + + + Email + + + + + + Message + + + + + + + + + ) +} + +export default ContactPage + +export const Head: HeadFC = () => ( + <> + Contact - Gatsby Example - react-fathom + + +) diff --git a/examples/gatsby/src/pages/index.tsx b/examples/gatsby/src/pages/index.tsx new file mode 100644 index 0000000..85c45fb --- /dev/null +++ b/examples/gatsby/src/pages/index.tsx @@ -0,0 +1,79 @@ +import React from 'react' +import type { HeadFC, PageProps } from 'gatsby' +import { Box, Heading, Text, VStack, Button, HStack } from '@chakra-ui/react' +import { useFathom } from 'react-fathom' + +import { Layout } from '../components/Layout' + +const IndexPage: React.FC = () => { + const { trackEvent, trackGoal } = useFathom() + + return ( + + + + + Gatsby + + Example + + + + This example demonstrates the Gatsby integration with react-fathom + for privacy-focused analytics. + + + + + + Try It Out + + + + + + + + + + Features + + + — Automatic pageview tracking on route changes + — Custom event tracking + — Goal conversion tracking + — Debug mode with event visualization + — Privacy-focused (no cookies) + + + + + ) +} + +export default IndexPage + +export const Head: HeadFC = () => ( + <> + Gatsby Example - react-fathom + + +) diff --git a/examples/gatsby/tsconfig.json b/examples/gatsby/tsconfig.json new file mode 100644 index 0000000..6568836 --- /dev/null +++ b/examples/gatsby/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "isolatedModules": true + }, + "include": ["src/**/*", "gatsby-config.ts"] +} diff --git a/examples/gatsby/vercel.json b/examples/gatsby/vercel.json new file mode 100644 index 0000000..99cfb00 --- /dev/null +++ b/examples/gatsby/vercel.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "gatsby", + "buildCommand": "cd ../.. && npm run build && cd examples/gatsby && npm run build", + "installCommand": "cd ../.. && npm install && cd examples/gatsby && npm install", + "outputDirectory": "public" +} diff --git a/examples/next-app/README.md b/examples/next-app/README.md index 369554e..99b4abc 100644 --- a/examples/next-app/README.md +++ b/examples/next-app/README.md @@ -2,6 +2,10 @@ A complete example of integrating privacy-focused analytics into a **Next.js 13+ App Router** application using `react-fathom`. +## Live Demo + +Visit [next-app.react-fathom.com](https://next-app.react-fathom.com) to see this example in action. + ## Why This Approach? The Next.js App Router introduces React Server Components, which require special handling for client-side analytics. This example shows the recommended pattern using `NextFathomProviderApp`, a pre-configured Client Component that works seamlessly in Server Component layouts. @@ -94,6 +98,10 @@ export default function MyComponent() { app/ ├── layout.tsx # FathomProvider setup ├── page.tsx # Home page with event tracking +├── docs/ +│ └── page.tsx # Self-documenting integration guide +├── events/ +│ └── page.tsx # Interactive event tracking demo ├── about/ │ └── page.tsx # Static page (auto pageview tracking) └── contact/ @@ -102,6 +110,6 @@ app/ ## Learn More -- [react-fathom Documentation](../../README.md) +- [react-fathom Documentation](https://react-fathom.com/docs/nextjs) - [Next.js App Router Guide](https://nextjs.org/docs/app) - [Fathom Analytics](https://usefathom.com/ref/EKONBS) diff --git a/examples/next-app/app/docs/page.tsx b/examples/next-app/app/docs/page.tsx new file mode 100644 index 0000000..6c22d84 --- /dev/null +++ b/examples/next-app/app/docs/page.tsx @@ -0,0 +1,181 @@ +export default function Docs() { + return ( +
+
+

Documentation

+

How to integrate react-fathom into your Next.js App Router application

+
+
+
+

Installation

+
+            npm install react-fathom
+          
+
+ +
+

Setup

+

+ Wrap your app with NextFathomProviderApp in your root layout: +

+
+            {`// app/layout.tsx
+import { NextFathomProviderApp } from 'react-fathom/next'
+
+export default function RootLayout({ children }) {
+  return (
+    
+      
+        
+          {children}
+        
+      
+    
+  )
+}`}
+          
+
+ +
+

Event Tracking

+

+ Use the useFathom hook to track custom events: +

+
+            {`'use client'
+
+import { useFathom } from 'react-fathom'
+
+export default function MyComponent() {
+  const { trackEvent, trackGoal } = useFathom()
+
+  return (
+    <>
+      
+      
+    
+  )
+}`}
+          
+
+ +
+

Automatic Pageview Tracking

+

+ The NextFathomProviderApp component automatically tracks pageviews + when the route changes. This is done using Next.js App Router's{' '} + usePathname and useSearchParams hooks. +

+

No additional configuration is needed for pageview tracking.

+
+ +
+

Environment Variables

+

Store your site ID in an environment variable:

+
+            {`# .env.local
+NEXT_PUBLIC_FATHOM_SITE_ID=YOUR_SITE_ID`}
+          
+

Then use it in your layout:

+
+            {`const siteId = process.env.NEXT_PUBLIC_FATHOM_SITE_ID`}
+          
+
+ +
+

Learn More

+ +
+
+
+ ) +} diff --git a/examples/next-app/app/events/page.tsx b/examples/next-app/app/events/page.tsx new file mode 100644 index 0000000..87fbafa --- /dev/null +++ b/examples/next-app/app/events/page.tsx @@ -0,0 +1,253 @@ +'use client' + +import { useFathom } from 'react-fathom' +import { useState } from 'react' + +type TrackedEvent = { + id: number + type: string + name: string + value?: number + timestamp: Date +} + +export default function Events() { + const { trackEvent, trackGoal } = useFathom() + const [events, setEvents] = useState([]) + const [eventId, setEventId] = useState(0) + + const addEvent = (type: string, name: string, value?: number) => { + setEvents((prev) => [ + { + id: eventId, + type, + name, + value, + timestamp: new Date(), + }, + ...prev, + ]) + setEventId((prev) => prev + 1) + } + + const handleTrackEvent = (name: string) => { + trackEvent(name) + addEvent('Event', name) + } + + const handleTrackGoal = (name: string, value: number) => { + trackGoal(name, value) + addEvent('Goal', name, value) + } + + return ( +
+
+

Event Tracking Demo

+

Test different event tracking methods

+
+
+

+ Click the buttons below to track different types of events. Events will be + logged below and sent to your Fathom Analytics dashboard. +

+ +
+

Custom Events

+

+ Track user interactions with custom event names +

+
+ + + +
+
+ +
+

Goal Tracking

+

+ Track conversions with optional monetary values (in cents) +

+
+ + + +
+
+ +
+

Event Log

+
+ {events.length === 0 ? ( +

+ No events tracked yet. Click a button above to get started. +

+ ) : ( +
    + {events.map((event) => ( +
  • + + {event.type} + + {event.name} + {event.value !== undefined && event.value > 0 && ( + + ${(event.value / 100).toFixed(2)} + + )} + + {event.timestamp.toLocaleTimeString()} + +
  • + ))} +
+ )} +
+
+ +
+

Code Example

+
+            {`'use client'
+
+import { useFathom } from 'react-fathom'
+
+export default function MyComponent() {
+  const { trackEvent, trackGoal } = useFathom()
+
+  return (
+    <>
+      {/* Track a custom event */}
+      
+
+      {/* Track a goal with monetary value (in cents) */}
+      
+    
+  )
+}`}
+          
+
+
+
+ ) +} diff --git a/examples/next-app/app/globals.css b/examples/next-app/app/globals.css index e49ff1c..b0727d3 100644 --- a/examples/next-app/app/globals.css +++ b/examples/next-app/app/globals.css @@ -16,7 +16,7 @@ body { } body { - color: #333; + color: #1a1a1a; background: #fff; min-height: 100vh; display: flex; @@ -28,96 +28,220 @@ a { text-decoration: none; } +a:hover { + opacity: 0.7; +} + +/* Header */ .nav { - border-bottom: 1px solid #e5e5e5; - background: #fff; - position: sticky; - top: 0; - z-index: 100; + padding: 1.5rem 0 1rem; } .nav-container { - max-width: 1200px; + max-width: 640px; margin: 0 auto; - padding: 1rem 2rem; + padding: 0 1.5rem; display: flex; justify-content: space-between; align-items: center; } +.nav-brand { + display: flex; + align-items: center; + gap: 0.75rem; +} + .nav-logo { - font-size: 1.25rem; - font-weight: 600; - color: #0070f3; + font-size: 0.875rem; + font-weight: 500; +} + +.nav-framework { + font-size: 0.875rem; + color: #666; } .nav-links { display: flex; - gap: 2rem; + gap: 1.25rem; } .nav-links a { + font-size: 0.875rem; color: #666; transition: color 0.2s; } .nav-links a:hover { - color: #0070f3; + color: #1a1a1a; + opacity: 1; } +/* Main */ .main { flex: 1; - max-width: 1200px; + max-width: 640px; width: 100%; margin: 0 auto; - padding: 3rem 2rem; + padding: 2rem 1.5rem 3rem; } +/* Page Header */ .page-header { - margin-bottom: 2rem; + margin-bottom: 2.5rem; } .page-header h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; + font-size: 2.75rem; + font-weight: 700; + line-height: 1.1; + margin-bottom: 1rem; +} + +.page-header h1 span { + display: block; + color: #666; } .page-header p { - font-size: 1.125rem; + font-size: 1rem; color: #666; + max-width: 480px; } +/* Content */ .content { - line-height: 1.75; + line-height: 1.6; } .content p { margin-bottom: 1rem; + color: #666; } .content ul { - margin-left: 2rem; - margin-bottom: 1rem; + list-style: none; + margin: 0; } .content li { - margin-bottom: 0.5rem; + margin-bottom: 0.75rem; + color: #666; +} + +.content li::before { + content: "— "; +} + +/* Section */ +.section { + border-top: 1px solid #e5e5e5; + padding-top: 2rem; + margin-top: 2.5rem; +} + +.section h2 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1.5rem; +} + +/* Buttons */ +.btn { + padding: 0.625rem 1.25rem; + border: none; + border-radius: 0.5rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + transition: opacity 0.2s; +} + +.btn:hover { + opacity: 0.8; +} + +.btn-primary { + background: #1a1a1a; + color: #fff; +} + +.btn-accent { + background: #E53935; + color: #fff; +} + +.btn-group { + display: flex; + gap: 0.75rem; + margin-top: 1.5rem; +} + +/* Feature list */ +.feature-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.feature-item h3 { + font-size: 1rem; + font-weight: 500; + margin-bottom: 0.25rem; +} + +.feature-item p { + font-size: 0.875rem; + color: #666; + margin: 0; } +/* Footer */ .footer { border-top: 1px solid #e5e5e5; - background: #f9f9f9; - padding: 2rem; - text-align: center; + padding: 1.5rem 0 2rem; +} + +.footer-container { + max-width: 640px; + margin: 0 auto; + padding: 0 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; +} + +.footer-copyright { + font-size: 0.75rem; color: #666; - font-size: 0.875rem; } -.footer a { - color: #0070f3; - text-decoration: underline; +.footer-links { + display: flex; + gap: 1rem; } -.footer a:hover { - text-decoration: none; +.footer-links a { + font-size: 0.75rem; + color: #666; +} + +.footer-links a:hover { + color: #1a1a1a; + opacity: 1; +} + +/* Responsive */ +@media (max-width: 640px) { + .page-header h1 { + font-size: 2.25rem; + } + + .footer-container { + flex-direction: column; + text-align: center; + } } diff --git a/examples/next-app/app/layout.tsx b/examples/next-app/app/layout.tsx index 0635909..26bab4d 100644 --- a/examples/next-app/app/layout.tsx +++ b/examples/next-app/app/layout.tsx @@ -1,12 +1,27 @@ import type { Metadata } from 'next' import Link from 'next/link' import { NextFathomProviderApp } from 'react-fathom/next' +import { ExampleProviderNext, ExampleLayout } from '@react-fathom/example-ui/next' import './globals.css' export const metadata: Metadata = { title: 'Next.js App Router Example - react-fathom', - description: 'Example Next.js App Router application using react-fathom', + description: + 'Example Next.js App Router application demonstrating react-fathom analytics integration', + openGraph: { + type: 'website', + title: 'Next.js App Router Example - react-fathom', + description: + 'Example Next.js App Router application demonstrating react-fathom analytics integration', + siteName: 'react-fathom examples', + }, + twitter: { + card: 'summary', + title: 'Next.js App Router Example - react-fathom', + description: + 'Example Next.js App Router application demonstrating react-fathom analytics integration', + }, } export default function RootLayout({ @@ -17,35 +32,18 @@ export default function RootLayout({ const siteId = process.env.NEXT_PUBLIC_FATHOM_SITE_ID || 'DEMO' return ( - + - - -
{children}
-
-

- This is an example Next.js App Router application demonstrating{' '} - - react-fathom - -

-
-
+ + + + {children} + + + ) diff --git a/examples/next-app/app/page.tsx b/examples/next-app/app/page.tsx index de7de8e..d13b5b2 100644 --- a/examples/next-app/app/page.tsx +++ b/examples/next-app/app/page.tsx @@ -5,83 +5,45 @@ import { useFathom } from 'react-fathom' export default function Home() { const { trackEvent, trackGoal } = useFathom() - const handleButtonClick = () => { - trackEvent('button-click') - alert('Button click tracked! Check your Fathom Analytics dashboard.') - } - - const handleGoalClick = () => { - trackGoal('demo-signup', 0) - alert('Goal tracked! Check your Fathom Analytics dashboard.') - } - return (
-

Welcome to react-fathom

-

Next.js App Router Example

-
-
-

- This is an example Next.js application using the App Router with{' '} - react-fathom for analytics tracking. -

+

+ Next.js App Router + Example +

- Navigate between pages to see automatic pageview tracking in action. - You can also use the buttons below to test event and goal tracking. + This example demonstrates how to integrate react-fathom into a Next.js + application using the App Router.

-
+
+ +
+

Try It Out

+
-

- Features Demonstrated -

-
    -
  • - Automatic pageview tracking - Pageviews are tracked - automatically when you navigate between pages -
  • -
  • - Event tracking - Use the "Track Event" - button to manually track custom events -
  • -
  • - Goal tracking - Use the "Track Goal" - button to track goal conversions -
  • -
  • - TypeScript support - Full type safety for all - tracking methods -
  • +
+ +
+

Features

+
    +
  • Automatic pageview tracking on route changes
  • +
  • Custom event tracking
  • +
  • Goal conversion tracking
  • +
  • Debug mode with event visualization
  • +
  • Privacy-focused (no cookies)
diff --git a/examples/next-app/next.config.js b/examples/next-app/next.config.js index a843cbe..42c10fd 100644 --- a/examples/next-app/next.config.js +++ b/examples/next-app/next.config.js @@ -1,5 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + output: 'export', reactStrictMode: true, } diff --git a/examples/next-app/package-lock.json b/examples/next-app/package-lock.json deleted file mode 100644 index 6f3d121..0000000 --- a/examples/next-app/package-lock.json +++ /dev/null @@ -1,1055 +0,0 @@ -{ - "name": "next-app-example", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "next-app-example", - "version": "0.1.0", - "dependencies": { - "fathom-client": "^3.6.0", - "next": "^16.1.0", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "react-fathom": "file:../.." - }, - "devDependencies": { - "@types/node": "^22.0.0", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.0", - "typescript": "^5.6.0" - } - }, - "../..": { - "version": "0.1.10", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ryanhefner" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/ryanhefner" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ryanhefner" - } - ], - "license": "MIT", - "devDependencies": { - "@babel/core": "^7.12.10", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.7", - "@babel/plugin-syntax-import-assertions": "^7.18.6", - "@babel/plugin-transform-runtime": "^7.12.10", - "@babel/preset-env": "^7.12.11", - "@babel/preset-react": "^7.23.3", - "@babel/preset-typescript": "^7.23.3", - "@eslint/js": "^9.0.0", - "@rollup/plugin-babel": "^6.0.2", - "@rollup/plugin-commonjs": "^29.0.0", - "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^16.0.3", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.1", - "@types/react": "^19.2.7", - "@typescript-eslint/eslint-plugin": "^8.50.0", - "@vitest/coverage-v8": "^4.0.16", - "babel-core": "^7.0.0-bridge.0", - "babel-plugin-dev-expression": "^0.2.2", - "coveralls": "^3.1.1", - "eslint": "^9.0.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^7.0.1", - "fathom-client": "^3.6.0", - "globals": "^16.5.0", - "jsdom": "^27.3.0", - "next": "^16.1.0", - "prettier": "^3.0.3", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "regenerator-runtime": "^0.14.0", - "rimraf": "^6.1.2", - "rollup": "^4.6.0", - "rollup-plugin-terser": "^7.0.2", - "snyk": "^1.437.3", - "typescript": "*", - "typescript-eslint": "^8.50.0", - "vitest": "^4.0.16" - }, - "peerDependencies": { - "fathom-client": ">=3.0.0", - "next": ">=10.0.0", - "react": ">=16.8", - "react-dom": ">=16.8" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - } - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@next/env": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", - "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz", - "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz", - "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz", - "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz", - "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz", - "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz", - "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz", - "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", - "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@types/node": { - "version": "22.19.3", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/fathom-client": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/fathom-client/-/fathom-client-3.7.2.tgz", - "integrity": "sha512-sWtaNivhg7uwp/q1bUuIiNj4LeQZMEZ5NXXFFpZ8le4uDedAfQG84gPOdYehtVXbl+1yX2s8lmXZ2+IQ9a/xxA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/next": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", - "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==", - "license": "MIT", - "dependencies": { - "@next/env": "16.1.1", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.1", - "@next/swc-darwin-x64": "16.1.1", - "@next/swc-linux-arm64-gnu": "16.1.1", - "@next/swc-linux-arm64-musl": "16.1.1", - "@next/swc-linux-x64-gnu": "16.1.1", - "@next/swc-linux-x64-musl": "16.1.1", - "@next/swc-win32-arm64-msvc": "16.1.1", - "@next/swc-win32-x64-msvc": "16.1.1", - "sharp": "^0.34.4" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.3" - } - }, - "node_modules/react-fathom": { - "resolved": "../..", - "link": true - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "dev": true, - "license": "MIT" - } - } -} diff --git a/examples/next-app/package.json b/examples/next-app/package.json index 7f40711..d11bac2 100644 --- a/examples/next-app/package.json +++ b/examples/next-app/package.json @@ -9,11 +9,12 @@ "lint": "next lint" }, "dependencies": { + "@react-fathom/example-ui": "file:../shared", + "fathom-client": "^3.6.0", "next": "^16.1.0", "react": "^19.2.3", "react-dom": "^19.2.3", - "react-fathom": "file:../..", - "fathom-client": "^3.6.0" + "react-fathom": "file:../.." }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/examples/next-app/vercel.json b/examples/next-app/vercel.json new file mode 100644 index 0000000..bbfec8a --- /dev/null +++ b/examples/next-app/vercel.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "nextjs", + "buildCommand": "cd ../.. && npm run build && cd examples/next-app && npm run build", + "installCommand": "cd ../.. && npm install && cd examples/shared && npm install && cd ../next-app && npm install" +} diff --git a/examples/next-pages/README.md b/examples/next-pages/README.md index c7f9d05..a5fc461 100644 --- a/examples/next-pages/README.md +++ b/examples/next-pages/README.md @@ -2,6 +2,10 @@ A complete example of integrating privacy-focused analytics into a **Next.js Pages Router** application using `react-fathom`. +## Live Demo + +Visit [next-pages.react-fathom.com](https://next-pages.react-fathom.com) to see this example in action. + ## Why This Approach? The Pages Router is the traditional Next.js routing system using the `pages/` directory. This example demonstrates the recommended pattern for adding Fathom Analytics to existing Pages Router applications or new projects that prefer this routing approach. @@ -89,6 +93,8 @@ export default function MyPage() { pages/ ├── _app.tsx # FathomProvider setup ├── index.tsx # Home page with event tracking +├── docs.tsx # Self-documenting integration guide +├── events.tsx # Interactive event tracking demo ├── about.tsx # Static page (auto pageview tracking) └── contact.tsx # Form with event tracking ``` @@ -99,6 +105,6 @@ If you're planning to migrate to the App Router, check out the [next-app example ## Learn More -- [react-fathom Documentation](../../README.md) +- [react-fathom Documentation](https://react-fathom.com/docs/nextjs) - [Next.js Pages Router Guide](https://nextjs.org/docs/pages) - [Fathom Analytics](https://usefathom.com/ref/EKONBS) diff --git a/examples/next-pages/package-lock.json b/examples/next-pages/package-lock.json deleted file mode 100644 index d062f4e..0000000 --- a/examples/next-pages/package-lock.json +++ /dev/null @@ -1,1055 +0,0 @@ -{ - "name": "next-pages-example", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "next-pages-example", - "version": "0.1.0", - "dependencies": { - "fathom-client": "^3.6.0", - "next": "^16.1.0", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "react-fathom": "file:../.." - }, - "devDependencies": { - "@types/node": "^22.0.0", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.0", - "typescript": "^5.6.0" - } - }, - "../..": { - "version": "0.1.10", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ryanhefner" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/ryanhefner" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ryanhefner" - } - ], - "license": "MIT", - "devDependencies": { - "@babel/core": "^7.12.10", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.7", - "@babel/plugin-syntax-import-assertions": "^7.18.6", - "@babel/plugin-transform-runtime": "^7.12.10", - "@babel/preset-env": "^7.12.11", - "@babel/preset-react": "^7.23.3", - "@babel/preset-typescript": "^7.23.3", - "@eslint/js": "^9.0.0", - "@rollup/plugin-babel": "^6.0.2", - "@rollup/plugin-commonjs": "^29.0.0", - "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^16.0.3", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.1", - "@types/react": "^19.2.7", - "@typescript-eslint/eslint-plugin": "^8.50.0", - "@vitest/coverage-v8": "^4.0.16", - "babel-core": "^7.0.0-bridge.0", - "babel-plugin-dev-expression": "^0.2.2", - "coveralls": "^3.1.1", - "eslint": "^9.0.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^7.0.1", - "fathom-client": "^3.6.0", - "globals": "^16.5.0", - "jsdom": "^27.3.0", - "next": "^16.1.0", - "prettier": "^3.0.3", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "regenerator-runtime": "^0.14.0", - "rimraf": "^6.1.2", - "rollup": "^4.6.0", - "rollup-plugin-terser": "^7.0.2", - "snyk": "^1.437.3", - "typescript": "*", - "typescript-eslint": "^8.50.0", - "vitest": "^4.0.16" - }, - "peerDependencies": { - "fathom-client": ">=3.0.0", - "next": ">=10.0.0", - "react": ">=16.8", - "react-dom": ">=16.8" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - } - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@next/env": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", - "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz", - "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz", - "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz", - "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz", - "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz", - "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz", - "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz", - "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", - "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@types/node": { - "version": "22.19.3", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/fathom-client": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/fathom-client/-/fathom-client-3.7.2.tgz", - "integrity": "sha512-sWtaNivhg7uwp/q1bUuIiNj4LeQZMEZ5NXXFFpZ8le4uDedAfQG84gPOdYehtVXbl+1yX2s8lmXZ2+IQ9a/xxA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/next": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", - "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==", - "license": "MIT", - "dependencies": { - "@next/env": "16.1.1", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.1", - "@next/swc-darwin-x64": "16.1.1", - "@next/swc-linux-arm64-gnu": "16.1.1", - "@next/swc-linux-arm64-musl": "16.1.1", - "@next/swc-linux-x64-gnu": "16.1.1", - "@next/swc-linux-x64-musl": "16.1.1", - "@next/swc-win32-arm64-msvc": "16.1.1", - "@next/swc-win32-x64-msvc": "16.1.1", - "sharp": "^0.34.4" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.3" - } - }, - "node_modules/react-fathom": { - "resolved": "../..", - "link": true - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "dev": true, - "license": "MIT" - } - } -} diff --git a/examples/next-pages/package.json b/examples/next-pages/package.json index ae53057..3bf3284 100644 --- a/examples/next-pages/package.json +++ b/examples/next-pages/package.json @@ -9,11 +9,12 @@ "lint": "next lint" }, "dependencies": { + "@react-fathom/example-ui": "file:../shared", + "fathom-client": "^3.6.0", "next": "^16.1.0", "react": "^19.2.3", "react-dom": "^19.2.3", - "react-fathom": "file:../..", - "fathom-client": "^3.6.0" + "react-fathom": "file:../.." }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/examples/next-pages/pages/_app.tsx b/examples/next-pages/pages/_app.tsx index 3735ec3..1116161 100644 --- a/examples/next-pages/pages/_app.tsx +++ b/examples/next-pages/pages/_app.tsx @@ -1,8 +1,10 @@ import type { AppProps } from 'next/app' +import Head from 'next/head' import Link from 'next/link' import { FathomProvider } from 'react-fathom' import { NextFathomTrackViewPages } from 'react-fathom/next' +import { ExampleProviderNext, ExampleLayout } from '@react-fathom/example-ui/next' import '../styles/globals.css' @@ -10,35 +12,40 @@ export default function App({ Component, pageProps }: AppProps) { const siteId = process.env.NEXT_PUBLIC_FATHOM_SITE_ID || 'DEMO' return ( - - - -
- -
-
-

- This is an example Next.js Pages Router application demonstrating{' '} - - react-fathom - -

-
-
+ + + + + + + + + + + + + Next.js Pages Router Example - react-fathom + + + + + + + ) } diff --git a/examples/next-pages/pages/docs.tsx b/examples/next-pages/pages/docs.tsx new file mode 100644 index 0000000..e481b74 --- /dev/null +++ b/examples/next-pages/pages/docs.tsx @@ -0,0 +1,199 @@ +export default function Docs() { + return ( +
+
+

Documentation

+

How to integrate react-fathom into your Next.js Pages Router application

+
+
+
+

Installation

+
+            npm install react-fathom
+          
+
+ +
+

Setup

+

+ Wrap your app with FathomProvider and add{' '} + NextFathomTrackViewPages in your _app.tsx: +

+
+            {`// pages/_app.tsx
+import { FathomProvider } from 'react-fathom'
+import { NextFathomTrackViewPages } from 'react-fathom/next'
+
+export default function App({ Component, pageProps }) {
+  return (
+    
+      
+      
+    
+  )
+}`}
+          
+
+ +
+

Event Tracking

+

+ Use the useFathom hook to track custom events: +

+
+            {`import { useFathom } from 'react-fathom'
+
+export default function MyComponent() {
+  const { trackEvent, trackGoal } = useFathom()
+
+  return (
+    <>
+      
+      
+    
+  )
+}`}
+          
+
+ +
+

Automatic Pageview Tracking

+

+ The NextFathomTrackViewPages component automatically tracks + pageviews when the route changes. It listens to Next.js Router events to + detect navigation. +

+

No additional configuration is needed for pageview tracking.

+
+ +
+

+ Differences from App Router +

+

The Pages Router setup differs from App Router in a few ways:

+
    +
  • + Provider: Use FathomProvider instead of{' '} + NextFathomProviderApp +
  • +
  • + Pageview tracking: Use NextFathomTrackViewPages{' '} + instead of the built-in tracking in NextFathomProviderApp +
  • +
  • + Location: Setup in pages/_app.tsx instead of{' '} + app/layout.tsx +
  • +
+
+ +
+

Environment Variables

+

Store your site ID in an environment variable:

+
+            {`# .env.local
+NEXT_PUBLIC_FATHOM_SITE_ID=YOUR_SITE_ID`}
+          
+

Then use it in your _app.tsx:

+
+            {`const siteId = process.env.NEXT_PUBLIC_FATHOM_SITE_ID`}
+          
+
+ +
+

Learn More

+ +
+
+
+ ) +} diff --git a/examples/next-pages/pages/events.tsx b/examples/next-pages/pages/events.tsx new file mode 100644 index 0000000..7e55c87 --- /dev/null +++ b/examples/next-pages/pages/events.tsx @@ -0,0 +1,249 @@ +import { useFathom } from 'react-fathom' +import { useState } from 'react' + +type TrackedEvent = { + id: number + type: string + name: string + value?: number + timestamp: Date +} + +export default function Events() { + const { trackEvent, trackGoal } = useFathom() + const [events, setEvents] = useState([]) + const [eventId, setEventId] = useState(0) + + const addEvent = (type: string, name: string, value?: number) => { + setEvents((prev) => [ + { + id: eventId, + type, + name, + value, + timestamp: new Date(), + }, + ...prev, + ]) + setEventId((prev) => prev + 1) + } + + const handleTrackEvent = (name: string) => { + trackEvent(name) + addEvent('Event', name) + } + + const handleTrackGoal = (name: string, value: number) => { + trackGoal(name, value) + addEvent('Goal', name, value) + } + + return ( +
+
+

Event Tracking Demo

+

Test different event tracking methods

+
+
+

+ Click the buttons below to track different types of events. Events will be + logged below and sent to your Fathom Analytics dashboard. +

+ +
+

Custom Events

+

+ Track user interactions with custom event names +

+
+ + + +
+
+ +
+

Goal Tracking

+

+ Track conversions with optional monetary values (in cents) +

+
+ + + +
+
+ +
+

Event Log

+
+ {events.length === 0 ? ( +

+ No events tracked yet. Click a button above to get started. +

+ ) : ( +
    + {events.map((event) => ( +
  • + + {event.type} + + {event.name} + {event.value !== undefined && event.value > 0 && ( + + ${(event.value / 100).toFixed(2)} + + )} + + {event.timestamp.toLocaleTimeString()} + +
  • + ))} +
+ )} +
+
+ +
+

Code Example

+
+            {`import { useFathom } from 'react-fathom'
+
+export default function MyComponent() {
+  const { trackEvent, trackGoal } = useFathom()
+
+  return (
+    <>
+      {/* Track a custom event */}
+      
+
+      {/* Track a goal with monetary value (in cents) */}
+      
+    
+  )
+}`}
+          
+
+
+
+ ) +} diff --git a/examples/next-pages/pages/index.tsx b/examples/next-pages/pages/index.tsx index dc1acad..042e630 100644 --- a/examples/next-pages/pages/index.tsx +++ b/examples/next-pages/pages/index.tsx @@ -3,83 +3,45 @@ import { useFathom } from 'react-fathom' export default function Home() { const { trackEvent, trackGoal } = useFathom() - const handleButtonClick = () => { - trackEvent('button-click') - alert('Button click tracked! Check your Fathom Analytics dashboard.') - } - - const handleGoalClick = () => { - trackGoal('demo-signup', 0) - alert('Goal tracked! Check your Fathom Analytics dashboard.') - } - return (
-

Welcome to react-fathom

-

Next.js Pages Router Example

-
-
-

- This is an example Next.js application using the Pages Router with{' '} - react-fathom for analytics tracking. -

+

+ Next.js Pages Router + Example +

- Navigate between pages to see automatic pageview tracking in action. - You can also use the buttons below to test event and goal tracking. + This example demonstrates how to integrate react-fathom into a Next.js + application using the Pages Router.

-
+
+ +
+

Try It Out

+
-

- Features Demonstrated -

-
    -
  • - Automatic pageview tracking - Pageviews are tracked - automatically when you navigate between pages -
  • -
  • - Event tracking - Use the "Track Event" - button to manually track custom events -
  • -
  • - Goal tracking - Use the "Track Goal" - button to track goal conversions -
  • -
  • - TypeScript support - Full type safety for all - tracking methods -
  • +
+ +
+

Features

+
    +
  • Automatic pageview tracking on route changes
  • +
  • Custom event tracking
  • +
  • Goal conversion tracking
  • +
  • Debug mode with event visualization
  • +
  • Privacy-focused (no cookies)
diff --git a/examples/next-pages/styles/globals.css b/examples/next-pages/styles/globals.css index e49ff1c..b0727d3 100644 --- a/examples/next-pages/styles/globals.css +++ b/examples/next-pages/styles/globals.css @@ -16,7 +16,7 @@ body { } body { - color: #333; + color: #1a1a1a; background: #fff; min-height: 100vh; display: flex; @@ -28,96 +28,220 @@ a { text-decoration: none; } +a:hover { + opacity: 0.7; +} + +/* Header */ .nav { - border-bottom: 1px solid #e5e5e5; - background: #fff; - position: sticky; - top: 0; - z-index: 100; + padding: 1.5rem 0 1rem; } .nav-container { - max-width: 1200px; + max-width: 640px; margin: 0 auto; - padding: 1rem 2rem; + padding: 0 1.5rem; display: flex; justify-content: space-between; align-items: center; } +.nav-brand { + display: flex; + align-items: center; + gap: 0.75rem; +} + .nav-logo { - font-size: 1.25rem; - font-weight: 600; - color: #0070f3; + font-size: 0.875rem; + font-weight: 500; +} + +.nav-framework { + font-size: 0.875rem; + color: #666; } .nav-links { display: flex; - gap: 2rem; + gap: 1.25rem; } .nav-links a { + font-size: 0.875rem; color: #666; transition: color 0.2s; } .nav-links a:hover { - color: #0070f3; + color: #1a1a1a; + opacity: 1; } +/* Main */ .main { flex: 1; - max-width: 1200px; + max-width: 640px; width: 100%; margin: 0 auto; - padding: 3rem 2rem; + padding: 2rem 1.5rem 3rem; } +/* Page Header */ .page-header { - margin-bottom: 2rem; + margin-bottom: 2.5rem; } .page-header h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; + font-size: 2.75rem; + font-weight: 700; + line-height: 1.1; + margin-bottom: 1rem; +} + +.page-header h1 span { + display: block; + color: #666; } .page-header p { - font-size: 1.125rem; + font-size: 1rem; color: #666; + max-width: 480px; } +/* Content */ .content { - line-height: 1.75; + line-height: 1.6; } .content p { margin-bottom: 1rem; + color: #666; } .content ul { - margin-left: 2rem; - margin-bottom: 1rem; + list-style: none; + margin: 0; } .content li { - margin-bottom: 0.5rem; + margin-bottom: 0.75rem; + color: #666; +} + +.content li::before { + content: "— "; +} + +/* Section */ +.section { + border-top: 1px solid #e5e5e5; + padding-top: 2rem; + margin-top: 2.5rem; +} + +.section h2 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1.5rem; +} + +/* Buttons */ +.btn { + padding: 0.625rem 1.25rem; + border: none; + border-radius: 0.5rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + transition: opacity 0.2s; +} + +.btn:hover { + opacity: 0.8; +} + +.btn-primary { + background: #1a1a1a; + color: #fff; +} + +.btn-accent { + background: #E53935; + color: #fff; +} + +.btn-group { + display: flex; + gap: 0.75rem; + margin-top: 1.5rem; +} + +/* Feature list */ +.feature-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.feature-item h3 { + font-size: 1rem; + font-weight: 500; + margin-bottom: 0.25rem; +} + +.feature-item p { + font-size: 0.875rem; + color: #666; + margin: 0; } +/* Footer */ .footer { border-top: 1px solid #e5e5e5; - background: #f9f9f9; - padding: 2rem; - text-align: center; + padding: 1.5rem 0 2rem; +} + +.footer-container { + max-width: 640px; + margin: 0 auto; + padding: 0 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; +} + +.footer-copyright { + font-size: 0.75rem; color: #666; - font-size: 0.875rem; } -.footer a { - color: #0070f3; - text-decoration: underline; +.footer-links { + display: flex; + gap: 1rem; } -.footer a:hover { - text-decoration: none; +.footer-links a { + font-size: 0.75rem; + color: #666; +} + +.footer-links a:hover { + color: #1a1a1a; + opacity: 1; +} + +/* Responsive */ +@media (max-width: 640px) { + .page-header h1 { + font-size: 2.25rem; + } + + .footer-container { + flex-direction: column; + text-align: center; + } } diff --git a/examples/next-pages/vercel.json b/examples/next-pages/vercel.json new file mode 100644 index 0000000..24ab35b --- /dev/null +++ b/examples/next-pages/vercel.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "nextjs", + "buildCommand": "cd ../.. && npm run build && cd examples/next-pages && npm run build", + "installCommand": "cd ../.. && npm install && cd examples/shared && npm install && cd ../next-pages && npm install" +} diff --git a/examples/react-native/.gitignore b/examples/react-native/.gitignore new file mode 100644 index 0000000..2894da4 --- /dev/null +++ b/examples/react-native/.gitignore @@ -0,0 +1,12 @@ +node_modules +.expo +dist +.env +.env.local +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ diff --git a/examples/react-native/README.md b/examples/react-native/README.md new file mode 100644 index 0000000..28b2f38 --- /dev/null +++ b/examples/react-native/README.md @@ -0,0 +1,102 @@ +# react-fathom React Native Example + +A demonstration of integrating react-fathom into a React Native application using Expo. + +## Live Demo + +Visit [native.react-fathom.com](https://native.react-fathom.com) to see the web version of this example. + +## Features + +- **Offline Event Queue** — Events are queued when offline and sent when connected +- **Navigation Tracking** — Automatic screen tracking with Expo Router +- **Custom Event Tracking** — Track user interactions with `useFathom` hook +- **Revenue Tracking** — Track events with monetary values + +## Getting Started + +1. Clone the repository: + ```bash + git clone https://github.com/ryanhefner/react-fathom.git + cd react-fathom/examples/react-native + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Create a `.env` file with your Fathom site ID: + ```bash + EXPO_PUBLIC_FATHOM_SITE_ID=YOUR_SITE_ID + ``` + +4. Start the development server: + ```bash + npm start + ``` + +5. Run on your device or simulator: + - Press `i` for iOS simulator + - Press `a` for Android emulator + - Scan QR code with Expo Go app + +## Project Structure + +``` +app/ +├── _layout.tsx # Root layout with FathomProvider +├── index.tsx # Home screen +├── about.tsx # About screen +├── docs.tsx # Documentation screen +└── events.tsx # Event tracking demo +``` + +## React Native Specific Features + +### Offline Support + +Events are automatically queued when the device is offline: + +```tsx +// Events track normally - they'll be queued if offline +trackEvent('user-action') +``` + +### Navigation Tracking + +Track screen views with React Navigation: + +```tsx +import { useNavigationTracking } from 'react-fathom/native' + +function App() { + const navigationRef = useNavigationContainerRef() + useNavigationTracking(navigationRef) + + return ... +} +``` + +### App State Tracking + +Track when the app enters foreground/background: + +```tsx +import { useAppStateTracking } from 'react-fathom/native' + +function App() { + useAppStateTracking({ + foregroundEventName: 'app-opened', + backgroundEventName: 'app-closed', + }) + + return +} +``` + +## Learn More + +- [react-fathom Documentation](https://react-fathom.com/docs/react-native) +- [Fathom Analytics](https://usefathom.com/ref/EKONBS) +- [Expo Documentation](https://docs.expo.dev) diff --git a/examples/react-native/app.json b/examples/react-native/app.json new file mode 100644 index 0000000..7583c51 --- /dev/null +++ b/examples/react-native/app.json @@ -0,0 +1,24 @@ +{ + "expo": { + "name": "react-fathom-example", + "slug": "react-fathom-example", + "version": "1.0.0", + "orientation": "portrait", + "scheme": "reactfathom", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "backgroundColor": "#ffffff" + } + }, + "web": { + "bundler": "metro", + "output": "static" + }, + "plugins": ["expo-router"] + } +} diff --git a/examples/react-native/app/_layout.tsx b/examples/react-native/app/_layout.tsx new file mode 100644 index 0000000..440d34c --- /dev/null +++ b/examples/react-native/app/_layout.tsx @@ -0,0 +1,42 @@ +import { Stack } from 'expo-router' +import { StatusBar } from 'expo-status-bar' +import { FathomProvider } from 'react-fathom/native' + +const SITE_ID = process.env.EXPO_PUBLIC_FATHOM_SITE_ID + +export default function RootLayout() { + return ( + <> + + {SITE_ID ? ( + + + + + + + + + ) : ( + + + + + + + )} + + ) +} diff --git a/examples/react-native/app/about.tsx b/examples/react-native/app/about.tsx new file mode 100644 index 0000000..5c55bbc --- /dev/null +++ b/examples/react-native/app/about.tsx @@ -0,0 +1,101 @@ +import { View, Text, StyleSheet, ScrollView, Linking, TouchableOpacity } from 'react-native' + +export default function About() { + return ( + + + About This Example + + This is a demonstration of react-fathom integration in a React Native + application using Expo. + + + + + Tech Stack + + • Expo — React Native framework + • Expo Router — File-based routing + • React Native — Mobile UI + • react-fathom — Privacy-focused analytics + + + + + Why Fathom Analytics? + + Fathom Analytics is a privacy-focused alternative to traditional analytics + that respects user privacy while still providing valuable insights. + + + • No cookies required — GDPR compliant + • No personal data collection + • Simple, actionable dashboard + • Fast and lightweight + + + + + React Native Features + + react-fathom provides special features for React Native: + + + • Offline event queue — events sent when online + • Navigation tracking — automatic screen tracking + • App state tracking — foreground/background events + • Hidden WebView — no visible UI component + + + + + Linking.openURL('https://usefathom.com/ref/EKONBS')} + > + Try Fathom Analytics (Get $10 credit) → + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + section: { + padding: 24, + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + title: { + fontSize: 24, + fontWeight: 'bold', + marginBottom: 8, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + marginBottom: 12, + }, + description: { + fontSize: 14, + color: '#666', + marginBottom: 16, + lineHeight: 20, + }, + list: { + gap: 8, + }, + listItem: { + fontSize: 14, + color: '#333', + lineHeight: 20, + }, + link: { + fontSize: 16, + color: '#3b82f6', + fontWeight: '500', + }, +}) diff --git a/examples/react-native/app/docs.tsx b/examples/react-native/app/docs.tsx new file mode 100644 index 0000000..849f0ab --- /dev/null +++ b/examples/react-native/app/docs.tsx @@ -0,0 +1,180 @@ +import { View, Text, StyleSheet, ScrollView, Linking, TouchableOpacity } from 'react-native' + +export default function Docs() { + return ( + + + Documentation + + How to integrate react-fathom into your React Native application. + + + + {/* Installation */} + + Installation + + + npm install react-fathom react-native-webview + + + + + {/* Setup */} + + Setup + + Wrap your app with FathomProvider from react-fathom/native: + + + + {`import { FathomProvider } from 'react-fathom/native' + +function App() { + return ( + + + + ) +}`} + + + + + {/* Event Tracking */} + + Event Tracking + + Use the useFathom hook to track events: + + + + {`import { useFathom } from 'react-fathom/native' + +function MyComponent() { + const { trackEvent } = useFathom() + + return ( + + ) +}`} + + + + {/* Declarative Tracking */} + + + Declarative Tracking + + + Use {''} for declarative event tracking: + + +{`import { TrackClick } from 'react-fathom' + +function CTAButton() { + return ( + + + + ) +}`} + + + + {/* Environment Variables */} + + + Environment Variables + + + Store your site ID in an environment variable: + + + # .env + VITE_FATHOM_SITE_ID=YOUR_SITE_ID + + +{` + +`} + + + + {/* More Info */} + + + Learn More + + + + Full Documentation → + + + API Reference → + + + GitHub Repository → + + + + + ) +} diff --git a/examples/react/src/pages/Events.tsx b/examples/react/src/pages/Events.tsx new file mode 100644 index 0000000..8907ddd --- /dev/null +++ b/examples/react/src/pages/Events.tsx @@ -0,0 +1,150 @@ +import { Box, Button, Heading, Text, VStack, HStack, Code, Input } from '@chakra-ui/react' +import { useState } from 'react' +import { useFathom, TrackClick } from 'react-fathom' + +export function Events() { + const { trackEvent } = useFathom() + const [customEventName, setCustomEventName] = useState('custom-event') + const [eventCount, setEventCount] = useState(0) + + const handleTrackEvent = (eventName: string) => { + trackEvent(eventName) + setEventCount((c) => c + 1) + } + + return ( + + + + Event Tracking Demo + + + This page demonstrates different ways to track custom events with react-fathom. + + + Events tracked this session: {eventCount} + + + + {/* useFathom Hook */} + + + Using the useFathom Hook + + + Call trackEvent() imperatively from any component. + + + + + + + const + {'{ '}trackEvent{' }'} = + useFathom + () + trackEvent('button-click') + + + + {/* TrackClick Component */} + + + Using the {''} Component + + + Wrap any clickable element to track clicks declaratively. + + + setEventCount((c) => c + 1)}> + + + setEventCount((c) => c + 1)}> + + + + + {'<'}TrackClick + eventName="cta-click" + {'>'} + {'<'}Button{'>'}Click Me{'Button{'>'} + {'TrackClick{'>'} + + + + {/* Custom Event */} + + + Custom Event Name + + + Track any event name you want. + + + setCustomEventName(e.target.value)} + placeholder="Event name" + maxW="200px" + /> + + + + + {/* Event with Value */} + + + Events with Monetary Value + + + Track events with an associated value (in cents) for revenue tracking. + + + + + + + trackEvent( + 'purchase', + {'{ '}_value: 1999{' }'} + ) + + + + ) +} diff --git a/examples/react/src/pages/Home.tsx b/examples/react/src/pages/Home.tsx new file mode 100644 index 0000000..9c2e940 --- /dev/null +++ b/examples/react/src/pages/Home.tsx @@ -0,0 +1,73 @@ +import { Box, Heading, Text, VStack, Link, Code } from '@chakra-ui/react' +import { Link as RouterLink } from 'react-router-dom' + +export function Home() { + return ( + + + + React + Vite + + Example + + + + This example demonstrates how to integrate react-fathom into a standard React application + using Vite and React Router. + + + + + + Features + + + + Automatic Pageview Tracking + + Navigate between pages to see pageviews tracked automatically via React Router integration. + + + + Custom Event Tracking + + Visit the Events Demo page + to see custom event tracking in action. + + + + useFathom Hook + + Access trackEvent and trackPageview from anywhere in your app. + + + + + + + + Quick Setup + + + import + {'{ '} + FathomProvider + {' }'} + from + 'react-fathom' + {'<'}FathomProvider + siteId="YOUR_SITE_ID" + {'>'} + {'<'}App /{'>'} + {'FathomProvider{'>'} + + + + + + Read the full documentation → + + + + ) +} diff --git a/examples/react/tsconfig.json b/examples/react/tsconfig.json new file mode 100644 index 0000000..a4c834a --- /dev/null +++ b/examples/react/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/vercel.json b/examples/react/vercel.json new file mode 100644 index 0000000..0aa5c4f --- /dev/null +++ b/examples/react/vercel.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "vite", + "buildCommand": "cd ../.. && npm run build && cd examples/react && npm run build", + "installCommand": "cd ../.. && npm install && cd examples/shared && npm install && cd ../react && npm install", + "outputDirectory": "dist" +} diff --git a/examples/react/vite.config.ts b/examples/react/vite.config.ts new file mode 100644 index 0000000..9ffcc67 --- /dev/null +++ b/examples/react/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], +}) diff --git a/examples/shared/package.json b/examples/shared/package.json new file mode 100644 index 0000000..0d272d9 --- /dev/null +++ b/examples/shared/package.json @@ -0,0 +1,21 @@ +{ + "name": "@react-fathom/example-ui", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./next": "./src/next.ts" + }, + "dependencies": { + "@chakra-ui/react": "^3.2.0", + "@emotion/react": "^11.13.0", + "next-themes": "^0.4.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } +} diff --git a/examples/shared/src/ColorModeButton.tsx b/examples/shared/src/ColorModeButton.tsx new file mode 100644 index 0000000..8f46502 --- /dev/null +++ b/examples/shared/src/ColorModeButton.tsx @@ -0,0 +1,44 @@ +'use client' + +import { IconButton } from '@chakra-ui/react' +import { useTheme } from 'next-themes' +import { useEffect, useState } from 'react' + +/** + * A button that toggles between light and dark mode. + * Uses next-themes under the hood. + */ +export function ColorModeButton() { + const { theme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return ( + + 🌙 + + ) + } + + const isDark = theme === 'dark' + + return ( + setTheme(isDark ? 'light' : 'dark')} + > + {isDark ? '☀️' : '🌙'} + + ) +} diff --git a/examples/shared/src/ColorModeButtonSimple.tsx b/examples/shared/src/ColorModeButtonSimple.tsx new file mode 100644 index 0000000..59384cf --- /dev/null +++ b/examples/shared/src/ColorModeButtonSimple.tsx @@ -0,0 +1,23 @@ +'use client' + +import { IconButton } from '@chakra-ui/react' +import { useColorMode } from './ColorModeContext' + +/** + * A button that toggles between light and dark mode. + * Uses the simple ColorModeContext (for non-Next.js apps). + */ +export function ColorModeButtonSimple() { + const { colorMode, toggleColorMode } = useColorMode() + + return ( + + {colorMode === 'dark' ? '☀️' : '🌙'} + + ) +} diff --git a/examples/shared/src/ColorModeContext.tsx b/examples/shared/src/ColorModeContext.tsx new file mode 100644 index 0000000..4aaf7c6 --- /dev/null +++ b/examples/shared/src/ColorModeContext.tsx @@ -0,0 +1,75 @@ +'use client' + +import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' + +type ColorMode = 'light' | 'dark' + +interface ColorModeContextValue { + colorMode: ColorMode + toggleColorMode: () => void +} + +const ColorModeContext = createContext(null) + +const STORAGE_KEY = 'react-fathom-example-color-mode' + +function getInitialColorMode(): ColorMode { + if (typeof window === 'undefined') return 'light' + + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored === 'dark' || stored === 'light') { + return stored + } + } catch { + // localStorage unavailable + } + + // Check system preference + if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) { + return 'dark' + } + + return 'light' +} + +export function ColorModeProvider({ children }: { children: ReactNode }) { + const [colorMode, setColorMode] = useState('light') + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + setColorMode(getInitialColorMode()) + }, []) + + useEffect(() => { + if (!mounted) return + + document.documentElement.classList.remove('light', 'dark') + document.documentElement.classList.add(colorMode) + + try { + localStorage.setItem(STORAGE_KEY, colorMode) + } catch { + // localStorage unavailable + } + }, [colorMode, mounted]) + + const toggleColorMode = () => { + setColorMode((prev) => (prev === 'light' ? 'dark' : 'light')) + } + + return ( + + {children} + + ) +} + +export function useColorMode() { + const context = useContext(ColorModeContext) + if (!context) { + throw new Error('useColorMode must be used within a ColorModeProvider') + } + return context +} diff --git a/examples/shared/src/EventStreamPanel.tsx b/examples/shared/src/EventStreamPanel.tsx new file mode 100644 index 0000000..5d797cc --- /dev/null +++ b/examples/shared/src/EventStreamPanel.tsx @@ -0,0 +1,246 @@ +'use client' + +import { + Box, + Button, + Flex, + Heading, + HStack, + IconButton, + Text, + VStack, +} from '@chakra-ui/react' +import { useDebugSubscription, type DebugEvent } from 'react-fathom/debug' +import { useState, useEffect } from 'react' + +const STORAGE_KEY = 'react-fathom-event-stream-visible' + +function formatTime(timestamp: number): string { + return new Date(timestamp).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) +} + +interface EventCardProps { + event: DebugEvent +} + +function EventCard({ event }: EventCardProps) { + const colors = { + pageview: 'blue.500', + event: 'purple.500', + goal: 'green.500', + } + + let title = '' + let subtitle = '' + + switch (event.type) { + case 'pageview': + title = 'Pageview' + subtitle = event.url || (typeof window !== 'undefined' ? window.location.pathname : '') + break + case 'event': + title = 'Event' + subtitle = event.eventName || '' + break + case 'goal': + title = 'Goal' + subtitle = `${event.goalCode} ($${((event.goalCents || 0) / 100).toFixed(2)})` + break + } + + return ( + + + + {title} + + + {formatTime(event.timestamp)} + + + + {subtitle} + + + ) +} + +export interface EventStreamPanelProps { + /** + * Maximum number of events to display + * @default 20 + */ + maxEvents?: number + /** + * Position of the toggle button + * @default 'bottom-right' + */ + position?: 'bottom-right' | 'bottom-left' + /** + * Initial visibility state (overridden by localStorage if available) + * @default false + */ + defaultVisible?: boolean +} + +/** + * A Chakra UI styled debug panel that displays tracked events in real-time. + * Only renders when debug mode is enabled in the FathomProvider. + */ +export function EventStreamPanel({ + maxEvents = 20, + position = 'bottom-right', + defaultVisible = false, +}: EventStreamPanelProps = {}) { + const [isVisible, setIsVisible] = useState(defaultVisible) + const [isHydrated, setIsHydrated] = useState(false) + const { events, debugEnabled, clearEvents } = useDebugSubscription({ maxEvents }) + + useEffect(() => { + setIsHydrated(true) + if (typeof window !== 'undefined') { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored !== null) { + setIsVisible(stored === 'true') + } + } catch { + // localStorage unavailable + } + } + }, []) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === '.') { + e.preventDefault() + setIsVisible((prev) => !prev) + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, []) + + useEffect(() => { + if (isHydrated && typeof window !== 'undefined') { + try { + localStorage.setItem(STORAGE_KEY, String(isVisible)) + } catch { + // localStorage unavailable + } + } + }, [isVisible, isHydrated]) + + if (!debugEnabled || !isHydrated) return null + + const isRight = position === 'bottom-right' + + return ( + <> + {/* Toggle Button */} + setIsVisible(!isVisible)} + position="fixed" + bottom={4} + {...(isRight ? { right: 4 } : { left: 4 })} + zIndex={1000} + rounded="full" + size="lg" + colorPalette="purple" + boxShadow="lg" + > + {isVisible ? '✕' : '📊'} + + + {/* Panel */} + {isVisible && ( + + {/* Header */} + + 📊 Event Stream + + + + {/* Event List */} + + {events.length === 0 ? ( + + 🔍 + + No events yet. +
+ Navigate or interact to see tracking events. +
+
+ ) : ( + events.map((event) => ) + )} +
+ + {/* Footer */} + + + {events.length} event{events.length !== 1 ? 's' : ''} + + + ⌘. + + to toggle + + +
+ )} + + ) +} diff --git a/examples/shared/src/ExampleLayout.tsx b/examples/shared/src/ExampleLayout.tsx new file mode 100644 index 0000000..abb6015 --- /dev/null +++ b/examples/shared/src/ExampleLayout.tsx @@ -0,0 +1,163 @@ +'use client' + +import { Box, Container, Flex, HStack, Link, Text } from '@chakra-ui/react' +import type { ComponentType, ReactNode } from 'react' +import { ColorModeButton } from './ColorModeButton' +import { EventStreamPanel } from './EventStreamPanel' + +export interface NavLink { + href: string + label: string +} + +export interface ExampleLayoutProps { + children: ReactNode + /** + * The Link component to use for navigation. + * Pass Next.js Link or React Router Link component. + */ + linkComponent: ComponentType<{ href: string; children: ReactNode; className?: string }> + /** + * Navigation links to display in the header. + */ + navLinks?: NavLink[] + /** + * The title/brand shown in the header. + * @default 'react-fathom' + */ + title?: string + /** + * Show the color mode toggle button. + * @default true + */ + showColorModeButton?: boolean + /** + * Show the debug EventStream panel. + * @default true + */ + showEventStream?: boolean + /** + * The framework name shown in the footer. + */ + frameworkName?: string +} + +const defaultNavLinks: NavLink[] = [ + { href: '/', label: 'Home' }, + { href: '/about', label: 'About' }, + { href: '/events', label: 'Events' }, + { href: '/contact', label: 'Contact' }, +] + +/** + * Shared layout component for example sites. + * Minimal, content-focused design. + */ +export function ExampleLayout({ + children, + linkComponent: LinkComponent, + navLinks = defaultNavLinks, + title = 'react-fathom', + showColorModeButton = true, + showEventStream = true, + frameworkName, +}: ExampleLayoutProps) { + return ( + <> + + {/* Header */} + + + + + + {title} + + {frameworkName && ( + + — {frameworkName} + + )} + + + + {navLinks.map((link) => ( + + {link.label} + + ))} + {showColorModeButton && } + + + + + + {/* Main Content */} + + + {children} + + + + {/* Footer */} + + + + + © {new Date().getFullYear()} —{' '} + + react-fathom + + + + + Docs + + + GitHub + + + Fathom + + + + + + + {showEventStream && } + + ) +} diff --git a/examples/shared/src/ExampleLayoutSimple.tsx b/examples/shared/src/ExampleLayoutSimple.tsx new file mode 100644 index 0000000..9b12ba4 --- /dev/null +++ b/examples/shared/src/ExampleLayoutSimple.tsx @@ -0,0 +1,163 @@ +'use client' + +import { Box, Container, Flex, HStack, Link, Text } from '@chakra-ui/react' +import { EventStreamPanel } from './EventStreamPanel' +import type { ComponentType, ReactNode } from 'react' +import { ColorModeButtonSimple } from './ColorModeButtonSimple' + +export interface NavLink { + href: string + label: string +} + +export interface ExampleLayoutSimpleProps { + children: ReactNode + /** + * The Link component to use for navigation. + * Pass React Router's Link component. + */ + linkComponent: ComponentType<{ to: string; children: ReactNode; className?: string }> + /** + * Navigation links to display in the header. + */ + navLinks?: NavLink[] + /** + * The title/brand shown in the header. + * @default 'react-fathom' + */ + title?: string + /** + * Show the color mode toggle button. + * @default true + */ + showColorModeButton?: boolean + /** + * Show the debug EventStream panel. + * @default true + */ + showEventStream?: boolean + /** + * The framework name shown in the footer. + */ + frameworkName?: string +} + +const defaultNavLinks: NavLink[] = [ + { href: '/', label: 'Home' }, + { href: '/about', label: 'About' }, + { href: '/events', label: 'Events' }, + { href: '/contact', label: 'Contact' }, +] + +/** + * Shared layout component for example sites using React Router. + * Minimal, content-focused design. + */ +export function ExampleLayoutSimple({ + children, + linkComponent: LinkComponent, + navLinks = defaultNavLinks, + title = 'react-fathom', + showColorModeButton = true, + showEventStream = true, + frameworkName, +}: ExampleLayoutSimpleProps) { + return ( + <> + + {/* Header */} + + + + + + {title} + + {frameworkName && ( + + — {frameworkName} + + )} + + + + {navLinks.map((link) => ( + + {link.label} + + ))} + {showColorModeButton && } + + + + + + {/* Main Content */} + + + {children} + + + + {/* Footer */} + + + + + © {new Date().getFullYear()} —{' '} + + react-fathom + + + + + Docs + + + GitHub + + + Fathom + + + + + + + {showEventStream && } + + ) +} diff --git a/examples/shared/src/ExampleProvider.tsx b/examples/shared/src/ExampleProvider.tsx new file mode 100644 index 0000000..ae3b535 --- /dev/null +++ b/examples/shared/src/ExampleProvider.tsx @@ -0,0 +1,22 @@ +'use client' + +import { ChakraProvider, defaultSystem } from '@chakra-ui/react' +import type { ReactNode } from 'react' +import { ColorModeProvider } from './ColorModeContext' + +export interface ExampleProviderProps { + children: ReactNode +} + +/** + * Provider for non-Next.js example sites (React, Vite, etc.). + * Wraps children with Chakra UI and simple color mode support. + * For Next.js apps, use ExampleProviderNext which uses next-themes. + */ +export function ExampleProvider({ children }: ExampleProviderProps) { + return ( + + {children} + + ) +} diff --git a/examples/shared/src/ExampleProviderNext.tsx b/examples/shared/src/ExampleProviderNext.tsx new file mode 100644 index 0000000..f418c60 --- /dev/null +++ b/examples/shared/src/ExampleProviderNext.tsx @@ -0,0 +1,23 @@ +'use client' + +import { ChakraProvider, defaultSystem } from '@chakra-ui/react' +import { ThemeProvider } from 'next-themes' +import type { ReactNode } from 'react' + +export interface ExampleProviderNextProps { + children: ReactNode +} + +/** + * Provider for Next.js example sites. + * Wraps children with Chakra UI and next-themes for color mode support. + */ +export function ExampleProviderNext({ children }: ExampleProviderNextProps) { + return ( + + + {children} + + + ) +} diff --git a/examples/shared/src/index.ts b/examples/shared/src/index.ts new file mode 100644 index 0000000..2330f9a --- /dev/null +++ b/examples/shared/src/index.ts @@ -0,0 +1,11 @@ +export { ExampleProvider } from './ExampleProvider' +export type { ExampleProviderProps } from './ExampleProvider' + +export { ExampleLayout } from './ExampleLayout' +export type { ExampleLayoutProps, NavLink } from './ExampleLayout' + +export { ExampleLayoutSimple } from './ExampleLayoutSimple' +export type { ExampleLayoutSimpleProps } from './ExampleLayoutSimple' + +export { ColorModeProvider, useColorMode } from './ColorModeContext' +export { ColorModeButtonSimple } from './ColorModeButtonSimple' diff --git a/examples/shared/src/next.ts b/examples/shared/src/next.ts new file mode 100644 index 0000000..35fe082 --- /dev/null +++ b/examples/shared/src/next.ts @@ -0,0 +1,7 @@ +export { ExampleProviderNext } from './ExampleProviderNext' +export type { ExampleProviderNextProps } from './ExampleProviderNext' + +export { ColorModeButton } from './ColorModeButton' + +export { ExampleLayout } from './ExampleLayout' +export type { ExampleLayoutProps, NavLink } from './ExampleLayout' diff --git a/examples/tanstack-router/.env.example b/examples/tanstack-router/.env.example new file mode 100644 index 0000000..bc5d2ad --- /dev/null +++ b/examples/tanstack-router/.env.example @@ -0,0 +1,5 @@ +# Get your site ID from https://usefathom.com +VITE_FATHOM_SITE_ID=YOUR_SITE_ID + +# Optional: Enable debug mode (enabled by default in this example) +# VITE_FATHOM_DEBUG=true diff --git a/examples/tanstack-router/.gitignore b/examples/tanstack-router/.gitignore new file mode 100644 index 0000000..9c97bbd --- /dev/null +++ b/examples/tanstack-router/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.env diff --git a/examples/tanstack-router/index.html b/examples/tanstack-router/index.html new file mode 100644 index 0000000..e0294cb --- /dev/null +++ b/examples/tanstack-router/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + TanStack Router Example - react-fathom + + +
+ + + diff --git a/examples/tanstack-router/package.json b/examples/tanstack-router/package.json new file mode 100644 index 0000000..d2ed1c4 --- /dev/null +++ b/examples/tanstack-router/package.json @@ -0,0 +1,27 @@ +{ + "name": "react-fathom-tanstack-router-example", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@react-fathom/example-ui": "file:../shared", + "@tanstack/react-router": "^1.120.3", + "fathom-client": "^3.6.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-fathom": "file:../.." + }, + "devDependencies": { + "@tanstack/router-plugin": "^1.120.3", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/examples/tanstack-router/src/main.tsx b/examples/tanstack-router/src/main.tsx new file mode 100644 index 0000000..ecc7b67 --- /dev/null +++ b/examples/tanstack-router/src/main.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { ExampleProvider } from '@react-fathom/example-ui' + +import { routeTree } from './routeTree.gen' + +// Create a new router instance +const router = createRouter({ routeTree }) + +// Register the router instance for type safety +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + +) diff --git a/examples/tanstack-router/src/routes/__root.tsx b/examples/tanstack-router/src/routes/__root.tsx new file mode 100644 index 0000000..5fda3b5 --- /dev/null +++ b/examples/tanstack-router/src/routes/__root.tsx @@ -0,0 +1,25 @@ +import { createRootRoute, Link, Outlet } from '@tanstack/react-router' +import { FathomProvider } from 'react-fathom' +import { TanStackRouterFathomTrackView } from 'react-fathom/tanstack-router' +import { ExampleLayoutSimple } from '@react-fathom/example-ui' + +const siteId = import.meta.env.VITE_FATHOM_SITE_ID || 'DEMO' + +// Custom Link component for ExampleLayoutSimple +function NavLink({ to, children }: { to: string; children: React.ReactNode }) { + return {children} +} + +export const Route = createRootRoute({ + component: () => ( + + + + + + + ), +}) diff --git a/examples/tanstack-router/src/routes/about.tsx b/examples/tanstack-router/src/routes/about.tsx new file mode 100644 index 0000000..23dac33 --- /dev/null +++ b/examples/tanstack-router/src/routes/about.tsx @@ -0,0 +1,37 @@ +import { createFileRoute } from '@tanstack/react-router' +import { Box, Heading, Text, VStack } from '@chakra-ui/react' + +export const Route = createFileRoute('/about')({ + component: AboutPage, +}) + +function AboutPage() { + return ( + + + + About + + + This page demonstrates automatic pageview tracking. When you navigate + here, react-fathom automatically tracks the pageview. + + + + + + How It Works + + + The TanStackRouterFathomTrackView component listens for + route changes and automatically sends pageview events to Fathom + Analytics. + + + Check the debug panel in the bottom-right corner to see the events + being tracked in real-time. + + + + ) +} diff --git a/examples/tanstack-router/src/routes/contact.tsx b/examples/tanstack-router/src/routes/contact.tsx new file mode 100644 index 0000000..9b8b31a --- /dev/null +++ b/examples/tanstack-router/src/routes/contact.tsx @@ -0,0 +1,57 @@ +import { createFileRoute } from '@tanstack/react-router' +import { Box, Heading, Text, VStack, Input, Button } from '@chakra-ui/react' +import { useFathom } from 'react-fathom' + +export const Route = createFileRoute('/contact')({ + component: ContactPage, +}) + +function ContactPage() { + const { trackEvent } = useFathom() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + trackEvent('contact_form_submit') + alert('Form submitted! Check the debug panel to see the tracked event.') + } + + return ( + + + + Contact + + + This page demonstrates form tracking. Submit the form to see custom + event tracking in action. + + + + + + + + Name + + + + + + Email + + + + + + Message + + + + + + + + ) +} diff --git a/examples/tanstack-router/src/routes/index.tsx b/examples/tanstack-router/src/routes/index.tsx new file mode 100644 index 0000000..71384d4 --- /dev/null +++ b/examples/tanstack-router/src/routes/index.tsx @@ -0,0 +1,66 @@ +import { createFileRoute } from '@tanstack/react-router' +import { Box, Heading, Text, VStack, Button, HStack } from '@chakra-ui/react' +import { useFathom } from 'react-fathom' + +export const Route = createFileRoute('/')({ + component: HomePage, +}) + +function HomePage() { + const { trackEvent, trackGoal } = useFathom() + + return ( + + + + TanStack Router + + Example + + + + This example demonstrates the TanStack Router integration with + react-fathom for privacy-focused analytics. + + + + + + Try It Out + + + + + + + + + + Features + + + — Automatic pageview tracking on route changes + — Custom event tracking + — Goal conversion tracking + — Debug mode with event visualization + — Privacy-focused (no cookies) + + + + ) +} diff --git a/examples/tanstack-router/tsconfig.json b/examples/tanstack-router/tsconfig.json new file mode 100644 index 0000000..0426f7b --- /dev/null +++ b/examples/tanstack-router/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/tanstack-router/vercel.json b/examples/tanstack-router/vercel.json new file mode 100644 index 0000000..ff3a0be --- /dev/null +++ b/examples/tanstack-router/vercel.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "vite", + "buildCommand": "cd ../.. && npm run build && cd examples/tanstack-router && npm run build", + "installCommand": "cd ../.. && npm install && cd examples/shared && npm install && cd ../tanstack-router && npm install", + "outputDirectory": "dist" +} diff --git a/examples/tanstack-router/vite.config.ts b/examples/tanstack-router/vite.config.ts new file mode 100644 index 0000000..74ab169 --- /dev/null +++ b/examples/tanstack-router/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { TanStackRouterVite } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [TanStackRouterVite(), react()], +}) diff --git a/package.json b/package.json index 37cd172..860c6b1 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,10 @@ "react", "react-native", "react-hooks", + "react-router", + "remix", + "gatsby", + "tanstack-router", "nextjs", "next.js", "typescript", @@ -86,11 +90,23 @@ "sideEffects": false, "typesVersions": { "*": { + "debug": [ + "./types/debug/index.d.ts" + ], "next": [ "./types/next/index.d.ts" ], "native": [ "./types/native/index.d.ts" + ], + "react-router": [ + "./types/react-router/index.d.ts" + ], + "gatsby": [ + "./types/gatsby/index.d.ts" + ], + "tanstack-router": [ + "./types/tanstack-router/index.d.ts" ] } }, @@ -101,6 +117,12 @@ "require": "./dist/cjs/index.cjs", "default": "./dist/es/index.js" }, + "./debug": { + "types": "./types/debug/index.d.ts", + "import": "./dist/es/debug/index.js", + "require": "./dist/cjs/debug/index.cjs", + "default": "./dist/es/debug/index.js" + }, "./next": { "types": "./types/next/index.d.ts", "import": "./dist/es/next/index.js", @@ -112,6 +134,24 @@ "import": "./dist/es/native/index.js", "require": "./dist/cjs/native/index.cjs", "default": "./dist/es/native/index.js" + }, + "./react-router": { + "types": "./types/react-router/index.d.ts", + "import": "./dist/es/react-router/index.js", + "require": "./dist/cjs/react-router/index.cjs", + "default": "./dist/es/react-router/index.js" + }, + "./gatsby": { + "types": "./types/gatsby/index.d.ts", + "import": "./dist/es/gatsby/index.js", + "require": "./dist/cjs/gatsby/index.cjs", + "default": "./dist/es/gatsby/index.js" + }, + "./tanstack-router": { + "types": "./types/tanstack-router/index.d.ts", + "import": "./dist/es/tanstack-router/index.js", + "require": "./dist/cjs/tanstack-router/index.cjs", + "default": "./dist/es/tanstack-router/index.js" } }, "scripts": { @@ -126,17 +166,32 @@ "push-release": "git push origin master && git push --tags", "tslint": "npx eslint types/index.d.ts", "test": "snyk test && vitest run", - "test:ci": "vitest run --coverage" + "test:ci": "vitest run --coverage", + "dev:all": "node scripts/dev-all.js", + "dev:proxy": "node scripts/dev-all.js --proxy" }, "peerDependencies": { + "@reach/router": ">=1.3.0", + "@tanstack/react-router": ">=1.0.0", "fathom-client": ">=3.0.0", + "gatsby": ">=4.0.0", "next": ">=10.0.0", "react": ">=16.8", "react-dom": ">=16.8", "react-native": ">=0.60.0", - "react-native-webview": ">=11.0.0" + "react-native-webview": ">=11.0.0", + "react-router-dom": ">=6.0.0" }, "peerDependenciesMeta": { + "@reach/router": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "gatsby": { + "optional": true + }, "next": { "optional": true }, @@ -146,6 +201,9 @@ "react-native-webview": { "optional": true }, + "react-router-dom": { + "optional": true + }, "fathom-client": { "optional": true } @@ -160,13 +218,16 @@ "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.23.3", "@eslint/js": "^9.0.0", + "@reach/router": "^1.3.4", "@rollup/plugin-babel": "^6.0.2", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^16.0.3", + "@tanstack/react-router": "^1.120.5", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", + "@types/reach__router": "^1.3.15", "@types/react": "^19.2.7", "@typescript-eslint/eslint-plugin": "^8.50.0", "@vitest/coverage-v8": "^4.0.16", @@ -186,6 +247,7 @@ "prettier": "^3.0.3", "react": "^19.2.3", "react-dom": "^19.2.3", + "react-router-dom": "^7.0.0", "regenerator-runtime": "^0.14.0", "rimraf": "^6.1.2", "rollup": "^4.6.0", diff --git a/rollup.config.js b/rollup.config.js index ab07518..4bd048c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -138,10 +138,17 @@ const nextExternal = [ 'next/navigation', ] const nativeExternal = ['react', 'react-native'] +const reactRouterExternal = ['fathom-client', 'react', 'react-router-dom'] +const gatsbyExternal = ['fathom-client', 'react', '@reach/router'] +const tanstackRouterExternal = ['fathom-client', 'react', '@tanstack/react-router'] const input = 'src/index.ts' +const debugInput = 'src/debug/index.ts' const nextInput = 'src/next/index.ts' const nativeInput = 'src/native/index.ts' +const reactRouterInput = 'src/react-router/index.ts' +const gatsbyInput = 'src/gatsby/index.ts' +const tanstackRouterInput = 'src/tanstack-router/index.ts' export default [ // UMD - Minified @@ -190,6 +197,30 @@ export default [ external: makeExternal(external), plugins: defaultPlugins, }, + // ES - debug + { + input: debugInput, + output: { + ...defaultOutputOptions, + dir: 'dist/es/debug', + format: 'esm', + entryFileNames: '[name].js', + }, + external: makeExternal(external), + plugins: defaultPlugins, + }, + // CJS - debug + { + input: debugInput, + output: { + ...defaultOutputOptions, + dir: 'dist/cjs/debug', + format: 'cjs', + entryFileNames: '[name].cjs', + }, + external: makeExternal(external), + plugins: defaultPlugins, + }, // ES - next { input: nextInput, @@ -238,4 +269,76 @@ export default [ external: makeExternal(nativeExternal), plugins: defaultPlugins, }, + // ES - react-router + { + input: reactRouterInput, + output: { + ...defaultOutputOptions, + dir: 'dist/es/react-router', + format: 'esm', + entryFileNames: '[name].js', + }, + external: makeExternal(reactRouterExternal), + plugins: defaultPlugins, + }, + // CJS - react-router + { + input: reactRouterInput, + output: { + ...defaultOutputOptions, + dir: 'dist/cjs/react-router', + format: 'cjs', + entryFileNames: '[name].cjs', + }, + external: makeExternal(reactRouterExternal), + plugins: defaultPlugins, + }, + // ES - gatsby + { + input: gatsbyInput, + output: { + ...defaultOutputOptions, + dir: 'dist/es/gatsby', + format: 'esm', + entryFileNames: '[name].js', + }, + external: makeExternal(gatsbyExternal), + plugins: defaultPlugins, + }, + // CJS - gatsby + { + input: gatsbyInput, + output: { + ...defaultOutputOptions, + dir: 'dist/cjs/gatsby', + format: 'cjs', + entryFileNames: '[name].cjs', + }, + external: makeExternal(gatsbyExternal), + plugins: defaultPlugins, + }, + // ES - tanstack-router + { + input: tanstackRouterInput, + output: { + ...defaultOutputOptions, + dir: 'dist/es/tanstack-router', + format: 'esm', + entryFileNames: '[name].js', + }, + external: makeExternal(tanstackRouterExternal), + plugins: defaultPlugins, + }, + // CJS - tanstack-router + { + input: tanstackRouterInput, + output: { + ...defaultOutputOptions, + dir: 'dist/cjs/tanstack-router', + format: 'cjs', + entryFileNames: '[name].cjs', + }, + external: makeExternal(tanstackRouterExternal), + plugins: defaultPlugins, + }, ] diff --git a/scripts/dev-all.js b/scripts/dev-all.js new file mode 100644 index 0000000..9f6afb4 --- /dev/null +++ b/scripts/dev-all.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node + +/** + * Development script to run docs and all example sites simultaneously + * + * Sites and their ports: + * - docs: http://localhost:3000 or http://docs.localhost:8080 + * - next-app: http://localhost:3001 or http://next-app.localhost:8080 + * - next-pages: http://localhost:3002 or http://next-pages.localhost:8080 + * - react: http://localhost:3003 or http://react.localhost:8080 + * - tanstack: http://localhost:3004 or http://tanstack.localhost:8080 + * - gatsby: http://localhost:3005 or http://gatsby.localhost:8080 + * + * Usage: + * node scripts/dev-all.js # Run all sites + * node scripts/dev-all.js --proxy # Run all sites with subdomain proxy on port 8080 + */ + +import { spawn } from 'child_process' +import { fileURLToPath } from 'url' +import path from 'path' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.resolve(__dirname, '..') + +const useProxy = process.argv.includes('--proxy') + +// Site configurations +const sites = [ + { + name: 'docs', + dir: 'docs', + command: 'npm', + args: ['run', 'dev', '--', '-p', '3000'], + port: 3000, + color: '\x1b[36m', // cyan + }, + { + name: 'next-app', + dir: 'examples/next-app', + command: 'npm', + args: ['run', 'dev', '--', '-p', '3001'], + port: 3001, + color: '\x1b[33m', // yellow + }, + { + name: 'next-pages', + dir: 'examples/next-pages', + command: 'npm', + args: ['run', 'dev', '--', '-p', '3002'], + port: 3002, + color: '\x1b[35m', // magenta + }, + { + name: 'react', + dir: 'examples/react', + command: 'npm', + args: ['run', 'dev', '--', '--port', '3003'], + port: 3003, + color: '\x1b[32m', // green + }, + { + name: 'tanstack', + dir: 'examples/tanstack-router', + command: 'npm', + args: ['run', 'dev', '--', '--port', '3004'], + port: 3004, + color: '\x1b[34m', // blue + }, + { + name: 'gatsby', + dir: 'examples/gatsby', + command: 'npm', + args: ['run', 'develop', '--', '-p', '3005'], + port: 3005, + color: '\x1b[31m', // red + }, +] + +const reset = '\x1b[0m' + +console.log('\n📦 Starting react-fathom development servers...\n') + +if (useProxy) { + console.log('🔀 Subdomain proxy enabled on port 8080') + console.log(' Access sites via: .localhost:8080\n') +} else { + console.log(' Use --proxy flag to enable subdomain routing on port 8080\n') +} + +console.log('Sites:') +sites.forEach(site => { + const url = useProxy + ? `http://${site.name}.localhost:8080` + : `http://localhost:${site.port}` + console.log(` ${site.color}${site.name.padEnd(12)}${reset} → ${url}`) +}) +console.log('') + +// Track child processes for cleanup +const processes = [] + +// Cleanup function +function cleanup() { + console.log('\n\nShutting down all servers...') + processes.forEach(proc => { + if (proc && !proc.killed) { + proc.kill('SIGTERM') + } + }) + process.exit(0) +} + +process.on('SIGINT', cleanup) +process.on('SIGTERM', cleanup) + +// Start each site +sites.forEach(site => { + const cwd = path.join(rootDir, site.dir) + const proc = spawn(site.command, site.args, { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + shell: true, + }) + + processes.push(proc) + + const prefix = `${site.color}[${site.name}]${reset}` + + proc.stdout.on('data', data => { + const lines = data.toString().split('\n').filter(Boolean) + lines.forEach(line => console.log(`${prefix} ${line}`)) + }) + + proc.stderr.on('data', data => { + const lines = data.toString().split('\n').filter(Boolean) + lines.forEach(line => console.log(`${prefix} ${line}`)) + }) + + proc.on('error', err => { + console.error(`${prefix} Failed to start: ${err.message}`) + }) + + proc.on('exit', (code, signal) => { + if (code !== null && code !== 0) { + console.log(`${prefix} Exited with code ${code}`) + } + }) +}) + +// Start proxy if requested +if (useProxy) { + const proxyPath = path.join(__dirname, 'subdomain-proxy.js') + const proxyProc = spawn('node', [proxyPath], { + cwd: rootDir, + stdio: ['ignore', 'pipe', 'pipe'], + }) + + processes.push(proxyProc) + + const prefix = '\x1b[37m[proxy]\x1b[0m' + + proxyProc.stdout.on('data', data => { + const lines = data.toString().split('\n').filter(Boolean) + lines.forEach(line => console.log(`${prefix} ${line}`)) + }) + + proxyProc.stderr.on('data', data => { + const lines = data.toString().split('\n').filter(Boolean) + lines.forEach(line => console.log(`${prefix} ${line}`)) + }) +} + +// Keep process running +process.stdin.resume() diff --git a/scripts/subdomain-proxy.js b/scripts/subdomain-proxy.js new file mode 100644 index 0000000..1aaeda7 --- /dev/null +++ b/scripts/subdomain-proxy.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +/** + * Simple subdomain proxy for local development + * + * Routes requests based on subdomain: + * docs.localhost:8080 → localhost:3000 + * next-app.localhost:8080 → localhost:3001 + * next-pages.localhost:8080 → localhost:3002 + * react.localhost:8080 → localhost:3003 + * tanstack.localhost:8080 → localhost:3004 + * gatsby.localhost:8080 → localhost:3005 + * + * Note: Most modern browsers resolve *.localhost to 127.0.0.1 automatically. + */ + +import http from 'http' +import { URL } from 'url' + +const PROXY_PORT = 8080 + +// Subdomain to port mapping +const routes = { + docs: 3000, + 'next-app': 3001, + 'next-pages': 3002, + react: 3003, + tanstack: 3004, + gatsby: 3005, +} + +// Extract subdomain from host header +function getSubdomain(host) { + if (!host) return null + // Remove port if present + const hostname = host.split(':')[0] + // Match patterns like "docs.localhost" or "next-app.localhost" + const match = hostname.match(/^([a-z0-9-]+)\.localhost$/i) + return match ? match[1] : null +} + +// Proxy request to target server +function proxyRequest(req, res, targetPort) { + const targetUrl = new URL(req.url, `http://localhost:${targetPort}`) + + const proxyReq = http.request( + { + hostname: 'localhost', + port: targetPort, + path: req.url, + method: req.method, + headers: { + ...req.headers, + host: `localhost:${targetPort}`, + }, + }, + proxyRes => { + res.writeHead(proxyRes.statusCode, proxyRes.headers) + proxyRes.pipe(res, { end: true }) + } + ) + + proxyReq.on('error', err => { + console.error(`Proxy error to port ${targetPort}: ${err.message}`) + res.writeHead(502, { 'Content-Type': 'text/plain' }) + res.end(`Bad Gateway: Could not connect to localhost:${targetPort}`) + }) + + req.pipe(proxyReq, { end: true }) +} + +// Handle WebSocket upgrades (for HMR) +function handleUpgrade(req, socket, head, targetPort) { + const proxyReq = http.request({ + hostname: 'localhost', + port: targetPort, + path: req.url, + method: req.method, + headers: { + ...req.headers, + host: `localhost:${targetPort}`, + }, + }) + + proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => { + socket.write( + `HTTP/1.1 101 Switching Protocols\r\n` + + Object.entries(proxyRes.headers) + .map(([k, v]) => `${k}: ${v}`) + .join('\r\n') + + '\r\n\r\n' + ) + proxySocket.pipe(socket) + socket.pipe(proxySocket) + }) + + proxyReq.on('error', err => { + console.error(`WebSocket proxy error: ${err.message}`) + socket.end() + }) + + proxyReq.end() +} + +// Create proxy server +const server = http.createServer((req, res) => { + const host = req.headers.host + const subdomain = getSubdomain(host) + + if (!subdomain || !routes[subdomain]) { + // Show available routes + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(` + + + + react-fathom Dev Sites + + + +

react-fathom Development Sites

+
    + ${Object.entries(routes) + .map( + ([name, port]) => + `
  • ${name}.localhost (port ${port})
  • ` + ) + .join('\n ')} +
+ + + `) + return + } + + const targetPort = routes[subdomain] + proxyRequest(req, res, targetPort) +}) + +// Handle WebSocket upgrades +server.on('upgrade', (req, socket, head) => { + const host = req.headers.host + const subdomain = getSubdomain(host) + + if (subdomain && routes[subdomain]) { + handleUpgrade(req, socket, head, routes[subdomain]) + } else { + socket.end() + } +}) + +server.listen(PROXY_PORT, () => { + console.log(`Subdomain proxy listening on port ${PROXY_PORT}`) + console.log('Available sites:') + Object.entries(routes).forEach(([name, port]) => { + console.log(` http://${name}.localhost:${PROXY_PORT} → localhost:${port}`) + }) +}) diff --git a/src/FathomContext.tsx b/src/FathomContext.tsx index d2f2096..359564c 100644 --- a/src/FathomContext.tsx +++ b/src/FathomContext.tsx @@ -28,6 +28,8 @@ const defaultContextValue: FathomContextInterface = { trackPageview: () => warnMissingProvider('trackPageview'), trackEvent: () => warnMissingProvider('trackEvent'), trackGoal: () => warnMissingProvider('trackGoal'), + subscribeToDebug: undefined, + debugEnabled: false, } export const FathomContext = diff --git a/src/FathomProvider.test.tsx b/src/FathomProvider.test.tsx index a3937df..04df4eb 100644 --- a/src/FathomProvider.test.tsx +++ b/src/FathomProvider.test.tsx @@ -527,6 +527,110 @@ describe('FathomProvider', () => { }) }) + it('should deep merge nested providers - child overrides specific defaultEventOptions while inheriting others', () => { + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ) + + const { result } = renderHook(() => useFathom(), { wrapper }) + + result.current.trackEvent?.('test-event') + + // Child overrides _value but inherits _site_id from parent + expect(mockClient.trackEvent).toHaveBeenCalledWith('test-event', { + _site_id: 'parent-id', + _value: 200, + }) + }) + + it('should deep merge nested providers - child overrides specific defaultPageviewOptions while inheriting others', () => { + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ) + + const { result } = renderHook(() => useFathom(), { wrapper }) + + result.current.trackPageview?.() + + // Child overrides url but inherits referrer from parent + expect(mockClient.trackPageview).toHaveBeenCalledWith({ + url: '/child', + referrer: 'https://parent.com', + }) + }) + + it('should deep merge three levels of nested providers', () => { + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + + {children} + + + + ) + + const { result } = renderHook(() => useFathom(), { wrapper }) + + result.current.trackEvent?.('test-event') + + // Deepest child overrides _value, inherits _site_id from root + expect(mockClient.trackEvent).toHaveBeenCalledWith('test-event', { + _site_id: 'root-id', + _value: 200, + }) + }) + it('should have displayName', () => { expect(FathomProvider.displayName).toBe('FathomProvider') }) @@ -629,4 +733,350 @@ describe('FathomProvider', () => { expect(clientRefValue).toBe(parentClient) }) }) + + describe('onError callback', () => { + it('should call onError when trackEvent throws', () => { + const error = new Error('trackEvent failed') + const onError = vi.fn() + const mockClient = { + trackEvent: vi.fn().mockImplementation(() => { + throw error + }), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook(() => useFathom(), { wrapper }) + + result.current.trackEvent?.('test-event') + + expect(onError).toHaveBeenCalledWith(error, { + method: 'trackEvent', + args: ['test-event', {}], + }) + }) + + it('should call onError when trackPageview throws', () => { + const error = new Error('trackPageview failed') + const onError = vi.fn() + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn().mockImplementation(() => { + throw error + }), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook(() => useFathom(), { wrapper }) + + result.current.trackPageview?.({ url: '/test' }) + + expect(onError).toHaveBeenCalledWith(error, { + method: 'trackPageview', + args: [{ url: '/test' }], + }) + }) + + it('should call onError when trackGoal throws', () => { + const error = new Error('trackGoal failed') + const onError = vi.fn() + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn().mockImplementation(() => { + throw error + }), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook(() => useFathom(), { wrapper }) + + result.current.trackGoal?.('GOAL_CODE', 1000) + + expect(onError).toHaveBeenCalledWith(error, { + method: 'trackGoal', + args: ['GOAL_CODE', 1000], + }) + }) + + it('should call onError when load throws', () => { + const error = new Error('load failed') + const onError = vi.fn() + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn().mockImplementation(() => { + throw error + }), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook(() => useFathom(), { wrapper }) + + result.current.load?.('SITE_ID') + + expect(onError).toHaveBeenCalledWith(error, { + method: 'load', + args: ['SITE_ID', undefined], + }) + }) + + it('should not throw when onError is not provided', () => { + const mockClient = { + trackEvent: vi.fn().mockImplementation(() => { + throw new Error('trackEvent failed') + }), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ) + + const { result } = renderHook(() => useFathom(), { wrapper }) + + // Should not throw + expect(() => result.current.trackEvent?.('test-event')).not.toThrow() + }) + }) + + describe('siteId validation', () => { + it('should warn when empty siteId is provided', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + renderHook(() => useFathom(), { wrapper }) + + await waitFor(() => { + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Empty siteId provided') + ) + }) + + // Should not call load with empty siteId + expect(mockClient.load).not.toHaveBeenCalled() + + warnSpy.mockRestore() + }) + + it('should not call load when siteId is undefined', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + renderHook(() => useFathom(), { wrapper }) + + await waitFor(() => { + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('No siteId provided') + ) + }) + + expect(mockClient.load).not.toHaveBeenCalled() + + warnSpy.mockRestore() + }) + }) + + describe('production debug warning', () => { + it('should warn when debug is enabled in production', async () => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + renderHook(() => useFathom(), { wrapper }) + + await waitFor(() => { + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Debug mode is enabled in production') + ) + }) + + warnSpy.mockRestore() + process.env.NODE_ENV = originalEnv + }) + + it('should not warn when debug is disabled in production', async () => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + renderHook(() => useFathom(), { wrapper }) + + // Give effects time to run + await new Promise((resolve) => setTimeout(resolve, 50)) + + expect(warnSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Debug mode is enabled in production') + ) + + warnSpy.mockRestore() + process.env.NODE_ENV = originalEnv + }) + }) + + describe('debug event counter isolation', () => { + it('should generate unique debug event IDs per provider instance', () => { + const events1: string[] = [] + const events2: string[] = [] + + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + // First provider + const wrapper1 = ({ children }: { children: React.ReactNode }) => ( + events1.push(e.id) }} + > + {children} + + ) + + const { result: result1 } = renderHook(() => useFathom(), { wrapper: wrapper1 }) + result1.current.trackEvent?.('event-a') + result1.current.trackEvent?.('event-b') + + // Second provider + const wrapper2 = ({ children }: { children: React.ReactNode }) => ( + events2.push(e.id) }} + > + {children} + + ) + + const { result: result2 } = renderHook(() => useFathom(), { wrapper: wrapper2 }) + result2.current.trackEvent?.('event-c') + + // Each provider should have its own counter sequence + expect(events1).toHaveLength(2) + expect(events2).toHaveLength(1) + + // All IDs should be unique + const allIds = [...events1, ...events2] + const uniqueIds = new Set(allIds) + expect(uniqueIds.size).toBe(allIds.length) + }) + }) }) diff --git a/src/FathomProvider.tsx b/src/FathomProvider.tsx index 4d5e1d8..360cca3 100644 --- a/src/FathomProvider.tsx +++ b/src/FathomProvider.tsx @@ -1,12 +1,12 @@ 'use client' -import React, { useCallback, useContext, useEffect, useMemo } from 'react' +import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react' import * as Fathom from 'fathom-client' import type { EventOptions, LoadOptions, PageViewOptions } from 'fathom-client' import { FathomContext } from './FathomContext' -import type { FathomProviderProps } from './types' +import type { DebugEvent, DebugEventCallback, DebugOptions, FathomProviderProps } from './types' const FathomProvider: React.FC = ({ children, @@ -16,26 +16,158 @@ const FathomProvider: React.FC = ({ siteId, defaultPageviewOptions: providedDefaultPageviewOptions, defaultEventOptions: providedDefaultEventOptions, + debug: debugProp, + onError, }) => { + // Instance-scoped counter for unique debug event IDs + const debugEventCounterRef = useRef(0) + const generateDebugEventId = useCallback( + () => `debug-${Date.now()}-${++debugEventCounterRef.current}`, + [], + ) + // Read parent context if it exists const parentContext = useContext(FathomContext) + // Parse debug options + const debugOptions: DebugOptions = useMemo(() => { + if (debugProp === true) { + return { enabled: true, console: true } + } + if (debugProp && typeof debugProp === 'object') { + return { + enabled: debugProp.enabled ?? false, + console: debugProp.console ?? debugProp.enabled ?? false, + onTrack: debugProp.onTrack, + } + } + // Inherit from parent if not specified + return { + enabled: parentContext.debugEnabled ?? false, + console: false, + } + }, [debugProp, parentContext.debugEnabled]) + + const debugEnabled = debugOptions.enabled + + // Log debug state when enabled + useEffect(() => { + if (debugEnabled) { + console.log('[react-fathom] Debug mode: enabled') + } + }, [debugEnabled]) + + // Warn if debug mode is enabled in production + useEffect(() => { + if (debugEnabled && process.env.NODE_ENV === 'production') { + console.warn( + '[react-fathom] Debug mode is enabled in production. ' + + 'This may expose tracking data via CustomEvent broadcasts and console logs. ' + + 'Consider disabling debug mode for production builds.' + ) + } + }, [debugEnabled]) + + // Store debug subscribers + const debugSubscribersRef = useRef>(new Set()) + + // Inherit parent's subscribers if we're a nested provider without our own debug config + useEffect(() => { + if (!debugProp && parentContext.subscribeToDebug) { + // We don't have our own debug config, so we don't need our own subscribers + // Events will flow through parent + } + }, [debugProp, parentContext.subscribeToDebug]) + + // Subscribe to debug events + const subscribeToDebug = useCallback((callback: DebugEventCallback) => { + debugSubscribersRef.current.add(callback) + return () => { + debugSubscribersRef.current.delete(callback) + } + }, []) + + // Emit debug event to all subscribers and optionally log to console + const emitDebugEvent = useCallback( + (event: DebugEvent) => { + // Always emit global custom event when debug is enabled + // This helps with linked packages where React context may not be shared + if (debugEnabled && typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('react-fathom:debug', { detail: event })) + } + + if (!debugEnabled) return + + // Log to console if enabled + if (debugOptions.console) { + const emoji = event.type === 'pageview' ? '📄' : event.type === 'event' ? '🎯' : '🏆' + const label = event.type === 'pageview' + ? `Pageview: ${event.url || '(current page)'}` + : event.type === 'event' + ? `Event: ${event.eventName}` + : `Goal: ${event.goalCode} ($${((event.goalCents || 0) / 100).toFixed(2)})` + + console.log( + `%c[react-fathom] ${emoji} ${label}`, + 'color: #8b5cf6; font-weight: bold;', + event.options || '' + ) + } + + // Call the onTrack callback if provided + if (debugOptions.onTrack) { + debugOptions.onTrack(event) + } + + // Notify all subscribers + debugSubscribersRef.current.forEach((callback) => { + try { + callback(event) + } catch (err) { + console.error('[react-fathom] Debug subscriber error:', err) + } + }) + }, + [debugEnabled, debugOptions] + ) + // Use provided client or fall back to parent client or default Fathom const client = useMemo( () => providedClient ?? parentContext.client ?? Fathom, [providedClient, parentContext.client], ) - // Merge defaultPageviewOptions: provided > parent > undefined + // Helper to safely call client methods with error handling + const safeClientCall = useCallback( + (method: string, fn: () => T, args?: unknown[]): T | undefined => { + try { + return fn() + } catch (error) { + if (process.env.NODE_ENV !== 'production') { + console.error(`[react-fathom] ${method}() failed:`, error) + } + onError?.(error, { method, args }) + return undefined + } + }, + [onError], + ) + + // Merge defaultPageviewOptions: parent + provided (provided overrides parent) const defaultPageviewOptions = useMemo( - () => - providedDefaultPageviewOptions ?? parentContext.defaultPageviewOptions, + () => ({ + ...parentContext.defaultPageviewOptions, + ...providedDefaultPageviewOptions, + }), [providedDefaultPageviewOptions, parentContext.defaultPageviewOptions], ) - // Merge defaultEventOptions: provided > parent > undefined + // Merge defaultEventOptions: parent + provided (provided overrides parent) const defaultEventOptions = useMemo( - () => providedDefaultEventOptions ?? parentContext.defaultEventOptions, + () => ({ + ...parentContext.defaultEventOptions, + ...providedDefaultEventOptions, + }), [providedDefaultEventOptions, parentContext.defaultEventOptions], ) @@ -53,9 +185,9 @@ const FathomProvider: React.FC = ({ const load = useCallback( (siteId: string, clientOptions?: LoadOptions) => { - client.load(siteId, clientOptions) + safeClientCall('load', () => client.load(siteId, clientOptions), [siteId, clientOptions]) }, - [client], + [client, safeClientCall], ) const setSite = useCallback( @@ -67,34 +199,81 @@ const FathomProvider: React.FC = ({ const trackEvent = useCallback( (eventName: string, options?: EventOptions) => { - client.trackEvent(eventName, { + const mergedOptions = { ...defaultEventOptions, ...options, + } + + // Emit debug event + emitDebugEvent({ + id: generateDebugEventId(), + timestamp: Date.now(), + type: 'event', + eventName, + options: mergedOptions, }) + + // Track to Fathom + safeClientCall('trackEvent', () => client.trackEvent(eventName, mergedOptions), [eventName, mergedOptions]) }, - [client, defaultEventOptions], + [client, defaultEventOptions, emitDebugEvent, generateDebugEventId, safeClientCall], ) const trackPageview = useCallback( (options?: PageViewOptions) => { - client.trackPageview({ + const mergedOptions = { ...defaultPageviewOptions, ...options, + } + + // Emit debug event + emitDebugEvent({ + id: generateDebugEventId(), + timestamp: Date.now(), + type: 'pageview', + url: mergedOptions.url, + options: mergedOptions, }) + + // Track to Fathom + safeClientCall('trackPageview', () => client.trackPageview(mergedOptions), [mergedOptions]) }, - [client, defaultPageviewOptions], + [client, defaultPageviewOptions, emitDebugEvent, generateDebugEventId, safeClientCall], ) const trackGoal = useCallback( (code: string, cents: number) => { - client.trackGoal(code, cents) + // Emit debug event + emitDebugEvent({ + id: generateDebugEventId(), + timestamp: Date.now(), + type: 'goal', + goalCode: code, + goalCents: cents, + }) + + // Track to Fathom + safeClientCall('trackGoal', () => client.trackGoal(code, cents), [code, cents]) }, - [client], + [client, emitDebugEvent, generateDebugEventId, safeClientCall], ) useEffect(() => { - if (siteId !== undefined) { + if (siteId !== undefined && siteId !== '') { load(siteId, clientOptions) + } else if (process.env.NODE_ENV !== 'production') { + if (siteId === '') { + console.warn( + '[react-fathom] Empty siteId provided to FathomProvider. ' + + 'Please provide a valid Fathom site ID.' + ) + } else { + console.warn( + '[react-fathom] No siteId provided to FathomProvider. ' + + 'Analytics tracking will not be sent to Fathom until a siteId is configured. ' + + 'Debug events will still be captured if debug mode is enabled.' + ) + } } }, [clientOptions, load, siteId]) @@ -119,6 +298,8 @@ const FathomProvider: React.FC = ({ client, defaultPageviewOptions, defaultEventOptions, + subscribeToDebug: debugEnabled ? subscribeToDebug : undefined, + debugEnabled, }} > {children} diff --git a/src/debug/EventStream.test.tsx b/src/debug/EventStream.test.tsx new file mode 100644 index 0000000..7588267 --- /dev/null +++ b/src/debug/EventStream.test.tsx @@ -0,0 +1,329 @@ +import React from 'react' + +import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest' + +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' + +import { FathomProvider } from '../FathomProvider' +import { EventStream } from './EventStream' + +// Mock fathom-client +vi.mock('fathom-client', () => ({ + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), +})) + +describe('EventStream', () => { + const mockClient = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const localStorageMock = (() => { + let store: Record = {} + return { + getItem: vi.fn((key: string) => store[key] || null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value + }), + removeItem: vi.fn((key: string) => { + delete store[key] + }), + clear: vi.fn(() => { + store = {} + }), + } + })() + + beforeEach(() => { + vi.clearAllMocks() + localStorageMock.clear() + Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true, + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should not render when debug is disabled', async () => { + render( + + + + ) + + // Wait for hydration + await waitFor(() => { + expect(screen.queryByRole('button')).not.toBeInTheDocument() + }) + }) + + it('should render toggle button when debug is enabled', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /show event stream/i })).toBeInTheDocument() + }) + }) + + it('should show panel when button is clicked', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /show event stream/i })).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: /show event stream/i })) + + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + expect(screen.getByText(/No events yet/)).toBeInTheDocument() + }) + + it('should hide panel when close button is clicked', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: /hide event stream/i })) + + expect(screen.queryByText('📊 Event Stream')).not.toBeInTheDocument() + }) + + it('should display events when tracking calls are made', async () => { + const TestComponent = () => { + const [tracked, setTracked] = React.useState(false) + return ( + + + {!tracked && ( + + )} + + ) + } + + render() + + await waitFor(() => { + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + }) + + // Initial state shows no events + expect(screen.getByText(/No events yet/)).toBeInTheDocument() + }) + + it('should toggle with keyboard shortcut', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /show event stream/i })).toBeInTheDocument() + }) + + // Panel should not be visible initially + expect(screen.queryByText('📊 Event Stream')).not.toBeInTheDocument() + + // Press Cmd + . + act(() => { + fireEvent.keyDown(document, { key: '.', metaKey: true }) + }) + + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + + // Press Cmd + . again to hide + act(() => { + fireEvent.keyDown(document, { key: '.', metaKey: true }) + }) + + expect(screen.queryByText('📊 Event Stream')).not.toBeInTheDocument() + }) + + it('should toggle with Ctrl + . on non-Mac', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /show event stream/i })).toBeInTheDocument() + }) + + // Press Ctrl + . + act(() => { + fireEvent.keyDown(document, { key: '.', ctrlKey: true }) + }) + + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + }) + + it('should persist visibility state to localStorage', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /show event stream/i })).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: /show event stream/i })) + + expect(localStorageMock.setItem).toHaveBeenCalledWith( + 'react-fathom-event-stream-visible', + 'true' + ) + }) + + it('should load visibility state from localStorage', async () => { + localStorageMock.getItem.mockReturnValue('true') + + render( + + + + ) + + await waitFor(() => { + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + }) + }) + + it('should handle localStorage errors gracefully', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + localStorageMock.getItem.mockImplementation(() => { + throw new Error('localStorage unavailable') + }) + + // Should not throw + render( + + + + ) + + await waitFor(() => { + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + }) + + errorSpy.mockRestore() + }) + + it('should handle localStorage setItem errors gracefully', async () => { + localStorageMock.setItem.mockImplementation(() => { + throw new Error('localStorage full') + }) + + render( + + + + ) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /show event stream/i })).toBeInTheDocument() + }) + + // Should not throw when clicking + fireEvent.click(screen.getByRole('button', { name: /show event stream/i })) + + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + }) + + it('should position panel on the left when position is bottom-left', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + }) + + // Panel container should be rendered - find the outer positioned div + const panelContainer = screen.getByText('📊 Event Stream').parentElement?.parentElement + expect(panelContainer).toHaveAttribute('style', expect.stringContaining('left: 0')) + }) + + it('should show keyboard shortcut hint in footer', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + }) + + expect(screen.getByText('⌘.')).toBeInTheDocument() + }) + + it('should disable clear button when no events', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByText('📊 Event Stream')).toBeInTheDocument() + }) + + const clearButton = screen.getByRole('button', { name: 'Clear' }) + expect(clearButton).toBeDisabled() + }) + + it('should use debug={true} shorthand', async () => { + render( + + + + ) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /show event stream/i })).toBeInTheDocument() + }) + }) +}) diff --git a/src/debug/EventStream.tsx b/src/debug/EventStream.tsx new file mode 100644 index 0000000..f936a04 --- /dev/null +++ b/src/debug/EventStream.tsx @@ -0,0 +1,244 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useDebugSubscription } from '../hooks/useDebugSubscription' +import type { DebugEvent } from '../types' + +const STORAGE_KEY = 'react-fathom-event-stream-visible' + +function formatTime(timestamp: number): string { + return new Date(timestamp).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) +} + +interface EventCardProps { + event: DebugEvent +} + +function EventCard({ event }: EventCardProps) { + const colors = { + pageview: '#3b82f6', + event: '#8b5cf6', + goal: '#22c55e', + } + + let title = '' + let subtitle = '' + + switch (event.type) { + case 'pageview': + title = 'Pageview' + subtitle = event.url || (typeof window !== 'undefined' ? window.location.pathname : '') + break + case 'event': + title = 'Event' + subtitle = event.eventName || '' + break + case 'goal': + title = 'Goal' + subtitle = `${event.goalCode} ($${((event.goalCents || 0) / 100).toFixed(2)})` + break + } + + return ( +
+
+ {title} + {formatTime(event.timestamp)} +
+
+ {subtitle} +
+
+ ) +} + +export interface EventStreamProps { + /** + * Maximum number of events to display + * @default 20 + */ + maxEvents?: number + /** + * Position of the toggle button + * @default 'bottom-right' + */ + position?: 'bottom-right' | 'bottom-left' + /** + * Initial visibility state (overridden by localStorage if available) + * @default false + */ + defaultVisible?: boolean +} + +/** + * A debug panel component that displays tracked events in real-time. + * Only renders when debug mode is enabled in the FathomProvider. + * + * @example + * ```tsx + * + * + * + * + * ``` + */ +export function EventStream({ + maxEvents = 20, + position = 'bottom-right', + defaultVisible = false, +}: EventStreamProps = {}) { + const [isVisible, setIsVisible] = useState(defaultVisible) + const [isHydrated, setIsHydrated] = useState(false) + const { events, debugEnabled, clearEvents } = useDebugSubscription({ maxEvents }) + + useEffect(() => { + setIsHydrated(true) + if (typeof window !== 'undefined') { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored !== null) { + setIsVisible(stored === 'true') + } + } catch { + // localStorage may be unavailable (private browsing, security restrictions, etc.) + } + } + }, []) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === '.') { + e.preventDefault() + setIsVisible((prev) => !prev) + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, []) + + useEffect(() => { + if (isHydrated && typeof window !== 'undefined') { + try { + localStorage.setItem(STORAGE_KEY, String(isVisible)) + } catch { + // localStorage may be unavailable (private browsing, security restrictions, etc.) + } + } + }, [isVisible, isHydrated]) + + if (!debugEnabled || !isHydrated) return null + + const isRight = position === 'bottom-right' + + return ( + <> + + + {isVisible && ( +
+
+ 📊 Event Stream + +
+ +
+ {events.length === 0 ? ( +
+
🔍
+
+ No events yet.
Navigate or interact to see tracking events. +
+
+ ) : ( + events.map((event) => ) + )} +
+ +
+ {events.length} event{events.length !== 1 ? 's' : ''} •{' '} + ⌘. to toggle +
+
+ )} + + ) +} diff --git a/src/debug/index.ts b/src/debug/index.ts new file mode 100644 index 0000000..3293cf3 --- /dev/null +++ b/src/debug/index.ts @@ -0,0 +1,7 @@ +export * from './EventStream' +export { useDebugSubscription } from '../hooks/useDebugSubscription' +export type { + DebugEvent, + DebugEventCallback, + DebugOptions, +} from '../types' diff --git a/src/gatsby/GatsbyFathomTrackView.test.tsx b/src/gatsby/GatsbyFathomTrackView.test.tsx new file mode 100644 index 0000000..be8ccfa --- /dev/null +++ b/src/gatsby/GatsbyFathomTrackView.test.tsx @@ -0,0 +1,343 @@ +import React from 'react' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { render, waitFor } from '@testing-library/react' + +import { FathomProvider } from '../FathomProvider' +import { GatsbyFathomTrackView } from './GatsbyFathomTrackView' + +// Mock @reach/router's globalHistory +const mockListeners: Array<(args: { location: { pathname: string; search: string; hash: string }; action: string }) => void> = [] + +vi.mock('@reach/router', () => ({ + globalHistory: { + listen: (callback: (args: { location: { pathname: string; search: string; hash: string }; action: string }) => void) => { + mockListeners.push(callback) + return () => { + const index = mockListeners.indexOf(callback) + if (index > -1) { + mockListeners.splice(index, 1) + } + } + }, + }, +})) + +// Mock fathom-client +vi.mock('fathom-client', () => { + const mockFathomDefault = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + return { + default: mockFathomDefault, + } +}) + +// Helper to simulate route changes +function simulateRouteChange(pathname: string, search = '', hash = '', action = 'PUSH') { + mockListeners.forEach((listener) => { + listener({ + location: { pathname, search, hash }, + action, + }) + }) +} + +describe('GatsbyFathomTrackView', () => { + beforeEach(() => { + vi.clearAllMocks() + mockListeners.length = 0 + delete (window as { location?: unknown }).location + window.location = { + href: 'https://example.com/test-page', + origin: 'https://example.com', + pathname: '/test-page', + search: '', + hash: '', + } as Location + }) + + it('should track initial pageview on mount', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/test-page', + }) + }) + + it('should track pageviews on route changes', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + , + ) + + // Wait for initial pageview + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalledTimes(1) + }) + + // Simulate route change + simulateRouteChange('/new-page') + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalledTimes(2) + }) + + expect(trackPageviewSpy).toHaveBeenLastCalledWith({ + url: 'https://example.com/new-page', + }) + }) + + it('should track on POP action (back/forward navigation)', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalledTimes(1) + }) + + // Simulate back navigation + simulateRouteChange('/previous-page', '', '', 'POP') + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalledTimes(2) + }) + }) + + it('should not track when disableAutoTrack is true', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + , + ) + + // Give time for effects to run + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(trackPageviewSpy).not.toHaveBeenCalled() + }) + + it('should include search params by default', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + window.location = { + ...window.location, + search: '?foo=bar', + } as Location + + render( + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/test-page?foo=bar', + }) + }) + + it('should include hash when includeHash is true', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + window.location = { + ...window.location, + hash: '#section', + } as Location + + render( + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/test-page#section', + }) + }) + + it('should transform URL when transformUrl is provided', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const transformUrl = (url: string) => url.replace('/test-page', '/transformed') + + render( + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/transformed', + }) + }) + + it('should skip tracking when transformUrl returns null', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const transformUrl = () => null + + render( + + + , + ) + + // Give time for effects to run + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(trackPageviewSpy).not.toHaveBeenCalled() + }) + + it('should have displayName', () => { + expect(GatsbyFathomTrackView.displayName).toBe('GatsbyFathomTrackView') + }) + + it('should cleanup listener on unmount', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const { unmount } = render( + + + , + ) + + await waitFor(() => { + expect(mockListeners.length).toBe(1) + }) + + unmount() + + expect(mockListeners.length).toBe(0) + }) +}) diff --git a/src/gatsby/GatsbyFathomTrackView.tsx b/src/gatsby/GatsbyFathomTrackView.tsx new file mode 100644 index 0000000..d30afec --- /dev/null +++ b/src/gatsby/GatsbyFathomTrackView.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useRef } from 'react' + +import { globalHistory } from '@reach/router' + +import { useFathom } from '../hooks/useFathom' +import { buildTrackingUrl } from '../utils' + +export interface GatsbyFathomTrackViewProps { + /** + * Disable automatic pageview tracking on route changes + * @default false + */ + disableAutoTrack?: boolean + + /** + * Include search/query parameters in the tracked URL + * @default true + */ + includeSearchParams?: boolean + + /** + * Include hash fragment in the tracked URL + * @default false + */ + includeHash?: boolean + + /** + * Custom function to transform the URL before tracking. + * Useful for removing sensitive data or normalizing URLs. + * @param url The URL that would be tracked + * @returns The transformed URL to track, or null/undefined to skip tracking + */ + transformUrl?: (url: string) => string | null | undefined +} + +/** + * Component that tracks pageviews for Gatsby applications. + * Uses @reach/router's globalHistory to listen for route changes. + * Must be used within a FathomProvider. + * + * @example + * ```tsx + * // src/components/Layout.tsx + * import { FathomProvider } from 'react-fathom' + * import { GatsbyFathomTrackView } from 'react-fathom/gatsby' + * + * export default function Layout({ children }) { + * return ( + * + * + * {children} + * + * ) + * } + * ``` + * + * @example + * ```tsx + * // With URL transformation + * { + * // Strip query params for cleaner analytics + * return url.split('?')[0] + * }} + * /> + * ``` + */ +export const GatsbyFathomTrackView: React.FC = ({ + disableAutoTrack = false, + includeSearchParams = true, + includeHash = false, + transformUrl, +}) => { + const hasTrackedInitialPageview = useRef(false) + const { trackPageview, client } = useFathom() + + // Build URL from location + const buildUrl = (location: { pathname: string; search: string; hash: string }) => { + return buildTrackingUrl({ + pathname: location.pathname, + search: location.search, + hash: location.hash, + includeSearchParams, + includeHash, + transformUrl, + }) + } + + // Track initial pageview + useEffect(() => { + if ( + !trackPageview || + !client || + disableAutoTrack || + hasTrackedInitialPageview.current || + typeof window === 'undefined' + ) { + return + } + + hasTrackedInitialPageview.current = true + const url = buildUrl({ + pathname: window.location.pathname, + search: window.location.search, + hash: window.location.hash, + }) + if (url) { + trackPageview({ url }) + } + }, [trackPageview, client, disableAutoTrack]) + + // Listen to route changes via globalHistory + useEffect(() => { + if (!trackPageview || !client || disableAutoTrack) { + return + } + + const unsubscribe = globalHistory.listen(({ location, action }) => { + // Only track on PUSH and POP actions (actual navigation) + if (action === 'PUSH' || action === 'POP') { + const url = buildUrl(location) + if (url) { + trackPageview({ url }) + } + } + }) + + return () => { + unsubscribe() + } + }, [trackPageview, client, disableAutoTrack, includeSearchParams, includeHash, transformUrl]) + + return null +} + +GatsbyFathomTrackView.displayName = 'GatsbyFathomTrackView' diff --git a/src/gatsby/gatsbyBrowser.ts b/src/gatsby/gatsbyBrowser.ts new file mode 100644 index 0000000..af996a8 --- /dev/null +++ b/src/gatsby/gatsbyBrowser.ts @@ -0,0 +1,185 @@ +import type * as FathomType from 'fathom-client' + +export interface GatsbyFathomOptions { + /** + * Your Fathom site ID + */ + siteId: string + + /** + * Include search/query parameters in the tracked URL + * @default true + */ + includeSearchParams?: boolean + + /** + * Include hash fragment in the tracked URL + * @default false + */ + includeHash?: boolean + + /** + * Custom function to transform the URL before tracking + * @param url The URL that would be tracked + * @returns The transformed URL to track, or null/undefined to skip tracking + */ + transformUrl?: (url: string) => string | null | undefined + + /** + * Additional options to pass to Fathom's load function + */ + loadOptions?: Parameters[1] +} + +/** + * Creates Gatsby browser API handlers for Fathom Analytics. + * Use in gatsby-browser.js to automatically track pageviews. + * + * @example + * ```js + * // gatsby-browser.js + * import { createGatsbyFathomPlugins } from 'react-fathom/gatsby' + * + * const fathomPlugins = createGatsbyFathomPlugins({ + * siteId: 'YOUR_SITE_ID', + * loadOptions: { + * includedDomains: ['yourdomain.com'], + * }, + * }) + * + * export const onClientEntry = fathomPlugins.onClientEntry + * export const onRouteUpdate = fathomPlugins.onRouteUpdate + * ``` + * + * @example + * ```js + * // Or use spread syntax to export all handlers + * import { createGatsbyFathomPlugins } from 'react-fathom/gatsby' + * + * const { onClientEntry, onRouteUpdate } = createGatsbyFathomPlugins({ + * siteId: process.env.GATSBY_FATHOM_SITE_ID, + * }) + * + * export { onClientEntry, onRouteUpdate } + * ``` + */ +export function createGatsbyFathomPlugins(options: GatsbyFathomOptions) { + const { + siteId, + includeSearchParams = true, + includeHash = false, + transformUrl, + loadOptions, + } = options + + let fathomClient: typeof FathomType | null = null + + const buildUrl = (location: { pathname: string; search?: string; hash?: string }) => { + if (typeof window === 'undefined') return null + + let url = window.location.origin + location.pathname + + if (includeSearchParams && location.search) { + url += location.search + } + + if (includeHash && location.hash) { + url += location.hash + } + + if (transformUrl) { + const transformed = transformUrl(url) + if (transformed === null || transformed === undefined) { + return null + } + url = transformed + } + + return url + } + + return { + /** + * Called when the Gatsby browser runtime first starts. + * Loads the Fathom script. + */ + onClientEntry: async () => { + if (typeof window === 'undefined') return + + // Dynamically import fathom-client + const Fathom = await import('fathom-client') + fathomClient = Fathom + + fathomClient.load(siteId, { + auto: false, // We handle tracking manually + ...loadOptions, + }) + }, + + /** + * Called when the user changes routes. + * Tracks a pageview for the new route. + */ + onRouteUpdate: ({ location }: { location: { pathname: string; search?: string; hash?: string } }) => { + if (!fathomClient) return + + const url = buildUrl(location) + if (url) { + fathomClient.trackPageview({ url }) + } + }, + } +} + +/** + * Simplified helper to track a pageview in Gatsby. + * Use this if you want more control over when tracking happens. + * + * @example + * ```js + * // gatsby-browser.js + * import * as Fathom from 'fathom-client' + * import { trackGatsbyPageview } from 'react-fathom/gatsby' + * + * export const onClientEntry = () => { + * Fathom.load('YOUR_SITE_ID', { auto: false }) + * } + * + * export const onRouteUpdate = ({ location }) => { + * trackGatsbyPageview(Fathom, location) + * } + * ``` + */ +export function trackGatsbyPageview( + fathomClient: typeof FathomType, + location: { pathname: string; search?: string; hash?: string }, + options?: { + includeSearchParams?: boolean + includeHash?: boolean + transformUrl?: (url: string) => string | null | undefined + } +) { + if (typeof window === 'undefined') return + + const { includeSearchParams = true, includeHash = false, transformUrl } = options || {} + + let url = window.location.origin + location.pathname + + if (includeSearchParams && location.search) { + url += location.search + } + + if (includeHash && location.hash) { + url += location.hash + } + + if (transformUrl) { + const transformed = transformUrl(url) + if (transformed === null || transformed === undefined) { + return + } + url = transformed + } + + fathomClient.trackPageview({ url }) +} diff --git a/src/gatsby/index.ts b/src/gatsby/index.ts new file mode 100644 index 0000000..e9e6ba0 --- /dev/null +++ b/src/gatsby/index.ts @@ -0,0 +1,5 @@ +// Gatsby tracking component +export * from './GatsbyFathomTrackView' + +// Gatsby browser API helpers +export * from './gatsbyBrowser' diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 5d1b702..93ce451 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,3 +2,4 @@ export * from './useFathom' export * from './useTrackOnMount' export * from './useTrackOnClick' export * from './useTrackOnVisible' +export * from './useDebugSubscription' diff --git a/src/hooks/useDebugSubscription.test.tsx b/src/hooks/useDebugSubscription.test.tsx new file mode 100644 index 0000000..c676705 --- /dev/null +++ b/src/hooks/useDebugSubscription.test.tsx @@ -0,0 +1,257 @@ +import React from 'react' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { act, renderHook, waitFor } from '@testing-library/react' + +import { FathomProvider } from '../FathomProvider' +import { useFathom } from './useFathom' +import { useDebugSubscription } from './useDebugSubscription' + +// Mock fathom-client +vi.mock('fathom-client', () => ({ + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), +})) + +describe('useDebugSubscription', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should return debugEnabled as false when debug is not enabled', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ) + + const { result } = renderHook(() => useDebugSubscription(), { wrapper }) + + expect(result.current.debugEnabled).toBe(false) + expect(result.current.events).toEqual([]) + }) + + it('should return debugEnabled as true when debug is enabled', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ) + + const { result } = renderHook(() => useDebugSubscription(), { wrapper }) + + expect(result.current.debugEnabled).toBe(true) + }) + + it('should receive events when trackEvent is called', async () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook( + () => { + const fathom = useFathom() + const debug = useDebugSubscription() + return { fathom, debug } + }, + { wrapper } + ) + + act(() => { + result.current.fathom.trackEvent?.('test-event', { _value: 100 }) + }) + + await waitFor(() => { + expect(result.current.debug.events.length).toBe(1) + }) + + expect(result.current.debug.events[0].type).toBe('event') + expect(result.current.debug.events[0].eventName).toBe('test-event') + }) + + it('should receive events when trackPageview is called', async () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook( + () => { + const fathom = useFathom() + const debug = useDebugSubscription() + return { fathom, debug } + }, + { wrapper } + ) + + act(() => { + result.current.fathom.trackPageview?.({ url: '/test-page' }) + }) + + await waitFor(() => { + expect(result.current.debug.events.length).toBe(1) + }) + + expect(result.current.debug.events[0].type).toBe('pageview') + expect(result.current.debug.events[0].url).toBe('/test-page') + }) + + it('should receive events when trackGoal is called', async () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook( + () => { + const fathom = useFathom() + const debug = useDebugSubscription() + return { fathom, debug } + }, + { wrapper } + ) + + act(() => { + result.current.fathom.trackGoal?.('GOAL123', 2999) + }) + + await waitFor(() => { + expect(result.current.debug.events.length).toBe(1) + }) + + expect(result.current.debug.events[0].type).toBe('goal') + expect(result.current.debug.events[0].goalCode).toBe('GOAL123') + expect(result.current.debug.events[0].goalCents).toBe(2999) + }) + + it('should limit events to maxEvents', async () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook( + () => { + const fathom = useFathom() + const debug = useDebugSubscription({ maxEvents: 3 }) + return { fathom, debug } + }, + { wrapper } + ) + + act(() => { + result.current.fathom.trackEvent?.('event-1') + result.current.fathom.trackEvent?.('event-2') + result.current.fathom.trackEvent?.('event-3') + result.current.fathom.trackEvent?.('event-4') + result.current.fathom.trackEvent?.('event-5') + }) + + await waitFor(() => { + expect(result.current.debug.events.length).toBe(3) + }) + + // Most recent events should be first + expect(result.current.debug.events[0].eventName).toBe('event-5') + expect(result.current.debug.events[1].eventName).toBe('event-4') + expect(result.current.debug.events[2].eventName).toBe('event-3') + }) + + it('should call onEvent callback when events are received', async () => { + const onEventSpy = vi.fn() + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook( + () => { + const fathom = useFathom() + const debug = useDebugSubscription({ onEvent: onEventSpy }) + return { fathom, debug } + }, + { wrapper } + ) + + act(() => { + result.current.fathom.trackEvent?.('test-event') + }) + + await waitFor(() => { + expect(onEventSpy).toHaveBeenCalled() + }) + + expect(onEventSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'event', + eventName: 'test-event', + }) + ) + }) + + it('should clear events when clearEvents is called', async () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook( + () => { + const fathom = useFathom() + const debug = useDebugSubscription() + return { fathom, debug } + }, + { wrapper } + ) + + act(() => { + result.current.fathom.trackEvent?.('event-1') + result.current.fathom.trackEvent?.('event-2') + }) + + await waitFor(() => { + expect(result.current.debug.events.length).toBe(2) + }) + + act(() => { + result.current.debug.clearEvents() + }) + + expect(result.current.debug.events.length).toBe(0) + }) + + it('should work with debug={true} shorthand', async () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ) + + const { result } = renderHook( + () => { + const fathom = useFathom() + const debug = useDebugSubscription() + return { fathom, debug } + }, + { wrapper } + ) + + expect(result.current.debug.debugEnabled).toBe(true) + + act(() => { + result.current.fathom.trackEvent?.('test-event') + }) + + await waitFor(() => { + expect(result.current.debug.events.length).toBe(1) + }) + }) +}) diff --git a/src/hooks/useDebugSubscription.ts b/src/hooks/useDebugSubscription.ts new file mode 100644 index 0000000..85e61ea --- /dev/null +++ b/src/hooks/useDebugSubscription.ts @@ -0,0 +1,89 @@ +import { useContext, useEffect, useState } from 'react' + +import { FathomContext } from '../FathomContext' +import type { DebugEvent, DebugEventCallback } from '../types' + +export interface UseDebugSubscriptionOptions { + /** + * Maximum number of events to keep in history. + * @default 50 + */ + maxEvents?: number + /** + * Callback fired when a new event is received. + */ + onEvent?: DebugEventCallback +} + +export interface UseDebugSubscriptionResult { + /** + * Array of debug events, most recent first. + */ + events: DebugEvent[] + /** + * Whether debug mode is enabled in the provider. + */ + debugEnabled: boolean + /** + * Clear all events from history. + */ + clearEvents: () => void +} + +/** + * Hook to subscribe to debug events from FathomProvider. + * Returns an array of events that can be displayed in a UI. + * + * @example + * ```tsx + * function DebugPanel() { + * const { events, debugEnabled, clearEvents } = useDebugSubscription({ + * maxEvents: 20, + * onEvent: (event) => console.log('New event:', event) + * }) + * + * if (!debugEnabled) return null + * + * return ( + *
+ * + * {events.map(event => ( + *
{event.type}: {event.eventName || event.url}
+ * ))} + *
+ * ) + * } + * ``` + */ +export function useDebugSubscription( + options: UseDebugSubscriptionOptions = {} +): UseDebugSubscriptionResult { + const { maxEvents = 50, onEvent } = options + const { subscribeToDebug, debugEnabled = false } = useContext(FathomContext) + const [events, setEvents] = useState([]) + + useEffect(() => { + if (!subscribeToDebug) return + + const unsubscribe = subscribeToDebug((event) => { + setEvents((prev) => { + const updated = [event, ...prev] + return updated.slice(0, maxEvents) + }) + + if (onEvent) { + onEvent(event) + } + }) + + return unsubscribe + }, [subscribeToDebug, maxEvents, onEvent]) + + const clearEvents = () => setEvents([]) + + return { + events, + debugEnabled, + clearEvents, + } +} diff --git a/src/next/NextFathomTrackViewApp.test.tsx b/src/next/NextFathomTrackViewApp.test.tsx index ecc19a6..09009f0 100644 --- a/src/next/NextFathomTrackViewApp.test.tsx +++ b/src/next/NextFathomTrackViewApp.test.tsx @@ -259,6 +259,50 @@ describe('NextFathomTrackViewApp', () => { ) }) + it('should apply transformUrl to tracked URL', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const nextNavigation = await import('next/navigation') + vi.mocked(nextNavigation.usePathname).mockReturnValue('/test-page') + vi.mocked(nextNavigation.useSearchParams).mockReturnValue( + new URLSearchParams('?token=secret&page=1'), + ) + + const transformUrl = (url: string) => { + const u = new URL(url) + u.searchParams.delete('token') + return u.toString() + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + ) + + renderHook(() => useFathom(), { wrapper }) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + // URL should have token param stripped + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/test-page?page=1', + }) + }) + it('should have displayName', () => { expect(NextFathomTrackViewApp.displayName).toBe('NextFathomTrackViewApp') }) diff --git a/src/next/NextFathomTrackViewApp.tsx b/src/next/NextFathomTrackViewApp.tsx index 1fd4b06..943b32e 100644 --- a/src/next/NextFathomTrackViewApp.tsx +++ b/src/next/NextFathomTrackViewApp.tsx @@ -5,6 +5,7 @@ import React, { useEffect, useRef } from 'react' import { usePathname, useSearchParams } from 'next/navigation.js' import { useFathom } from '../hooks/useFathom' +import { buildTrackingUrl } from './utils' export interface NextFathomTrackViewAppProps { /** @@ -12,6 +13,22 @@ export interface NextFathomTrackViewAppProps { * @default false */ disableAutoTrack?: boolean + /** + * Transform the URL before tracking. + * Useful for stripping sensitive parameters or normalizing URLs. + * + * @example + * ```tsx + * { + * const u = new URL(url) + * u.searchParams.delete('token') + * return u.toString() + * }} + * /> + * ``` + */ + transformUrl?: (url: string) => string } /** @@ -40,6 +57,7 @@ export interface NextFathomTrackViewAppProps { */ export const NextFathomTrackViewApp: React.FC = ({ disableAutoTrack = false, + transformUrl, }) => { const pathname = usePathname() const searchParams = useSearchParams() @@ -53,25 +71,20 @@ export const NextFathomTrackViewApp: React.FC = ({ } const searchString = searchParams?.toString() - const url = + const path = pathname + (searchString !== undefined && searchString !== '' ? `?${searchString}` : '') + const url = buildTrackingUrl(path, transformUrl) + // Track initial pageview only once if (!hasTrackedInitialPageview.current) { hasTrackedInitialPageview.current = true - trackPageview({ - url: window.location.origin + url, - }) - } else { - // Track subsequent route changes - trackPageview({ - url: window.location.origin + url, - }) } - }, [pathname, searchParams, trackPageview, client, disableAutoTrack]) + trackPageview({ url }) + }, [pathname, searchParams, trackPageview, client, disableAutoTrack, transformUrl]) // This component doesn't render anything return null diff --git a/src/next/NextFathomTrackViewPages.test.tsx b/src/next/NextFathomTrackViewPages.test.tsx index 4cb7e0f..5e8647f 100644 --- a/src/next/NextFathomTrackViewPages.test.tsx +++ b/src/next/NextFathomTrackViewPages.test.tsx @@ -55,6 +55,8 @@ describe('NextFathomTrackViewPages', () => { window.location = { href: 'https://example.com/test-page', origin: 'https://example.com', + pathname: '/test-page', + search: '', } as Location }) @@ -219,6 +221,99 @@ describe('NextFathomTrackViewPages', () => { ) }) + it('should apply transformUrl to initial pageview', async () => { + // Set up window.location with a token parameter + delete (window as { location?: unknown }).location + window.location = { + href: 'https://example.com/test-page?token=secret&page=1', + origin: 'https://example.com', + pathname: '/test-page', + search: '?token=secret&page=1', + } as Location + + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const transformUrl = (url: string) => { + const u = new URL(url) + u.searchParams.delete('token') + return u.toString() + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + ) + + renderHook(() => useFathom(), { wrapper }) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + // URL should have token param stripped + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/test-page?page=1', + }) + }) + + it('should apply transformUrl to route change pageviews', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const transformUrl = (url: string) => { + const u = new URL(url) + u.searchParams.delete('token') + return u.toString() + } + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + ) + + renderHook(() => useFathom(), { wrapper }) + + await waitFor(() => { + expect(mockRouter.events.on).toHaveBeenCalled() + }) + + // Get the handler that was registered + const onCall = mockRouter.events.on.mock.calls.find( + (call) => call[0] === 'routeChangeComplete', + ) + const handler = onCall?.[1] + + // Simulate a route change with a token parameter + handler?.('/new-page?token=secret&id=123') + + expect(trackPageviewSpy).toHaveBeenLastCalledWith({ + url: 'https://example.com/new-page?id=123', + }) + }) + it('should have displayName', () => { expect(NextFathomTrackViewPages.displayName).toBe( 'NextFathomTrackViewPages', diff --git a/src/next/NextFathomTrackViewPages.tsx b/src/next/NextFathomTrackViewPages.tsx index 8eb7c95..be43a32 100644 --- a/src/next/NextFathomTrackViewPages.tsx +++ b/src/next/NextFathomTrackViewPages.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useRef } from 'react' import { useRouter } from 'next/compat/router.js' import { useFathom } from '../hooks/useFathom' +import { buildTrackingUrl } from './utils' export interface NextFathomTrackViewPagesProps { /** @@ -10,6 +11,22 @@ export interface NextFathomTrackViewPagesProps { * @default false */ disableAutoTrack?: boolean + /** + * Transform the URL before tracking. + * Useful for stripping sensitive parameters or normalizing URLs. + * + * @example + * ```tsx + * { + * const u = new URL(url) + * u.searchParams.delete('token') + * return u.toString() + * }} + * /> + * ``` + */ + transformUrl?: (url: string) => string } /** @@ -34,7 +51,7 @@ export interface NextFathomTrackViewPagesProps { */ export const NextFathomTrackViewPages: React.FC< NextFathomTrackViewPagesProps -> = ({ disableAutoTrack = false }) => { +> = ({ disableAutoTrack = false, transformUrl }) => { const hasTrackedInitialPageview = useRef(false) const { trackPageview, client } = useFathom() @@ -54,10 +71,9 @@ export const NextFathomTrackViewPages: React.FC< return } - const handleRouteChangeComplete = (url: string): void => { - trackPageview({ - url: window.location.origin + url, - }) + const handleRouteChangeComplete = (path: string): void => { + const url = buildTrackingUrl(path, transformUrl) + trackPageview({ url }) } // router.events is stable in Next.js, so we can use it without including router in dependencies @@ -67,7 +83,7 @@ export const NextFathomTrackViewPages: React.FC< router?.events?.off('routeChangeComplete', handleRouteChangeComplete) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [trackPageview, client, disableAutoTrack]) + }, [trackPageview, client, disableAutoTrack, transformUrl]) // Track initial pageview (routeChangeComplete doesn't fire on initial load) useEffect(() => { @@ -83,10 +99,12 @@ export const NextFathomTrackViewPages: React.FC< } hasTrackedInitialPageview.current = true - trackPageview({ - url: window.location.href, - }) - }, [trackPageview, client, disableAutoTrack, router]) + const url = buildTrackingUrl( + window.location.pathname + window.location.search, + transformUrl, + ) + trackPageview({ url }) + }, [trackPageview, client, disableAutoTrack, router, transformUrl]) // This component doesn't render anything return null diff --git a/src/next/utils.test.ts b/src/next/utils.test.ts new file mode 100644 index 0000000..fb23940 --- /dev/null +++ b/src/next/utils.test.ts @@ -0,0 +1,55 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import { buildTrackingUrl } from './utils' + +describe('buildTrackingUrl', () => { + beforeEach(() => { + delete (window as { location?: unknown }).location + window.location = { + origin: 'https://example.com', + } as Location + }) + + it('should build a full URL from a path', () => { + const url = buildTrackingUrl('/test-page') + expect(url).toBe('https://example.com/test-page') + }) + + it('should build a full URL from a path with query string', () => { + const url = buildTrackingUrl('/test-page?foo=bar') + expect(url).toBe('https://example.com/test-page?foo=bar') + }) + + it('should apply transformUrl when provided', () => { + const transformUrl = (url: string) => { + const u = new URL(url) + u.searchParams.delete('token') + return u.toString() + } + + const url = buildTrackingUrl('/test-page?token=secret&page=1', transformUrl) + expect(url).toBe('https://example.com/test-page?page=1') + }) + + it('should not modify URL when transformUrl is not provided', () => { + const url = buildTrackingUrl('/test-page?token=secret') + expect(url).toBe('https://example.com/test-page?token=secret') + }) + + it('should handle transformUrl that returns a different URL', () => { + const transformUrl = () => 'https://other.com/different-page' + + const url = buildTrackingUrl('/test-page', transformUrl) + expect(url).toBe('https://other.com/different-page') + }) + + it('should handle empty path', () => { + const url = buildTrackingUrl('') + expect(url).toBe('https://example.com') + }) + + it('should handle root path', () => { + const url = buildTrackingUrl('/') + expect(url).toBe('https://example.com/') + }) +}) diff --git a/src/next/utils.ts b/src/next/utils.ts new file mode 100644 index 0000000..80e5cb3 --- /dev/null +++ b/src/next/utils.ts @@ -0,0 +1,20 @@ +import { buildTrackingUrl as buildTrackingUrlFromParts } from '../utils' + +/** + * Builds a full URL for tracking, applying optional transformation. + * + * @param path - The path portion of the URL (can include query string) + * @param transformUrl - Optional function to transform the URL before tracking + * @returns The full URL ready for tracking + */ +export function buildTrackingUrl( + path: string, + transformUrl?: (url: string) => string, +): string { + // The Next.js adapter always returns a string (never null) since it runs + // client-side only and its transformUrl signature doesn't support null. + return buildTrackingUrlFromParts({ + pathname: path, + transformUrl, + }) ?? '' +} diff --git a/src/react-router/ReactRouterFathomTrackView.test.tsx b/src/react-router/ReactRouterFathomTrackView.test.tsx new file mode 100644 index 0000000..61f0842 --- /dev/null +++ b/src/react-router/ReactRouterFathomTrackView.test.tsx @@ -0,0 +1,297 @@ +import React from 'react' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { render, waitFor } from '@testing-library/react' +import { MemoryRouter, useNavigate } from 'react-router-dom' + +import { FathomProvider } from '../FathomProvider' +import { ReactRouterFathomTrackView } from './ReactRouterFathomTrackView' + +// Mock fathom-client +vi.mock('fathom-client', () => { + const mockFathomDefault = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + return { + default: mockFathomDefault, + } +}) + +// Helper component to trigger navigation +function NavigateButton({ to }: { to: string }) { + const navigate = useNavigate() + return +} + +describe('ReactRouterFathomTrackView', () => { + beforeEach(() => { + vi.clearAllMocks() + delete (window as { location?: unknown }).location + window.location = { + href: 'https://example.com/test-page', + origin: 'https://example.com', + } as Location + }) + + it('should track initial pageview on mount', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/test-page', + }) + }) + + it('should track pageviews on route changes', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const { getByText } = render( + + + + + + , + ) + + // Wait for initial pageview + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalledTimes(1) + }) + + // Navigate to new page + getByText('Navigate').click() + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalledTimes(2) + }) + + expect(trackPageviewSpy).toHaveBeenLastCalledWith({ + url: 'https://example.com/new-page', + }) + }) + + it('should not track when disableAutoTrack is true', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + + + , + ) + + // Give time for effects to run + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(trackPageviewSpy).not.toHaveBeenCalled() + }) + + it('should include search params by default', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/test-page?foo=bar', + }) + }) + + it('should exclude search params when includeSearchParams is false', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/test-page', + }) + }) + + it('should include hash when includeHash is true', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + render( + + + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/test-page#section', + }) + }) + + it('should transform URL when transformUrl is provided', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const transformUrl = (url: string) => url.replace('/test-page', '/transformed') + + render( + + + + + , + ) + + await waitFor(() => { + expect(trackPageviewSpy).toHaveBeenCalled() + }) + + expect(trackPageviewSpy).toHaveBeenCalledWith({ + url: 'https://example.com/transformed', + }) + }) + + it('should skip tracking when transformUrl returns null', async () => { + const trackPageviewSpy = vi.fn() + const client = { + trackEvent: vi.fn(), + trackPageview: trackPageviewSpy, + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + const transformUrl = () => null + + render( + + + + + , + ) + + // Give time for effects to run + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(trackPageviewSpy).not.toHaveBeenCalled() + }) + + it('should have displayName', () => { + expect(ReactRouterFathomTrackView.displayName).toBe( + 'ReactRouterFathomTrackView', + ) + }) +}) diff --git a/src/react-router/ReactRouterFathomTrackView.tsx b/src/react-router/ReactRouterFathomTrackView.tsx new file mode 100644 index 0000000..80a67fe --- /dev/null +++ b/src/react-router/ReactRouterFathomTrackView.tsx @@ -0,0 +1,151 @@ +import React, { useCallback, useEffect, useRef } from 'react' + +import { useLocation } from 'react-router-dom' + +import { useFathom } from '../hooks/useFathom' +import { buildTrackingUrl } from '../utils' + +export interface ReactRouterFathomTrackViewProps { + /** + * Disable automatic pageview tracking on route changes + * @default false + */ + disableAutoTrack?: boolean + + /** + * Include search/query parameters in the tracked URL + * @default true + */ + includeSearchParams?: boolean + + /** + * Include hash fragment in the tracked URL + * @default false + */ + includeHash?: boolean + + /** + * Custom function to transform the URL before tracking. + * Useful for removing sensitive data or normalizing URLs. + * @param url The URL that would be tracked + * @returns The transformed URL to track, or null/undefined to skip tracking + */ + transformUrl?: (url: string) => string | null | undefined +} + +/** + * Component that tracks pageviews for React Router applications. + * Compatible with React Router v6+ and Remix. + * Must be used within a FathomProvider and a React Router context. + * + * @example + * ```tsx + * // App.tsx (React Router) + * import { BrowserRouter } from 'react-router-dom' + * import { FathomProvider } from 'react-fathom' + * import { ReactRouterFathomTrackView } from 'react-fathom/react-router' + * + * function App() { + * return ( + * + * + * + * ... + * + * + * ) + * } + * ``` + * + * @example + * ```tsx + * // root.tsx (Remix) + * import { FathomProvider } from 'react-fathom' + * import { ReactRouterFathomTrackView } from 'react-fathom/react-router' + * + * export default function App() { + * return ( + * + * + * + * + * ) + * } + * ``` + * + * @example + * ```tsx + * // With URL transformation (strip sensitive params) + * { + * const urlObj = new URL(url) + * urlObj.searchParams.delete('token') + * return urlObj.toString() + * }} + * /> + * ``` + */ +export const ReactRouterFathomTrackView: React.FC< + ReactRouterFathomTrackViewProps +> = ({ + disableAutoTrack = false, + includeSearchParams = true, + includeHash = false, + transformUrl, +}) => { + const hasTrackedInitialPageview = useRef(false) + const { trackPageview, client } = useFathom() + const location = useLocation() + + // Build URL from location parts + const buildUrl = useCallback(() => { + return buildTrackingUrl({ + pathname: location.pathname, + search: location.search, + hash: location.hash, + includeSearchParams, + includeHash, + transformUrl, + }) + }, [location.pathname, location.search, location.hash, includeSearchParams, includeHash, transformUrl]) + + // Track pageviews on route changes + useEffect(() => { + if (!trackPageview || !client || disableAutoTrack) { + return + } + + // Skip initial render - handled separately + if (!hasTrackedInitialPageview.current) { + return + } + + const url = buildUrl() + if (url) { + trackPageview({ url }) + } + }, [location.pathname, location.search, location.hash, trackPageview, client, disableAutoTrack, buildUrl]) + + // Track initial pageview + useEffect(() => { + if ( + !trackPageview || + !client || + disableAutoTrack || + hasTrackedInitialPageview.current + ) { + return + } + + hasTrackedInitialPageview.current = true + const url = buildUrl() + if (url) { + trackPageview({ url }) + } + }, [trackPageview, client, disableAutoTrack, buildUrl]) + + // This component doesn't render anything + return null +} + +ReactRouterFathomTrackView.displayName = 'ReactRouterFathomTrackView' diff --git a/src/react-router/index.ts b/src/react-router/index.ts new file mode 100644 index 0000000..c651901 --- /dev/null +++ b/src/react-router/index.ts @@ -0,0 +1,2 @@ +// React Router tracking component +export * from './ReactRouterFathomTrackView' diff --git a/src/tanstack-router/TanStackRouterFathomTrackView.test.tsx b/src/tanstack-router/TanStackRouterFathomTrackView.test.tsx new file mode 100644 index 0000000..b7cbb66 --- /dev/null +++ b/src/tanstack-router/TanStackRouterFathomTrackView.test.tsx @@ -0,0 +1,238 @@ +import React from 'react' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { act, render, waitFor } from '@testing-library/react' +import { + createMemoryHistory, + createRootRoute, + createRoute, + createRouter, + Outlet, + RouterProvider, +} from '@tanstack/react-router' + +import { FathomProvider } from '../FathomProvider' +import { TanStackRouterFathomTrackView } from './TanStackRouterFathomTrackView' + +// Mock fathom-client +vi.mock('fathom-client', () => { + const mockFathomDefault = { + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), + } + + return { + default: mockFathomDefault, + } +}) + +// Create a mock client factory +const createMockClient = () => ({ + trackEvent: vi.fn(), + trackPageview: vi.fn(), + trackGoal: vi.fn(), + load: vi.fn(), + setSite: vi.fn(), + blockTrackingForMe: vi.fn(), + enableTrackingForMe: vi.fn(), + isTrackingEnabled: vi.fn(() => true), +}) + +// Create a test router setup helper that includes tracking in root component +function createTestRouter( + initialPath: string = '/', + client: ReturnType, + trackViewProps: React.ComponentProps = {}, +) { + const rootRoute = createRootRoute({ + component: () => ( + + + + + ), + }) + + const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () =>
Home
, + }) + + const testRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/test-page', + component: () =>
Test Page
, + }) + + const newRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/new-page', + component: () =>
New Page
, + }) + + const routeTree = rootRoute.addChildren([indexRoute, testRoute, newRoute]) + + const router = createRouter({ + routeTree, + history: createMemoryHistory({ + initialEntries: [initialPath], + }), + }) + + return router +} + +describe('TanStackRouterFathomTrackView', () => { + beforeEach(() => { + vi.clearAllMocks() + delete (window as { location?: unknown }).location + window.location = { + href: 'https://example.com/test-page', + origin: 'https://example.com', + } as Location + }) + + it('should track initial pageview on mount', async () => { + const client = createMockClient() + const router = createTestRouter('/test-page', client) + + render() + + await waitFor(() => { + expect(client.trackPageview).toHaveBeenCalled() + }) + + expect(client.trackPageview).toHaveBeenCalledWith({ + url: 'https://example.com/test-page', + }) + }) + + it('should track pageviews on route changes', async () => { + const client = createMockClient() + const router = createTestRouter('/', client) + + render() + + // Wait for initial pageview + await waitFor(() => { + expect(client.trackPageview).toHaveBeenCalledTimes(1) + }) + + // Navigate to new page using router + await act(async () => { + await router.navigate({ to: '/new-page' }) + }) + + await waitFor(() => { + expect(client.trackPageview).toHaveBeenCalledTimes(2) + }) + + expect(client.trackPageview).toHaveBeenLastCalledWith({ + url: 'https://example.com/new-page', + }) + }) + + it('should not track when disableAutoTrack is true', async () => { + const client = createMockClient() + const router = createTestRouter('/test-page', client, { disableAutoTrack: true }) + + render() + + // Give time for effects to run + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + }) + + expect(client.trackPageview).not.toHaveBeenCalled() + }) + + it('should include search params by default', async () => { + const client = createMockClient() + const router = createTestRouter('/test-page?foo=bar', client) + + render() + + await waitFor(() => { + expect(client.trackPageview).toHaveBeenCalled() + }) + + expect(client.trackPageview).toHaveBeenCalledWith({ + url: 'https://example.com/test-page?foo=bar', + }) + }) + + it('should exclude search params when includeSearchParams is false', async () => { + const client = createMockClient() + const router = createTestRouter('/test-page?foo=bar', client, { includeSearchParams: false }) + + render() + + await waitFor(() => { + expect(client.trackPageview).toHaveBeenCalled() + }) + + expect(client.trackPageview).toHaveBeenCalledWith({ + url: 'https://example.com/test-page', + }) + }) + + it('should include hash when includeHash is true', async () => { + const client = createMockClient() + const router = createTestRouter('/test-page#section', client, { includeHash: true }) + + render() + + await waitFor(() => { + expect(client.trackPageview).toHaveBeenCalled() + }) + + expect(client.trackPageview).toHaveBeenCalledWith({ + url: 'https://example.com/test-page#section', + }) + }) + + it('should transform URL when transformUrl is provided', async () => { + const client = createMockClient() + const transformUrl = (url: string) => url.replace('/test-page', '/transformed') + const router = createTestRouter('/test-page', client, { transformUrl }) + + render() + + await waitFor(() => { + expect(client.trackPageview).toHaveBeenCalled() + }) + + expect(client.trackPageview).toHaveBeenCalledWith({ + url: 'https://example.com/transformed', + }) + }) + + it('should skip tracking when transformUrl returns null', async () => { + const client = createMockClient() + const transformUrl = () => null + const router = createTestRouter('/test-page', client, { transformUrl }) + + render() + + // Give time for effects to run + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + }) + + expect(client.trackPageview).not.toHaveBeenCalled() + }) + + it('should have displayName', () => { + expect(TanStackRouterFathomTrackView.displayName).toBe( + 'TanStackRouterFathomTrackView', + ) + }) +}) diff --git a/src/tanstack-router/TanStackRouterFathomTrackView.tsx b/src/tanstack-router/TanStackRouterFathomTrackView.tsx new file mode 100644 index 0000000..eaed313 --- /dev/null +++ b/src/tanstack-router/TanStackRouterFathomTrackView.tsx @@ -0,0 +1,147 @@ +import React, { useCallback, useEffect, useRef } from 'react' + +import { useRouterState } from '@tanstack/react-router' + +import { useFathom } from '../hooks/useFathom' +import { buildTrackingUrl } from '../utils' + +export interface TanStackRouterFathomTrackViewProps { + /** + * Disable automatic pageview tracking on route changes + * @default false + */ + disableAutoTrack?: boolean + + /** + * Include search/query parameters in the tracked URL + * @default true + */ + includeSearchParams?: boolean + + /** + * Include hash fragment in the tracked URL + * @default false + */ + includeHash?: boolean + + /** + * Custom function to transform the URL before tracking. + * Useful for removing sensitive data or normalizing URLs. + * @param url The URL that would be tracked + * @returns The transformed URL to track, or null/undefined to skip tracking + */ + transformUrl?: (url: string) => string | null | undefined +} + +/** + * Component that tracks pageviews for TanStack Router applications. + * Must be used within a FathomProvider and a TanStack Router context. + * + * @example + * ```tsx + * import { RouterProvider, createRouter } from '@tanstack/react-router' + * import { FathomProvider } from 'react-fathom' + * import { TanStackRouterFathomTrackView } from 'react-fathom/tanstack-router' + * + * // In your root route component + * function RootComponent() { + * return ( + * + * + * + * + * ) + * } + * ``` + * + * @example + * ```tsx + * // With URL transformation (strip sensitive params) + * { + * const urlObj = new URL(url) + * urlObj.searchParams.delete('token') + * return urlObj.toString() + * }} + * /> + * ``` + * + * @example + * ```tsx + * // Normalize dynamic route segments + * { + * // /users/123 → /users/[id] + * return url.replace(/\/users\/\d+/, '/users/[id]') + * }} + * /> + * ``` + */ +export const TanStackRouterFathomTrackView: React.FC< + TanStackRouterFathomTrackViewProps +> = ({ + disableAutoTrack = false, + includeSearchParams = true, + includeHash = false, + transformUrl, +}) => { + const hasTrackedInitialPageview = useRef(false) + const { trackPageview, client } = useFathom() + + // Get location from TanStack Router state + const location = useRouterState({ select: (s) => s.location }) + + // Build URL from location parts + const buildUrl = useCallback(() => { + return buildTrackingUrl({ + pathname: location.pathname, + // TanStack Router provides search as an object, use the serialized searchStr + search: location.searchStr, + // TanStack Router's hash may not include the # prefix - buildTrackingUrl normalizes this + hash: location.hash, + includeSearchParams, + includeHash, + transformUrl, + }) + }, [location.pathname, location.searchStr, location.hash, includeSearchParams, includeHash, transformUrl]) + + // Track pageviews on route changes + useEffect(() => { + if (!trackPageview || !client || disableAutoTrack) { + return + } + + // Skip initial render - handled separately + if (!hasTrackedInitialPageview.current) { + return + } + + const url = buildUrl() + if (url) { + trackPageview({ url }) + } + }, [location.pathname, location.searchStr, location.hash, trackPageview, client, disableAutoTrack, buildUrl]) + + // Track initial pageview + useEffect(() => { + if ( + !trackPageview || + !client || + disableAutoTrack || + hasTrackedInitialPageview.current + ) { + return + } + + hasTrackedInitialPageview.current = true + const url = buildUrl() + if (url) { + trackPageview({ url }) + } + }, [trackPageview, client, disableAutoTrack, buildUrl]) + + // This component doesn't render anything + return null +} + +TanStackRouterFathomTrackView.displayName = 'TanStackRouterFathomTrackView' diff --git a/src/tanstack-router/index.ts b/src/tanstack-router/index.ts new file mode 100644 index 0000000..0049b82 --- /dev/null +++ b/src/tanstack-router/index.ts @@ -0,0 +1 @@ +export * from './TanStackRouterFathomTrackView' diff --git a/src/types.ts b/src/types.ts index 1dc0ce0..42c37f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,54 @@ import type { EventOptions, LoadOptions, PageViewOptions } from 'fathom-client' // Re-export fathom-client types for convenience export type { EventOptions, LoadOptions, PageViewOptions } +/** + * Represents a debug event emitted by FathomProvider when debug mode is enabled. + */ +export interface DebugEvent { + /** Unique identifier for this event */ + id: string + /** Timestamp when the event occurred */ + timestamp: number + /** Type of tracking call */ + type: 'pageview' | 'event' | 'goal' + /** Event name (for 'event' type) */ + eventName?: string + /** Goal code (for 'goal' type) */ + goalCode?: string + /** Goal value in cents (for 'goal' type) */ + goalCents?: number + /** URL being tracked (for 'pageview' type) */ + url?: string + /** Additional options passed to the tracking call */ + options?: PageViewOptions | EventOptions +} + +/** + * Callback function for debug events. + */ +export type DebugEventCallback = (event: DebugEvent) => void + +/** + * Options for debug mode in FathomProvider. + */ +export interface DebugOptions { + /** + * Enable debug mode. + * @default false + */ + enabled?: boolean + /** + * Log tracking calls to the console. + * @default true when debug is enabled + */ + console?: boolean + /** + * Callback fired when any tracking call is made. + * Use this to integrate with custom UI (e.g., toast notifications). + */ + onTrack?: DebugEventCallback +} + export interface FathomClient { blockTrackingForMe: () => void enableTrackingForMe: () => void @@ -28,6 +76,15 @@ export interface FathomContextInterface { client?: FathomClient defaultPageviewOptions?: PageViewOptions defaultEventOptions?: EventOptions + /** + * Subscribe to debug events. Returns an unsubscribe function. + * Only available when debug mode is enabled. + */ + subscribeToDebug?: (callback: DebugEventCallback) => () => void + /** + * Whether debug mode is enabled. + */ + debugEnabled?: boolean } export interface FathomProviderProps extends PropsWithChildren { @@ -60,4 +117,40 @@ export interface FathomProviderProps extends PropsWithChildren { siteId?: string defaultPageviewOptions?: PageViewOptions defaultEventOptions?: EventOptions + /** + * Enable debug mode to log and/or receive callbacks for all tracking calls. + * Useful for development, demos, and debugging. + * Does not block actual Fathom tracking. + * + * @example + * ```tsx + * // Simple console logging + * + * + * // Custom callback for toast notifications + * showToast(event) + * }} + * /> + * ``` + */ + debug?: DebugOptions | boolean + /** + * Callback fired when a tracking call fails. + * Useful for error monitoring and debugging. + * + * @example + * ```tsx + * { + * console.error(`Fathom ${context.method} failed:`, error) + * }} + * /> + * ``` + */ + onError?: (error: unknown, context: { method: string; args?: unknown[] }) => void } diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..b21f65a --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,142 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import { buildTrackingUrl } from './utils' + +describe('buildTrackingUrl', () => { + beforeEach(() => { + delete (window as { location?: unknown }).location + window.location = { + origin: 'https://example.com', + } as Location + }) + + it('should build a URL from pathname only', () => { + const url = buildTrackingUrl({ pathname: '/test-page' }) + expect(url).toBe('https://example.com/test-page') + }) + + it('should include search params by default', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + search: '?foo=bar', + }) + expect(url).toBe('https://example.com/test-page?foo=bar') + }) + + it('should exclude search params when includeSearchParams is false', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + search: '?foo=bar', + includeSearchParams: false, + }) + expect(url).toBe('https://example.com/test-page') + }) + + it('should exclude hash by default', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + hash: '#section', + }) + expect(url).toBe('https://example.com/test-page') + }) + + it('should include hash when includeHash is true', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + hash: '#section', + includeHash: true, + }) + expect(url).toBe('https://example.com/test-page#section') + }) + + it('should normalize hash without # prefix', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + hash: 'section', + includeHash: true, + }) + expect(url).toBe('https://example.com/test-page#section') + }) + + it('should not double # prefix on hash', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + hash: '#section', + includeHash: true, + }) + expect(url).toBe('https://example.com/test-page#section') + }) + + it('should include both search and hash', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + search: '?q=test', + hash: '#results', + includeSearchParams: true, + includeHash: true, + }) + expect(url).toBe('https://example.com/test-page?q=test#results') + }) + + it('should apply transformUrl', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + transformUrl: (u) => u.replace('/test-page', '/transformed'), + }) + expect(url).toBe('https://example.com/transformed') + }) + + it('should return null when transformUrl returns null', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + transformUrl: () => null, + }) + expect(url).toBeNull() + }) + + it('should return null when transformUrl returns undefined', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + transformUrl: () => undefined, + }) + expect(url).toBeNull() + }) + + it('should handle empty search string', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + search: '', + }) + expect(url).toBe('https://example.com/test-page') + }) + + it('should handle empty hash string', () => { + const url = buildTrackingUrl({ + pathname: '/test-page', + hash: '', + includeHash: true, + }) + expect(url).toBe('https://example.com/test-page') + }) + + it('should handle root path', () => { + const url = buildTrackingUrl({ pathname: '/' }) + expect(url).toBe('https://example.com/') + }) + + it('should handle empty path', () => { + const url = buildTrackingUrl({ pathname: '' }) + expect(url).toBe('https://example.com') + }) + + it('should return null in SSR (no window)', () => { + const origWindow = globalThis.window + // @ts-expect-error - simulating SSR + delete globalThis.window + + const url = buildTrackingUrl({ pathname: '/test' }) + expect(url).toBeNull() + + globalThis.window = origWindow + }) +}) diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..d53a9f3 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,56 @@ +export interface BuildTrackingUrlOptions { + /** The pathname portion of the URL */ + pathname: string + /** Search/query string (should include leading '?') */ + search?: string + /** Hash fragment (will be normalized to include leading '#') */ + hash?: string + /** Whether to include search params in the tracked URL @default true */ + includeSearchParams?: boolean + /** Whether to include hash fragment in the tracked URL @default false */ + includeHash?: boolean + /** Optional function to transform the URL before tracking. Return null/undefined to skip. */ + transformUrl?: (url: string) => string | null | undefined +} + +/** + * Builds a full tracking URL from location parts, with optional search/hash + * inclusion and URL transformation. + * + * Shared across router adapters (React Router, Gatsby, TanStack Router) + * to eliminate duplicated URL-building logic. + * + * @returns The built URL, or null if the URL should be skipped (SSR or transformUrl returned null) + */ +export function buildTrackingUrl(options: BuildTrackingUrlOptions): string | null { + if (typeof window === 'undefined') return null + + const { + pathname, + search, + hash, + includeSearchParams = true, + includeHash = false, + transformUrl, + } = options + + let url = window.location.origin + pathname + + if (includeSearchParams && search) { + url += search + } + + if (includeHash && hash) { + url += hash.startsWith('#') ? hash : `#${hash}` + } + + if (transformUrl) { + const transformed = transformUrl(url) + if (transformed === null || transformed === undefined) { + return null + } + url = transformed + } + + return url +} diff --git a/yarn.lock b/yarn.lock index e6beb43..8c4a517 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,9 +3,9 @@ "@acemir/cssom@^0.9.28": - version "0.9.30" - resolved "https://registry.yarnpkg.com/@acemir/cssom/-/cssom-0.9.30.tgz#78e73afd5284d2655f0a83458afefb2920d7bfba" - integrity sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg== + version "0.9.31" + resolved "https://registry.yarnpkg.com/@acemir/cssom/-/cssom-0.9.31.tgz#bd5337d290fb8be2ac18391f37386bc53778b0bc" + integrity sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA== "@adobe/css-tools@^4.4.0": version "4.4.4" @@ -39,34 +39,34 @@ resolved "https://registry.yarnpkg.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz#ad5549322dfe9d153d4b4dd6f7ff2ae234b06e24" integrity sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q== -"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" - integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== +"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7" + integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q== dependencies: - "@babel/helper-validator-identifier" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f" - integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== +"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.6.tgz#103f466803fa0f059e82ccac271475470570d74c" + integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg== "@babel/core@^7.12.10", "@babel/core@^7.24.4": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" - integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.5" - "@babel/helper-compilation-targets" "^7.27.2" - "@babel/helper-module-transforms" "^7.28.3" - "@babel/helpers" "^7.28.4" - "@babel/parser" "^7.28.5" - "@babel/template" "^7.27.2" - "@babel/traverse" "^7.28.5" - "@babel/types" "^7.28.5" + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.6.tgz#531bf883a1126e53501ba46eb3bb414047af507f" + integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/generator" "^7.28.6" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" @@ -74,13 +74,13 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" - integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== +"@babel/generator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1" + integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw== dependencies: - "@babel/parser" "^7.28.5" - "@babel/types" "^7.28.5" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" "@jridgewell/gen-mapping" "^0.3.12" "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" @@ -92,31 +92,31 @@ dependencies: "@babel/types" "^7.27.3" -"@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": - version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" - integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== +"@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2", "@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== dependencies: - "@babel/compat-data" "^7.27.2" + "@babel/compat-data" "^7.28.6" "@babel/helper-validator-option" "^7.27.1" browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3", "@babel/helper-create-class-features-plugin@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz#472d0c28028850968979ad89f173594a6995da46" - integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ== +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz#611ff5482da9ef0db6291bcd24303400bca170fb" + integrity sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" "@babel/helper-member-expression-to-functions" "^7.28.5" "@babel/helper-optimise-call-expression" "^7.27.1" - "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-replace-supers" "^7.28.6" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" - "@babel/traverse" "^7.28.5" + "@babel/traverse" "^7.28.6" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1", "@babel/helper-create-regexp-features-plugin@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz#7c1ddd64b2065c7f78034b25b43346a7e19ed997" integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== @@ -141,7 +141,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== -"@babel/helper-member-expression-to-functions@^7.27.1", "@babel/helper-member-expression-to-functions@^7.28.5": +"@babel/helper-member-expression-to-functions@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== @@ -149,22 +149,22 @@ "@babel/traverse" "^7.28.5" "@babel/types" "^7.28.5" -"@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" - integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== +"@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.27.1", "@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== dependencies: - "@babel/traverse" "^7.27.1" - "@babel/types" "^7.27.1" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" -"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" - integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3", "@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== dependencies: - "@babel/helper-module-imports" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" - "@babel/traverse" "^7.28.3" + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" "@babel/helper-optimise-call-expression@^7.27.1": version "7.27.1" @@ -173,10 +173,10 @@ dependencies: "@babel/types" "^7.27.1" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" - integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.28.6", "@babel/helper-plugin-utils@^7.8.0": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== "@babel/helper-remap-async-to-generator@^7.27.1": version "7.27.1" @@ -187,14 +187,14 @@ "@babel/helper-wrap-function" "^7.27.1" "@babel/traverse" "^7.27.1" -"@babel/helper-replace-supers@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" - integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== +"@babel/helper-replace-supers@^7.27.1", "@babel/helper-replace-supers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz#94aa9a1d7423a00aead3f204f78834ce7d53fe44" + integrity sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg== dependencies: - "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-member-expression-to-functions" "^7.28.5" "@babel/helper-optimise-call-expression" "^7.27.1" - "@babel/traverse" "^7.27.1" + "@babel/traverse" "^7.28.6" "@babel/helper-skip-transparent-expression-wrappers@^7.27.1": version "7.27.1" @@ -209,7 +209,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5": +"@babel/helper-validator-identifier@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== @@ -220,28 +220,28 @@ integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== "@babel/helper-wrap-function@^7.27.1": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz#fe4872092bc1438ffd0ce579e6f699609f9d0a7a" - integrity sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g== + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz#4e349ff9222dab69a93a019cc296cdd8442e279a" + integrity sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ== dependencies: - "@babel/template" "^7.27.2" - "@babel/traverse" "^7.28.3" - "@babel/types" "^7.28.2" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" -"@babel/helpers@^7.28.4": - version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" - integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== +"@babel/helpers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" + integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== dependencies: - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.4" + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" -"@babel/parser@^7.24.4", "@babel/parser@^7.27.2", "@babel/parser@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" - integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== +"@babel/parser@^7.24.4", "@babel/parser@^7.28.5", "@babel/parser@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" + integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== dependencies: - "@babel/types" "^7.28.5" + "@babel/types" "^7.28.6" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": version "7.28.5" @@ -274,13 +274,13 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" "@babel/plugin-transform-optional-chaining" "^7.27.1" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz#373f6e2de0016f73caf8f27004f61d167743742a" - integrity sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw== +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz#0e8289cec28baaf05d54fd08d81ae3676065f69f" + integrity sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/traverse" "^7.28.3" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/traverse" "^7.28.6" "@babel/plugin-proposal-class-properties@^7.18.6": version "7.18.6" @@ -306,26 +306,26 @@ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== -"@babel/plugin-syntax-import-assertions@^7.18.6", "@babel/plugin-syntax-import-assertions@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz#88894aefd2b03b5ee6ad1562a7c8e1587496aecd" - integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== +"@babel/plugin-syntax-import-assertions@^7.18.6", "@babel/plugin-syntax-import-assertions@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz#ae9bc1923a6ba527b70104dd2191b0cd872c8507" + integrity sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-syntax-import-attributes@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" - integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== +"@babel/plugin-syntax-import-attributes@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz#b71d5914665f60124e133696f17cd7669062c503" + integrity sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-syntax-jsx@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" - integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== +"@babel/plugin-syntax-jsx@^7.27.1", "@babel/plugin-syntax-jsx@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz#f8ca28bbd84883b5fea0e447c635b81ba73997ee" + integrity sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" @@ -334,12 +334,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-typescript@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" - integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== +"@babel/plugin-syntax-typescript@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz#c7b2ddf1d0a811145b1de800d1abd146af92e3a2" + integrity sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -356,22 +356,22 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-async-generator-functions@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz#1276e6c7285ab2cd1eccb0bc7356b7a69ff842c2" - integrity sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q== +"@babel/plugin-transform-async-generator-functions@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz#80cb86d3eaa2102e18ae90dd05ab87bdcad3877d" + integrity sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-remap-async-to-generator" "^7.27.1" - "@babel/traverse" "^7.28.0" + "@babel/traverse" "^7.28.6" -"@babel/plugin-transform-async-to-generator@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" - integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== +"@babel/plugin-transform-async-to-generator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz#bd97b42237b2d1bc90d74bcb486c39be5b4d7e77" + integrity sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g== dependencies: - "@babel/helper-module-imports" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-remap-async-to-generator" "^7.27.1" "@babel/plugin-transform-block-scoped-functions@^7.27.1": @@ -381,50 +381,50 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-block-scoping@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz#e0d3af63bd8c80de2e567e690a54e84d85eb16f6" - integrity sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g== +"@babel/plugin-transform-block-scoping@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz#e1ef5633448c24e76346125c2534eeb359699a99" + integrity sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-transform-class-properties@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" - integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== +"@babel/plugin-transform-class-properties@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz#d274a4478b6e782d9ea987fda09bdb6d28d66b72" + integrity sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-transform-class-static-block@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz#d1b8e69b54c9993bc558203e1f49bfc979bfd852" - integrity sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg== +"@babel/plugin-transform-class-static-block@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz#1257491e8259c6d125ac4d9a6f39f9d2bf3dba70" + integrity sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.28.3" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-transform-classes@^7.28.4": - version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz#75d66175486788c56728a73424d67cbc7473495c" - integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA== +"@babel/plugin-transform-classes@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz#8f6fb79ba3703978e701ce2a97e373aae7dda4b7" + integrity sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" - "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-compilation-targets" "^7.28.6" "@babel/helper-globals" "^7.28.0" - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/helper-replace-supers" "^7.27.1" - "@babel/traverse" "^7.28.4" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/traverse" "^7.28.6" -"@babel/plugin-transform-computed-properties@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" - integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== +"@babel/plugin-transform-computed-properties@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz#936824fc71c26cb5c433485776d79c8e7b0202d2" + integrity sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/template" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/template" "^7.28.6" -"@babel/plugin-transform-destructuring@^7.28.0", "@babel/plugin-transform-destructuring@^7.28.5": +"@babel/plugin-transform-destructuring@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz#b8402764df96179a2070bb7b501a1586cf8ad7a7" integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== @@ -432,13 +432,13 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/traverse" "^7.28.5" -"@babel/plugin-transform-dotall-regex@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz#aa6821de864c528b1fecf286f0a174e38e826f4d" - integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== +"@babel/plugin-transform-dotall-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz#def31ed84e0fb6e25c71e53c124e7b76a4ab8e61" + integrity sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-duplicate-keys@^7.27.1": version "7.27.1" @@ -447,13 +447,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz#5043854ca620a94149372e69030ff8cb6a9eb0ec" - integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz#e0c59ba54f1655dd682f2edf5f101b5910a8f6f3" + integrity sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-dynamic-import@^7.27.1": version "7.27.1" @@ -462,20 +462,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-explicit-resource-management@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz#45be6211b778dbf4b9d54c4e8a2b42fa72e09a1a" - integrity sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ== +"@babel/plugin-transform-explicit-resource-management@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz#dd6788f982c8b77e86779d1d029591e39d9d8be7" + integrity sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/plugin-transform-destructuring" "^7.28.0" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" -"@babel/plugin-transform-exponentiation-operator@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz#7cc90a8170e83532676cfa505278e147056e94fe" - integrity sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw== +"@babel/plugin-transform-exponentiation-operator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz#5e477eb7eafaf2ab5537a04aaafcf37e2d7f1091" + integrity sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-export-namespace-from@^7.27.1": version "7.27.1" @@ -501,12 +501,12 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/traverse" "^7.27.1" -"@babel/plugin-transform-json-strings@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz#a2e0ce6ef256376bd527f290da023983527a4f4c" - integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== +"@babel/plugin-transform-json-strings@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz#4c8c15b2dc49e285d110a4cf3dac52fd2dfc3038" + integrity sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-literals@^7.27.1": version "7.27.1" @@ -515,12 +515,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-logical-assignment-operators@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz#d028fd6db8c081dee4abebc812c2325e24a85b0e" - integrity sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA== +"@babel/plugin-transform-logical-assignment-operators@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz#53028a3d77e33c50ef30a8fce5ca17065936e605" + integrity sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-member-expression-literals@^7.27.1": version "7.27.1" @@ -537,13 +537,13 @@ "@babel/helper-module-transforms" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-commonjs@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" - integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== +"@babel/plugin-transform-modules-commonjs@^7.27.1", "@babel/plugin-transform-modules-commonjs@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz#c0232e0dfe66a734cc4ad0d5e75fc3321b6fdef1" + integrity sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA== dependencies: - "@babel/helper-module-transforms" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-modules-systemjs@^7.28.5": version "7.28.5" @@ -578,30 +578,30 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" - integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== +"@babel/plugin-transform-nullish-coalescing-operator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz#9bc62096e90ab7a887f3ca9c469f6adec5679757" + integrity sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-transform-numeric-separator@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" - integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== +"@babel/plugin-transform-numeric-separator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz#1310b0292762e7a4a335df5f580c3320ee7d9e9f" + integrity sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-transform-object-rest-spread@^7.28.4": - version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz#9ee1ceca80b3e6c4bac9247b2149e36958f7f98d" - integrity sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew== +"@babel/plugin-transform-object-rest-spread@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz#fdd4bc2d72480db6ca42aed5c051f148d7b067f7" + integrity sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA== dependencies: - "@babel/helper-compilation-targets" "^7.27.2" - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/plugin-transform-destructuring" "^7.28.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" "@babel/plugin-transform-parameters" "^7.27.7" - "@babel/traverse" "^7.28.4" + "@babel/traverse" "^7.28.6" "@babel/plugin-transform-object-super@^7.27.1": version "7.27.1" @@ -611,19 +611,19 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-replace-supers" "^7.27.1" -"@babel/plugin-transform-optional-catch-binding@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" - integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== +"@babel/plugin-transform-optional-catch-binding@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz#75107be14c78385978201a49c86414a150a20b4c" + integrity sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz#8238c785f9d5c1c515a90bf196efb50d075a4b26" - integrity sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ== +"@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz#926cf150bd421fc8362753e911b4a1b1ce4356cd" + integrity sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" "@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.27.7": @@ -633,22 +633,22 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-private-methods@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" - integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== +"@babel/plugin-transform-private-methods@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz#c76fbfef3b86c775db7f7c106fff544610bdb411" + integrity sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-transform-private-property-in-object@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" - integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== +"@babel/plugin-transform-private-property-in-object@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz#4fafef1e13129d79f1d75ac180c52aafefdb2811" + integrity sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA== dependencies: - "@babel/helper-annotate-as-pure" "^7.27.1" - "@babel/helper-create-class-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-property-literals@^7.27.1": version "7.27.1" @@ -672,15 +672,15 @@ "@babel/plugin-transform-react-jsx" "^7.27.1" "@babel/plugin-transform-react-jsx@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz#1023bc94b78b0a2d68c82b5e96aed573bcfb9db0" - integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz#f51cb70a90b9529fbb71ee1f75ea27b7078eed62" + integrity sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow== dependencies: - "@babel/helper-annotate-as-pure" "^7.27.1" - "@babel/helper-module-imports" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/plugin-syntax-jsx" "^7.27.1" - "@babel/types" "^7.27.1" + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-syntax-jsx" "^7.28.6" + "@babel/types" "^7.28.6" "@babel/plugin-transform-react-pure-annotations@^7.27.1": version "7.27.1" @@ -690,20 +690,20 @@ "@babel/helper-annotate-as-pure" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regenerator@^7.28.4": - version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz#9d3fa3bebb48ddd0091ce5729139cd99c67cea51" - integrity sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA== +"@babel/plugin-transform-regenerator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz#6ca2ed5b76cff87980f96eaacfc2ce833e8e7a1b" + integrity sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" -"@babel/plugin-transform-regexp-modifiers@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" - integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== +"@babel/plugin-transform-regexp-modifiers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz#7ef0163bd8b4a610481b2509c58cf217f065290b" + integrity sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-reserved-words@^7.27.1": version "7.27.1" @@ -731,12 +731,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-spread@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" - integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== +"@babel/plugin-transform-spread@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz#40a2b423f6db7b70f043ad027a58bcb44a9757b6" + integrity sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" "@babel/plugin-transform-sticky-regex@^7.27.1": @@ -761,15 +761,15 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-typescript@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz#441c5f9a4a1315039516c6c612fc66d5f4594e72" - integrity sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA== + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz#1e93d96da8adbefdfdade1d4956f73afa201a158" + integrity sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" - "@babel/helper-create-class-features-plugin" "^7.28.5" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" - "@babel/plugin-syntax-typescript" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.28.6" "@babel/plugin-transform-unicode-escapes@^7.27.1": version "7.27.1" @@ -778,13 +778,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-unicode-property-regex@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz#bdfe2d3170c78c5691a3c3be934c8c0087525956" - integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== +"@babel/plugin-transform-unicode-property-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz#63a7a6c21a0e75dae9b1861454111ea5caa22821" + integrity sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-unicode-regex@^7.27.1": version "7.27.1" @@ -794,83 +794,83 @@ "@babel/helper-create-regexp-features-plugin" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-unicode-sets-regex@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz#6ab706d10f801b5c72da8bb2548561fa04193cd1" - integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== +"@babel/plugin-transform-unicode-sets-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz#924912914e5df9fe615ec472f88ff4788ce04d4e" + integrity sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/preset-env@^7.12.11": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.28.5.tgz#82dd159d1563f219a1ce94324b3071eb89e280b0" - integrity sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg== + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.28.6.tgz#b4586bb59d8c61be6c58997f4912e7ea6bd17178" + integrity sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw== dependencies: - "@babel/compat-data" "^7.28.5" - "@babel/helper-compilation-targets" "^7.27.2" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/compat-data" "^7.28.6" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-validator-option" "^7.27.1" "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.28.5" "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.28.3" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.28.6" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions" "^7.27.1" - "@babel/plugin-syntax-import-attributes" "^7.27.1" + "@babel/plugin-syntax-import-assertions" "^7.28.6" + "@babel/plugin-syntax-import-attributes" "^7.28.6" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" "@babel/plugin-transform-arrow-functions" "^7.27.1" - "@babel/plugin-transform-async-generator-functions" "^7.28.0" - "@babel/plugin-transform-async-to-generator" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.28.6" + "@babel/plugin-transform-async-to-generator" "^7.28.6" "@babel/plugin-transform-block-scoped-functions" "^7.27.1" - "@babel/plugin-transform-block-scoping" "^7.28.5" - "@babel/plugin-transform-class-properties" "^7.27.1" - "@babel/plugin-transform-class-static-block" "^7.28.3" - "@babel/plugin-transform-classes" "^7.28.4" - "@babel/plugin-transform-computed-properties" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.28.6" + "@babel/plugin-transform-class-properties" "^7.28.6" + "@babel/plugin-transform-class-static-block" "^7.28.6" + "@babel/plugin-transform-classes" "^7.28.6" + "@babel/plugin-transform-computed-properties" "^7.28.6" "@babel/plugin-transform-destructuring" "^7.28.5" - "@babel/plugin-transform-dotall-regex" "^7.27.1" + "@babel/plugin-transform-dotall-regex" "^7.28.6" "@babel/plugin-transform-duplicate-keys" "^7.27.1" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.28.6" "@babel/plugin-transform-dynamic-import" "^7.27.1" - "@babel/plugin-transform-explicit-resource-management" "^7.28.0" - "@babel/plugin-transform-exponentiation-operator" "^7.28.5" + "@babel/plugin-transform-explicit-resource-management" "^7.28.6" + "@babel/plugin-transform-exponentiation-operator" "^7.28.6" "@babel/plugin-transform-export-namespace-from" "^7.27.1" "@babel/plugin-transform-for-of" "^7.27.1" "@babel/plugin-transform-function-name" "^7.27.1" - "@babel/plugin-transform-json-strings" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.28.6" "@babel/plugin-transform-literals" "^7.27.1" - "@babel/plugin-transform-logical-assignment-operators" "^7.28.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.28.6" "@babel/plugin-transform-member-expression-literals" "^7.27.1" "@babel/plugin-transform-modules-amd" "^7.27.1" - "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.28.6" "@babel/plugin-transform-modules-systemjs" "^7.28.5" "@babel/plugin-transform-modules-umd" "^7.27.1" "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" "@babel/plugin-transform-new-target" "^7.27.1" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" - "@babel/plugin-transform-numeric-separator" "^7.27.1" - "@babel/plugin-transform-object-rest-spread" "^7.28.4" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.28.6" + "@babel/plugin-transform-numeric-separator" "^7.28.6" + "@babel/plugin-transform-object-rest-spread" "^7.28.6" "@babel/plugin-transform-object-super" "^7.27.1" - "@babel/plugin-transform-optional-catch-binding" "^7.27.1" - "@babel/plugin-transform-optional-chaining" "^7.28.5" + "@babel/plugin-transform-optional-catch-binding" "^7.28.6" + "@babel/plugin-transform-optional-chaining" "^7.28.6" "@babel/plugin-transform-parameters" "^7.27.7" - "@babel/plugin-transform-private-methods" "^7.27.1" - "@babel/plugin-transform-private-property-in-object" "^7.27.1" + "@babel/plugin-transform-private-methods" "^7.28.6" + "@babel/plugin-transform-private-property-in-object" "^7.28.6" "@babel/plugin-transform-property-literals" "^7.27.1" - "@babel/plugin-transform-regenerator" "^7.28.4" - "@babel/plugin-transform-regexp-modifiers" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.28.6" + "@babel/plugin-transform-regexp-modifiers" "^7.28.6" "@babel/plugin-transform-reserved-words" "^7.27.1" "@babel/plugin-transform-shorthand-properties" "^7.27.1" - "@babel/plugin-transform-spread" "^7.27.1" + "@babel/plugin-transform-spread" "^7.28.6" "@babel/plugin-transform-sticky-regex" "^7.27.1" "@babel/plugin-transform-template-literals" "^7.27.1" "@babel/plugin-transform-typeof-symbol" "^7.27.1" "@babel/plugin-transform-unicode-escapes" "^7.27.1" - "@babel/plugin-transform-unicode-property-regex" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.28.6" "@babel/plugin-transform-unicode-regex" "^7.27.1" - "@babel/plugin-transform-unicode-sets-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.28.6" "@babel/preset-modules" "0.1.6-no-external-plugins" babel-plugin-polyfill-corejs2 "^0.4.14" babel-plugin-polyfill-corejs3 "^0.13.0" @@ -911,36 +911,36 @@ "@babel/plugin-transform-typescript" "^7.28.5" "@babel/runtime@^7.12.5": - version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" - integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== - -"@babel/template@^7.27.1", "@babel/template@^7.27.2": - version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" - integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/parser" "^7.27.2" - "@babel/types" "^7.27.1" - -"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" - integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.5" + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== + +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e" + integrity sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/generator" "^7.28.6" "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.5" - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.5" + "@babel/parser" "^7.28.6" + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" debug "^4.3.1" -"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5", "@babel/types@^7.4.4": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" - integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.4.4": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" + integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" @@ -974,9 +974,9 @@ integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== "@csstools/css-syntax-patches-for-csstree@^1.0.21": - version "1.0.23" - resolved "https://registry.yarnpkg.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.23.tgz#0642a7627f10eb7f8c95c27fffbe9f726c1ff655" - integrity sha512-YEmgyklR6l/oKUltidNVYdjSmLSW88vMsKx0pmiS3r71s8ZZRpd8A0Yf0U+6p/RzElmMnPBv27hNWjDQMSZRtQ== + version "1.0.25" + resolved "https://registry.yarnpkg.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz#200b4680988f33b07c2dfea70e6fddebaa578470" + integrity sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q== "@csstools/css-tokenizer@^3.0.4": version "3.0.4" @@ -1189,9 +1189,9 @@ levn "^0.4.1" "@exodus/bytes@^1.6.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@exodus/bytes/-/bytes-1.8.0.tgz#8382835f71db8377cf634a4ef5a71806e86ba9c7" - integrity sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ== + version "1.9.0" + resolved "https://registry.yarnpkg.com/@exodus/bytes/-/bytes-1.9.0.tgz#1644554c4103d956bf90196ae961ced85f99c972" + integrity sha512-lagqsvnk09NKogQaN/XrtlWeUF8SRhT12odMvbTIIaVObqzwAogL6jhR4DAp0gPuKoM1AOVrKUshJpRdpMFrww== "@humanfs/core@^0.19.1": version "0.19.1" @@ -1409,7 +1409,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31": +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31": version "0.3.31" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== @@ -1417,56 +1417,66 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@next/env@16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@next/env/-/env-16.1.1.tgz#3d06a470efff135746ef609cc02a4996512bd9ab" - integrity sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA== - -"@next/swc-darwin-arm64@16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz#1e7e87fd21fcabce546dfb04fb946ecd9f866917" - integrity sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA== - -"@next/swc-darwin-x64@16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz#6617d03b96bdffad7bf4df50d4a699faea0d04c3" - integrity sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw== - -"@next/swc-linux-arm64-gnu@16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz#d8337e2f4881a80221a1f56ac3f979b665b7e574" - integrity sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ== - -"@next/swc-linux-arm64-musl@16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz#6c24392824406a50f27fb9cf4e49362d4666db1c" - integrity sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg== - -"@next/swc-linux-x64-gnu@16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz#f1bd19fc7d119f27c4cf7f51915aef0f119c943f" - integrity sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ== - -"@next/swc-linux-x64-musl@16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz#20c1ea0c98988c337614ce6fda01b82300ff00fd" - integrity sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA== - -"@next/swc-win32-arm64-msvc@16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz#9d66be9dc4ebc458d445a7f6ee804f416d5c2daf" - integrity sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA== - -"@next/swc-win32-x64-msvc@16.1.1": - version "16.1.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz#96e9e335e2577481dab6fc7a2af48f3e4a28fbdb" - integrity sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw== +"@next/env@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-16.1.4.tgz#1f5155b16bad9825432b5e398b83df687b7b86f9" + integrity sha512-gkrXnZyxPUy0Gg6SrPQPccbNVLSP3vmW8LU5dwEttEEC1RwDivk8w4O+sZIjFvPrSICXyhQDCG+y3VmjlJf+9A== + +"@next/swc-darwin-arm64@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.4.tgz#2d5ee68da80c9b822edd06caa360aef1917d0f37" + integrity sha512-T8atLKuvk13XQUdVLCv1ZzMPgLPW0+DWWbHSQXs0/3TjPrKNxTmUIhOEaoEyl3Z82k8h/gEtqyuoZGv6+Ugawg== + +"@next/swc-darwin-x64@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.4.tgz#2f8d4462f48d4cb3c927de1962ca7a7b2f8a5b03" + integrity sha512-AKC/qVjUGUQDSPI6gESTx0xOnOPQ5gttogNS3o6bA83yiaSZJek0Am5yXy82F1KcZCx3DdOwdGPZpQCluonuxg== + +"@next/swc-linux-arm64-gnu@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.4.tgz#79fecac25ad4a0ee1081110f4c8863b87e754943" + integrity sha512-POQ65+pnYOkZNdngWfMEt7r53bzWiKkVNbjpmCt1Zb3V6lxJNXSsjwRuTQ8P/kguxDC8LRkqaL3vvsFrce4dMQ== + +"@next/swc-linux-arm64-musl@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.4.tgz#e9a99b1ea9a68908c3d36a847a6fe367b4fc3855" + integrity sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q== + +"@next/swc-linux-x64-gnu@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.4.tgz#4804de5f42ac8333e0049ab538473cbd996507f6" + integrity sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ== + +"@next/swc-linux-x64-musl@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.4.tgz#4aa01e59b0e0fd19ab493ee239e3904c42419ca6" + integrity sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw== + +"@next/swc-win32-arm64-msvc@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.4.tgz#67652a5c57889f44c11e145d49f777ac2e6cde58" + integrity sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw== + +"@next/swc-win32-x64-msvc@16.1.4": + version "16.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.4.tgz#3c51597eb64a96b8fcade74ab3f21ef3ad278a33" + integrity sha512-JSVlm9MDhmTXw/sO2PE/MRj+G6XOSMZB+BcZ0a7d6KwVFZVpkHcb2okyoYFBaco6LeiL53BBklRlOrDDbOeE5w== "@pkgr/core@^0.2.9": version "0.2.9" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== +"@reach/router@^1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@reach/router/-/router-1.3.4.tgz#d2574b19370a70c80480ed91f3da840136d10f8c" + integrity sha512-+mtn9wjlB9NN2CNnnC/BRYtwdKBfSyyasPYraNAyvaV1occr/5NnB4CVzjEZipNHwYebQwcndGUmpFzxAUoqSA== + dependencies: + create-react-context "0.3.0" + invariant "^2.2.3" + prop-types "^15.6.1" + react-lifecycles-compat "^3.0.4" + "@rollup/plugin-babel@^6.0.2": version "6.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz#5766913722057f28a56365bb6c1ca61306c7e527" @@ -1515,130 +1525,130 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-android-arm-eabi@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz#76e0fef6533b3ce313f969879e61e8f21f0eeb28" - integrity sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg== - -"@rollup/rollup-android-arm64@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz#d3cfc675a40bbdec97bda6d7fe3b3b05f0e1cd93" - integrity sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg== - -"@rollup/rollup-darwin-arm64@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz#eb912b8f59dd47c77b3c50a78489013b1d6772b4" - integrity sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg== - -"@rollup/rollup-darwin-x64@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz#e7d0839fdfd1276a1d34bc5ebbbd0dfd7d0b81a0" - integrity sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ== - -"@rollup/rollup-freebsd-arm64@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz#7ff8118760f7351e48fd0cd3717ff80543d6aac8" - integrity sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg== - -"@rollup/rollup-freebsd-x64@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz#49d330dadbda1d4e9b86b4a3951b59928a9489a9" - integrity sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw== - -"@rollup/rollup-linux-arm-gnueabihf@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz#98c5f1f8b9776b4a36e466e2a1c9ed1ba52ef1b6" - integrity sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ== - -"@rollup/rollup-linux-arm-musleabihf@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz#b9acecd3672e742f70b0c8a94075c816a91ff040" - integrity sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg== - -"@rollup/rollup-linux-arm64-gnu@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz#7a6ab06651bc29e18b09a50ed1a02bc972977c9b" - integrity sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ== - -"@rollup/rollup-linux-arm64-musl@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz#3c8c9072ba4a4d4ef1156b85ab9a2cbb57c1fad0" - integrity sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA== - -"@rollup/rollup-linux-loong64-gnu@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz#17a7af13530f4e4a7b12cd26276c54307a84a8b0" - integrity sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g== - -"@rollup/rollup-linux-loong64-musl@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz#5cd7a900fd7b077ecd753e34a9b7ff1157fe70c1" - integrity sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw== - -"@rollup/rollup-linux-ppc64-gnu@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz#03a097e70243ddf1c07b59d3c20f38e6f6800539" - integrity sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw== - -"@rollup/rollup-linux-ppc64-musl@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz#a5389873039d4650f35b4fa060d286392eb21a94" - integrity sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw== - -"@rollup/rollup-linux-riscv64-gnu@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz#789e60e7d6e2b76132d001ffb24ba80007fb17d0" - integrity sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw== - -"@rollup/rollup-linux-riscv64-musl@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz#3556fa88d139282e9a73c337c9a170f3c5fe7aa4" - integrity sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg== - -"@rollup/rollup-linux-s390x-gnu@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz#c085995b10143c16747a67f1a5487512b2ff04b2" - integrity sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg== - -"@rollup/rollup-linux-x64-gnu@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz#9563a5419dd2604841bad31a39ccfdd2891690fb" - integrity sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg== - -"@rollup/rollup-linux-x64-musl@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz#691bb06e6269a8959c13476b0cd2aa7458facb31" - integrity sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w== - -"@rollup/rollup-openbsd-x64@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz#223e71224746a59ce6d955bbc403577bb5a8be9d" - integrity sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg== - -"@rollup/rollup-openharmony-arm64@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz#0817e5d8ecbfeb8b7939bf58f8ce3c9dd67fce77" - integrity sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw== - -"@rollup/rollup-win32-arm64-msvc@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz#de56d8f2013c84570ef5fb917aae034abda93e4a" - integrity sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g== - -"@rollup/rollup-win32-ia32-msvc@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz#659aff5244312475aeea2c9479a6c7d397b517bf" - integrity sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA== - -"@rollup/rollup-win32-x64-gnu@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz#2cb09549cbb66c1b979f9238db6dd454cac14a88" - integrity sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg== - -"@rollup/rollup-win32-x64-msvc@4.55.1": - version "4.55.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz#f79437939020b83057faf07e98365b1fa51c458b" - integrity sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw== +"@rollup/rollup-android-arm-eabi@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz#067cfcd81f1c1bfd92aefe3ad5ef1523549d5052" + integrity sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw== + +"@rollup/rollup-android-arm64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz#85e39a44034d7d4e4fee2a1616f0bddb85a80517" + integrity sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q== + +"@rollup/rollup-darwin-arm64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz#17d92fe98f2cc277b91101eb1528b7c0b6c00c54" + integrity sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w== + +"@rollup/rollup-darwin-x64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz#89ae6c66b1451609bd1f297da9384463f628437d" + integrity sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g== + +"@rollup/rollup-freebsd-arm64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz#cdbdb9947b26e76c188a31238c10639347413628" + integrity sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ== + +"@rollup/rollup-freebsd-x64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz#9b1458d07b6e040be16ee36d308a2c9520f7f7cc" + integrity sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg== + +"@rollup/rollup-linux-arm-gnueabihf@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz#1d50ded7c965d5f125f5832c971ad5b287befef7" + integrity sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A== + +"@rollup/rollup-linux-arm-musleabihf@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz#53597e319b7e65990d3bc2a5048097384814c179" + integrity sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw== + +"@rollup/rollup-linux-arm64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz#597002909dec198ca4bdccb25f043d32db3d6283" + integrity sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ== + +"@rollup/rollup-linux-arm64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz#286f0e0f799545ce288bdc5a7c777261fcba3d54" + integrity sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA== + +"@rollup/rollup-linux-loong64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz#1fab07fa1a4f8d3697735b996517f1bae0ba101b" + integrity sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg== + +"@rollup/rollup-linux-loong64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz#efc2cb143d6c067f95205482afb177f78ed9ea3d" + integrity sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA== + +"@rollup/rollup-linux-ppc64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz#e8de8bd3463f96b92b7dfb7f151fd80ffe8a937c" + integrity sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw== + +"@rollup/rollup-linux-ppc64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz#8c508fe28a239da83b3a9da75bcf093186e064b4" + integrity sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg== + +"@rollup/rollup-linux-riscv64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz#ff6d51976e0830732880770a9e18553136b8d92b" + integrity sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew== + +"@rollup/rollup-linux-riscv64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz#325fb35eefc7e81d75478318f0deee1e4a111493" + integrity sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ== + +"@rollup/rollup-linux-s390x-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz#37410fabb5d3ba4ad34abcfbe9ba9b6288413f30" + integrity sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ== + +"@rollup/rollup-linux-x64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz#8ef907a53b2042068fc03fcc6a641e2b02276eca" + integrity sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw== + +"@rollup/rollup-linux-x64-musl@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz#61b9ba09ea219e0174b3f35a6ad2afc94bdd5662" + integrity sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA== + +"@rollup/rollup-openbsd-x64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz#fc4e54133134c1787d0b016ffdd5aeb22a5effd3" + integrity sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA== + +"@rollup/rollup-openharmony-arm64@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz#959ae225b1eeea0cc5b7c9f88e4834330fb6cd09" + integrity sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ== + +"@rollup/rollup-win32-arm64-msvc@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz#842acd38869fa1cbdbc240c76c67a86f93444c27" + integrity sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing== + +"@rollup/rollup-win32-ia32-msvc@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz#7ab654def4042df44cb29f8ed9d5044e850c66d5" + integrity sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg== + +"@rollup/rollup-win32-x64-gnu@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz#7426cdec1b01d2382ffd5cda83cbdd1c8efb3ca6" + integrity sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ== + +"@rollup/rollup-win32-x64-msvc@4.56.0": + version "4.56.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz#9eec0212732a432c71bde0350bc40b673d15b2db" + integrity sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -1707,6 +1717,49 @@ dependencies: tslib "^2.8.0" +"@tanstack/history@1.154.7": + version "1.154.7" + resolved "https://registry.yarnpkg.com/@tanstack/history/-/history-1.154.7.tgz#2e3c45ba5619078af0a288af0abf7cc096685dc4" + integrity sha512-YBgwS9qG4rs1ZY/ZrhQtjOH8BG9Qa2wf2AsxT/SnZ4HZJ1DcCEqkoiHH0yH6CYvdDit31X5HokOqQrRSsZEwGA== + +"@tanstack/react-router@^1.120.5": + version "1.154.10" + resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-1.154.10.tgz#4ce9ceda0a02168347755fcc65f0f7a42a7dc5b6" + integrity sha512-vDYLiNdR9ASDjh6/v7P4aKbBRgnAfi97sGNNFzcE6sdHOsNhVfVaI+ZFc6OqLu1f0NllXc3c06MtU3XBQ6lRGw== + dependencies: + "@tanstack/history" "1.154.7" + "@tanstack/react-store" "^0.8.0" + "@tanstack/router-core" "1.154.8" + isbot "^5.1.22" + tiny-invariant "^1.3.3" + tiny-warning "^1.0.3" + +"@tanstack/react-store@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-store/-/react-store-0.8.0.tgz#af2e73f1a466e08897349829bae1073f6172e7da" + integrity sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow== + dependencies: + "@tanstack/store" "0.8.0" + use-sync-external-store "^1.6.0" + +"@tanstack/router-core@1.154.8": + version "1.154.8" + resolved "https://registry.yarnpkg.com/@tanstack/router-core/-/router-core-1.154.8.tgz#46f781ec01c380067d800ef40a90e57550b800a7" + integrity sha512-bJvc4scMXktX2ZrO6xE8R3SC6SM9kCLu34KMlS5pFbK6C/Ry4yGgW4UWDY+Nwznbhc59Ehcw+HeBoE9WDWOzDA== + dependencies: + "@tanstack/history" "1.154.7" + "@tanstack/store" "^0.8.0" + cookie-es "^2.0.0" + seroval "^1.4.2" + seroval-plugins "^1.4.2" + tiny-invariant "^1.3.3" + tiny-warning "^1.0.3" + +"@tanstack/store@0.8.0", "@tanstack/store@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@tanstack/store/-/store-0.8.0.tgz#f1c533b9cff000fc792ed77edda178000abc9442" + integrity sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ== + "@testing-library/dom@^10.4.1": version "10.4.1" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" @@ -1734,9 +1787,9 @@ redent "^3.0.0" "@testing-library/react@^16.3.1": - version "16.3.1" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.1.tgz#60a9f1f6a930399d9e41b506a8bf68dbf4831fe8" - integrity sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw== + version "16.3.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.2.tgz#672883b7acb8e775fc0492d9e9d25e06e89786d0" + integrity sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g== dependencies: "@babel/runtime" "^7.12.5" @@ -1774,16 +1827,23 @@ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/node@*": - version "25.0.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.3.tgz#79b9ac8318f373fbfaaf6e2784893efa9701f269" - integrity sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA== + version "25.0.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7" + integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg== dependencies: undici-types "~7.16.0" -"@types/react@^19.2.7": - version "19.2.7" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.7.tgz#84e62c0f23e8e4e5ac2cadcea1ffeacccae7f62f" - integrity sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg== +"@types/reach__router@^1.3.15": + version "1.3.15" + resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.3.15.tgz#be4e23ee57786a9a16db9af3cff4c085de9e0db0" + integrity sha512-5WEHKGglRjq/Ae3F8UQxg+GYUIhTUEiyBT9GKPoOLU/vPTn8iZrRbdzxqvarOaGludIejJykHLMdOCdhgWqaxA== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^19.2.7": + version "19.2.9" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.9.tgz#84ec7669742bb3e7e2e8d6a5258d95ead7764200" + integrity sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA== dependencies: csstype "^3.2.2" @@ -1792,175 +1852,174 @@ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== -"@typescript-eslint/eslint-plugin@8.52.0", "@typescript-eslint/eslint-plugin@^8.50.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz#9a9f1d2ee974ed77a8b1bda94e77123f697ee8b4" - integrity sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q== +"@typescript-eslint/eslint-plugin@8.53.1", "@typescript-eslint/eslint-plugin@^8.50.0": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz#f6640f6f8749b71d9ab457263939e8932a3c6b46" + integrity sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.52.0" - "@typescript-eslint/type-utils" "8.52.0" - "@typescript-eslint/utils" "8.52.0" - "@typescript-eslint/visitor-keys" "8.52.0" + "@typescript-eslint/scope-manager" "8.53.1" + "@typescript-eslint/type-utils" "8.53.1" + "@typescript-eslint/utils" "8.53.1" + "@typescript-eslint/visitor-keys" "8.53.1" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.4.0" -"@typescript-eslint/parser@8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.52.0.tgz#9fae9f5f13ebb1c8f31a50c34381bfd6bf96a05f" - integrity sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg== +"@typescript-eslint/parser@8.53.1": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.53.1.tgz#58d4a70cc2daee2becf7d4521d65ea1782d6ec68" + integrity sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg== dependencies: - "@typescript-eslint/scope-manager" "8.52.0" - "@typescript-eslint/types" "8.52.0" - "@typescript-eslint/typescript-estree" "8.52.0" - "@typescript-eslint/visitor-keys" "8.52.0" + "@typescript-eslint/scope-manager" "8.53.1" + "@typescript-eslint/types" "8.53.1" + "@typescript-eslint/typescript-estree" "8.53.1" + "@typescript-eslint/visitor-keys" "8.53.1" debug "^4.4.3" -"@typescript-eslint/project-service@8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.52.0.tgz#5fb4c16af4eda6d74c70cbc62f5d3f77b96e4cbe" - integrity sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw== +"@typescript-eslint/project-service@8.53.1": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.53.1.tgz#4e47856a0b14a1ceb28b0294b4badef3be1e9734" + integrity sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.52.0" - "@typescript-eslint/types" "^8.52.0" + "@typescript-eslint/tsconfig-utils" "^8.53.1" + "@typescript-eslint/types" "^8.53.1" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.52.0.tgz#9884ff690fad30380ccabfb08af1ac200af6b4e5" - integrity sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA== +"@typescript-eslint/scope-manager@8.53.1": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz#6c4b8c82cd45ae3b365afc2373636e166743a8fa" + integrity sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ== dependencies: - "@typescript-eslint/types" "8.52.0" - "@typescript-eslint/visitor-keys" "8.52.0" + "@typescript-eslint/types" "8.53.1" + "@typescript-eslint/visitor-keys" "8.53.1" -"@typescript-eslint/tsconfig-utils@8.52.0", "@typescript-eslint/tsconfig-utils@^8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.52.0.tgz#0296751c22ed05c83787a6eaec65ae221bd8b8ed" - integrity sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg== +"@typescript-eslint/tsconfig-utils@8.53.1", "@typescript-eslint/tsconfig-utils@^8.53.1": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz#efe80b8d019cd49e5a1cf46c2eb0cd2733076424" + integrity sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA== -"@typescript-eslint/type-utils@8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.52.0.tgz#6e554113f8a074cf9b2faa818d2ebfccb867d6c5" - integrity sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ== +"@typescript-eslint/type-utils@8.53.1": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz#95de2651a96d580bf5c6c6089ddd694284d558ad" + integrity sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w== dependencies: - "@typescript-eslint/types" "8.52.0" - "@typescript-eslint/typescript-estree" "8.52.0" - "@typescript-eslint/utils" "8.52.0" + "@typescript-eslint/types" "8.53.1" + "@typescript-eslint/typescript-estree" "8.53.1" + "@typescript-eslint/utils" "8.53.1" debug "^4.4.3" ts-api-utils "^2.4.0" -"@typescript-eslint/types@8.52.0", "@typescript-eslint/types@^8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.52.0.tgz#1eb0a16b324824bc23b89d109a267c38c9213c4a" - integrity sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg== +"@typescript-eslint/types@8.53.1", "@typescript-eslint/types@^8.53.1": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.53.1.tgz#101f203f0807a63216cceceedb815fabe21d5793" + integrity sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A== -"@typescript-eslint/typescript-estree@8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.52.0.tgz#2ad7721c671be2127951286cb7f44c4ce55b0591" - integrity sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ== +"@typescript-eslint/typescript-estree@8.53.1": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz#b6dce2303c9e27e95b8dcd8c325868fff53e488f" + integrity sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg== dependencies: - "@typescript-eslint/project-service" "8.52.0" - "@typescript-eslint/tsconfig-utils" "8.52.0" - "@typescript-eslint/types" "8.52.0" - "@typescript-eslint/visitor-keys" "8.52.0" + "@typescript-eslint/project-service" "8.53.1" + "@typescript-eslint/tsconfig-utils" "8.53.1" + "@typescript-eslint/types" "8.53.1" + "@typescript-eslint/visitor-keys" "8.53.1" debug "^4.4.3" minimatch "^9.0.5" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.4.0" -"@typescript-eslint/utils@8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.52.0.tgz#b249be8264899b80d996fa353b4b84da4662f962" - integrity sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ== +"@typescript-eslint/utils@8.53.1": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.53.1.tgz#81fe6c343de288701b774f4d078382f567e6edaa" + integrity sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.52.0" - "@typescript-eslint/types" "8.52.0" - "@typescript-eslint/typescript-estree" "8.52.0" + "@typescript-eslint/scope-manager" "8.53.1" + "@typescript-eslint/types" "8.53.1" + "@typescript-eslint/typescript-estree" "8.53.1" -"@typescript-eslint/visitor-keys@8.52.0": - version "8.52.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.52.0.tgz#50361c48a6302676230fe498f80f6decce4bf673" - integrity sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ== +"@typescript-eslint/visitor-keys@8.53.1": + version "8.53.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz#405f04959be22b9be364939af8ac19c3649b6eb7" + integrity sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg== dependencies: - "@typescript-eslint/types" "8.52.0" + "@typescript-eslint/types" "8.53.1" eslint-visitor-keys "^4.2.1" "@vitest/coverage-v8@^4.0.16": - version "4.0.16" - resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-4.0.16.tgz#375fa0c1cbb357443628a1fcbf694154dfee9561" - integrity sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A== + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz#b9c4db7479acd51d5f0ced91b2853c29c3d0cda7" + integrity sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg== dependencies: "@bcoe/v8-coverage" "^1.0.2" - "@vitest/utils" "4.0.16" - ast-v8-to-istanbul "^0.3.8" + "@vitest/utils" "4.0.18" + ast-v8-to-istanbul "^0.3.10" istanbul-lib-coverage "^3.2.2" istanbul-lib-report "^3.0.1" - istanbul-lib-source-maps "^5.0.6" istanbul-reports "^3.2.0" magicast "^0.5.1" obug "^2.1.1" std-env "^3.10.0" tinyrainbow "^3.0.3" -"@vitest/expect@4.0.16": - version "4.0.16" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.0.16.tgz#3cb324c35f59ae72a9e1fb3b4f7b92e596628151" - integrity sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA== +"@vitest/expect@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.0.18.tgz#361510d99fbf20eb814222e4afcb8539d79dc94d" + integrity sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ== dependencies: "@standard-schema/spec" "^1.0.0" "@types/chai" "^5.2.2" - "@vitest/spy" "4.0.16" - "@vitest/utils" "4.0.16" + "@vitest/spy" "4.0.18" + "@vitest/utils" "4.0.18" chai "^6.2.1" tinyrainbow "^3.0.3" -"@vitest/mocker@4.0.16": - version "4.0.16" - resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.0.16.tgz#0351f17f5843b226f237f86cad7fc6dd7fd5b36d" - integrity sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg== +"@vitest/mocker@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.0.18.tgz#b9735da114ef65ea95652c5bdf13159c6fab4865" + integrity sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ== dependencies: - "@vitest/spy" "4.0.16" + "@vitest/spy" "4.0.18" estree-walker "^3.0.3" magic-string "^0.30.21" -"@vitest/pretty-format@4.0.16": - version "4.0.16" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.0.16.tgz#91893e0337dbdd6f80a89bcc9710c0d03650f090" - integrity sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA== +"@vitest/pretty-format@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.0.18.tgz#fbccd4d910774072ec15463553edb8ca5ce53218" + integrity sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw== dependencies: tinyrainbow "^3.0.3" -"@vitest/runner@4.0.16": - version "4.0.16" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.0.16.tgz#a9eb6786545727436e53eb51308abd6af8154323" - integrity sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q== +"@vitest/runner@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.0.18.tgz#c2c0a3ed226ec85e9312f9cc8c43c5b3a893a8b1" + integrity sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw== dependencies: - "@vitest/utils" "4.0.16" + "@vitest/utils" "4.0.18" pathe "^2.0.3" -"@vitest/snapshot@4.0.16": - version "4.0.16" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.0.16.tgz#6a7e41bdd3a60206c167720042c836c30dc50f3a" - integrity sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA== +"@vitest/snapshot@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.0.18.tgz#bcb40fd6d742679c2ac927ba295b66af1c6c34c5" + integrity sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA== dependencies: - "@vitest/pretty-format" "4.0.16" + "@vitest/pretty-format" "4.0.18" magic-string "^0.30.21" pathe "^2.0.3" -"@vitest/spy@4.0.16": - version "4.0.16" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.0.16.tgz#3ac2e63e3e0cf304f1a84ec086d8e36cd185fbbd" - integrity sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw== +"@vitest/spy@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.0.18.tgz#ba0f20503fb6d08baf3309d690b3efabdfa88762" + integrity sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw== -"@vitest/utils@4.0.16": - version "4.0.16" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.0.16.tgz#f789a4ef5c5b2e8eef90a4c3304678dbc6c92599" - integrity sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA== +"@vitest/utils@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.0.18.tgz#9636b16d86a4152ec68a8d6859cff702896433d4" + integrity sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA== dependencies: - "@vitest/pretty-format" "4.0.16" + "@vitest/pretty-format" "4.0.18" tinyrainbow "^3.0.3" acorn-jsx@^5.3.2: @@ -2137,7 +2196,7 @@ assertion-error@^2.0.1: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== -ast-v8-to-istanbul@^0.3.8: +ast-v8-to-istanbul@^0.3.10: version "0.3.10" resolved "https://registry.yarnpkg.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz#ceff0094c8c64b9e04393c2377fd61857429ec04" integrity sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ== @@ -2213,9 +2272,9 @@ balanced-match@^1.0.0: integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== baseline-browser-mapping@^2.8.3, baseline-browser-mapping@^2.9.0: - version "2.9.12" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.12.tgz#fbed5f37edf24b708e6e0b1fb26c70982a577dfc" - integrity sha512-Mij6Lij93pTAIsSYy5cyBQ975Qh9uLEc5rwGTpomiZeXZL9yIS6uORJakb3ScHgfs0serMMfIbXzokPMuEiRyw== + version "2.9.17" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz#9d6019766cd7eba738cb5f32c84b9f937cc87780" + integrity sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ== bcrypt-pbkdf@^1.0.0: version "1.0.2" @@ -2251,7 +2310,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -browserslist@^4.24.0, browserslist@^4.28.0: +browserslist@^4.24.0, browserslist@^4.28.1: version "4.28.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== @@ -2299,9 +2358,9 @@ callsites@^3.0.0: integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001759: - version "1.0.30001762" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz#e4dbfeda63d33258cdde93e53af2023a13ba27d4" - integrity sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw== + version "1.0.30001765" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz#4a78d8a797fd4124ebaab2043df942eb091648ee" + integrity sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ== caseless@~0.12.0: version "0.12.0" @@ -2365,12 +2424,22 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-es@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-2.0.0.tgz#ca6163d7ef8686ea6bbdd551f1de575569c1ed69" + integrity sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg== + +cookie@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + core-js-compat@^3.43.0: - version "3.47.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.47.0.tgz#698224bbdbb6f2e3f39decdda4147b161e3772a3" - integrity sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ== + version "3.48.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.48.0.tgz#7efbe1fc1cbad44008190462217cc5558adaeaa6" + integrity sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q== dependencies: - browserslist "^4.28.0" + browserslist "^4.28.1" core-util-is@1.0.2: version "1.0.2" @@ -2388,6 +2457,14 @@ coveralls@^3.1.1: minimist "^1.2.5" request "^2.88.2" +create-react-context@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c" + integrity sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw== + dependencies: + gud "^1.0.0" + warning "^4.0.3" + cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -2433,12 +2510,12 @@ dashdash@^1.12.0: assert-plus "^1.0.0" data-urls@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-6.0.0.tgz#95a7943c8ac14c1d563b771f2621cc50e8ec7744" - integrity sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA== + version "6.0.1" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-6.0.1.tgz#b448c8637997abe34978c9bfdb3d0a7778540184" + integrity sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ== dependencies: - whatwg-mimetype "^4.0.0" - whatwg-url "^15.0.0" + whatwg-mimetype "^5.0.0" + whatwg-url "^15.1.0" data-view-buffer@^1.0.2: version "1.0.2" @@ -2467,7 +2544,7 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1, debug@^4.4.3: +debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -2569,9 +2646,9 @@ ecc-jsbn@~0.1.1: safer-buffer "^2.1.0" electron-to-chromium@^1.5.263: - version "1.5.267" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7" - integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw== + version "1.5.277" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz#7164191a07bf32a7e646e68334f402dd60629821" + integrity sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw== entities@^6.0.0: version "6.0.1" @@ -2802,12 +2879,12 @@ eslint-plugin-import@^2.32.0: tsconfig-paths "^3.15.0" eslint-plugin-prettier@^5.0.1: - version "5.5.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz#9d61c4ea11de5af704d4edf108c82ccfa7f2e61c" - integrity sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg== + version "5.5.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz#9eae11593faa108859c26f9a9c367d619a0769c0" + integrity sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw== dependencies: - prettier-linter-helpers "^1.0.0" - synckit "^0.11.7" + prettier-linter-helpers "^1.0.1" + synckit "^0.11.12" eslint-plugin-react-hooks@^7.0.1: version "7.0.1" @@ -3179,6 +3256,11 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== +gud@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" + integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -3326,6 +3408,13 @@ internal-slot@^1.1.0: hasown "^2.0.2" side-channel "^1.1.0" +invariant@^2.2.3: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" @@ -3531,6 +3620,11 @@ isarray@^2.0.5: resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== +isbot@^5.1.22: + version "5.1.33" + resolved "https://registry.yarnpkg.com/isbot/-/isbot-5.1.33.tgz#99afd99be96390452dcf794d5ff97400339f40d7" + integrity sha512-P4Hgb5NqswjkI0J1CM6XKXon/sxKY1SuowE7Qx2hrBhIwICFyXy54mfgB5eMHXsbe/eStzzpbIGNOvGmz+dlKg== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -3555,15 +3649,6 @@ istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1: make-dir "^4.0.0" supports-color "^7.1.0" -istanbul-lib-source-maps@^5.0.6: - version "5.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441" - integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== - dependencies: - "@jridgewell/trace-mapping" "^0.3.23" - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - istanbul-reports@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" @@ -3767,7 +3852,7 @@ log-driver@^1.2.7: resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8" integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg== -loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -3900,25 +3985,25 @@ natural-compare@^1.4.0: integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== next@^16.1.0: - version "16.1.1" - resolved "https://registry.yarnpkg.com/next/-/next-16.1.1.tgz#4cc3477daa0fe22678532ff4778baee95842d866" - integrity sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w== + version "16.1.4" + resolved "https://registry.yarnpkg.com/next/-/next-16.1.4.tgz#d024bace2d52a2bea1dec33149b8bbd0852632c5" + integrity sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ== dependencies: - "@next/env" "16.1.1" + "@next/env" "16.1.4" "@swc/helpers" "0.5.15" baseline-browser-mapping "^2.8.3" caniuse-lite "^1.0.30001579" postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "16.1.1" - "@next/swc-darwin-x64" "16.1.1" - "@next/swc-linux-arm64-gnu" "16.1.1" - "@next/swc-linux-arm64-musl" "16.1.1" - "@next/swc-linux-x64-gnu" "16.1.1" - "@next/swc-linux-x64-musl" "16.1.1" - "@next/swc-win32-arm64-msvc" "16.1.1" - "@next/swc-win32-x64-msvc" "16.1.1" + "@next/swc-darwin-arm64" "16.1.4" + "@next/swc-darwin-x64" "16.1.4" + "@next/swc-linux-arm64-gnu" "16.1.4" + "@next/swc-linux-arm64-musl" "16.1.4" + "@next/swc-linux-x64-gnu" "16.1.4" + "@next/swc-linux-x64-musl" "16.1.4" + "@next/swc-win32-arm64-msvc" "16.1.4" + "@next/swc-win32-x64-msvc" "16.1.4" sharp "^0.34.4" node-releases@^2.0.27: @@ -4127,7 +4212,7 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-linter-helpers@^1.0.0: +prettier-linter-helpers@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz#6a31f88a4bad6c7adda253de12ba4edaea80ebcd" integrity sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg== @@ -4135,9 +4220,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^3.0.3: - version "3.7.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f" - integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA== + version "3.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" + integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== pretty-format@^27.0.2: version "27.5.1" @@ -4148,7 +4233,7 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" -prop-types@^15.8.1: +prop-types@^15.6.1, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -4198,6 +4283,26 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-router-dom@^7.0.0: + version "7.12.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.12.0.tgz#0f2a059c6b2c4ae04474fe4171c59fb48b9fb8cf" + integrity sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA== + dependencies: + react-router "7.12.0" + +react-router@7.12.0: + version "7.12.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.12.0.tgz#459a86862abbedd02e76e686751fe71f9fd73a4f" + integrity sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw== + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + react@^19.2.3: version "19.2.3" resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8" @@ -4363,37 +4468,37 @@ rollup-plugin-terser@^7.0.2: terser "^5.0.0" rollup@^4.43.0, rollup@^4.6.0: - version "4.55.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.55.1.tgz#4ec182828be440648e7ee6520dc35e9f20e05144" - integrity sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A== + version "4.56.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.56.0.tgz#65959d13cfbd7e48b8868c05165b1738f0143862" + integrity sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg== dependencies: "@types/estree" "1.0.8" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.55.1" - "@rollup/rollup-android-arm64" "4.55.1" - "@rollup/rollup-darwin-arm64" "4.55.1" - "@rollup/rollup-darwin-x64" "4.55.1" - "@rollup/rollup-freebsd-arm64" "4.55.1" - "@rollup/rollup-freebsd-x64" "4.55.1" - "@rollup/rollup-linux-arm-gnueabihf" "4.55.1" - "@rollup/rollup-linux-arm-musleabihf" "4.55.1" - "@rollup/rollup-linux-arm64-gnu" "4.55.1" - "@rollup/rollup-linux-arm64-musl" "4.55.1" - "@rollup/rollup-linux-loong64-gnu" "4.55.1" - "@rollup/rollup-linux-loong64-musl" "4.55.1" - "@rollup/rollup-linux-ppc64-gnu" "4.55.1" - "@rollup/rollup-linux-ppc64-musl" "4.55.1" - "@rollup/rollup-linux-riscv64-gnu" "4.55.1" - "@rollup/rollup-linux-riscv64-musl" "4.55.1" - "@rollup/rollup-linux-s390x-gnu" "4.55.1" - "@rollup/rollup-linux-x64-gnu" "4.55.1" - "@rollup/rollup-linux-x64-musl" "4.55.1" - "@rollup/rollup-openbsd-x64" "4.55.1" - "@rollup/rollup-openharmony-arm64" "4.55.1" - "@rollup/rollup-win32-arm64-msvc" "4.55.1" - "@rollup/rollup-win32-ia32-msvc" "4.55.1" - "@rollup/rollup-win32-x64-gnu" "4.55.1" - "@rollup/rollup-win32-x64-msvc" "4.55.1" + "@rollup/rollup-android-arm-eabi" "4.56.0" + "@rollup/rollup-android-arm64" "4.56.0" + "@rollup/rollup-darwin-arm64" "4.56.0" + "@rollup/rollup-darwin-x64" "4.56.0" + "@rollup/rollup-freebsd-arm64" "4.56.0" + "@rollup/rollup-freebsd-x64" "4.56.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.56.0" + "@rollup/rollup-linux-arm-musleabihf" "4.56.0" + "@rollup/rollup-linux-arm64-gnu" "4.56.0" + "@rollup/rollup-linux-arm64-musl" "4.56.0" + "@rollup/rollup-linux-loong64-gnu" "4.56.0" + "@rollup/rollup-linux-loong64-musl" "4.56.0" + "@rollup/rollup-linux-ppc64-gnu" "4.56.0" + "@rollup/rollup-linux-ppc64-musl" "4.56.0" + "@rollup/rollup-linux-riscv64-gnu" "4.56.0" + "@rollup/rollup-linux-riscv64-musl" "4.56.0" + "@rollup/rollup-linux-s390x-gnu" "4.56.0" + "@rollup/rollup-linux-x64-gnu" "4.56.0" + "@rollup/rollup-linux-x64-musl" "4.56.0" + "@rollup/rollup-openbsd-x64" "4.56.0" + "@rollup/rollup-openharmony-arm64" "4.56.0" + "@rollup/rollup-win32-arm64-msvc" "4.56.0" + "@rollup/rollup-win32-ia32-msvc" "4.56.0" + "@rollup/rollup-win32-x64-gnu" "4.56.0" + "@rollup/rollup-win32-x64-msvc" "4.56.0" fsevents "~2.3.2" safe-array-concat@^1.1.3: @@ -4475,6 +4580,21 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" +seroval-plugins@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/seroval-plugins/-/seroval-plugins-1.4.2.tgz#e903e9a9f4f77f82bf80714213fa4bcae3b395cf" + integrity sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA== + +seroval@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.4.2.tgz#0718ce33f0363407eee7c78272c24bc48ec940c8" + integrity sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ== + +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -4598,9 +4718,9 @@ siginfo@^2.0.0: integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== snyk@^1.437.3: - version "1.1301.2" - resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.1301.2.tgz#9e64882778cbd191393e4f85dc221c508d5f3a0b" - integrity sha512-FUTV/2WStY0GyvJROvxKPa2A3FzgVNIUB8rR2LMiR1to9onrnyO233HEIOS1HU+dJTXHFSh8z3fMze2TqPt58w== + version "1.1302.1" + resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.1302.1.tgz#bbd8522699291ff605f6450748ca9174e2f5b509" + integrity sha512-RT85Pz4N36xma7Mcob0Jno5TXu22VbVoT+7mWHk2TVkBHqsMZ8sKW55X+r+LMaT8GwlC5+cYjzw2iuv1VcPZOg== dependencies: "@sentry/node" "^7.36.0" global-agent "^3.0.0" @@ -4766,23 +4886,33 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -synckit@^0.11.7: - version "0.11.11" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.11.tgz#c0b619cf258a97faa209155d9cd1699b5c998cb0" - integrity sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw== +synckit@^0.11.12: + version "0.11.12" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.12.tgz#abe74124264fbc00a48011b0d98bdc1cffb64a7b" + integrity sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ== dependencies: "@pkgr/core" "^0.2.9" terser@^5.0.0: - version "5.44.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c" - integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw== + version "5.46.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.0.tgz#1b81e560d584bbdd74a8ede87b4d9477b0ff9695" + integrity sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.15.0" commander "^2.20.0" source-map-support "~0.5.20" +tiny-invariant@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + +tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tinybench@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" @@ -4930,14 +5060,14 @@ typed-array-length@^1.0.7: reflect.getprototypeof "^1.0.6" typescript-eslint@^8.50.0: - version "8.52.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.52.0.tgz#b8c156b6f2b4dee202a85712ff6a37f614476413" - integrity sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA== + version "8.53.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.53.1.tgz#e8d2888083af4638d2952b938d69458f54865921" + integrity sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg== dependencies: - "@typescript-eslint/eslint-plugin" "8.52.0" - "@typescript-eslint/parser" "8.52.0" - "@typescript-eslint/typescript-estree" "8.52.0" - "@typescript-eslint/utils" "8.52.0" + "@typescript-eslint/eslint-plugin" "8.53.1" + "@typescript-eslint/parser" "8.53.1" + "@typescript-eslint/typescript-estree" "8.53.1" + "@typescript-eslint/utils" "8.53.1" typescript@*: version "5.9.3" @@ -4997,6 +5127,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-sync-external-store@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -5026,17 +5161,17 @@ verror@1.10.0: fsevents "~2.3.3" vitest@^4.0.16: - version "4.0.16" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.0.16.tgz#7ceaecd4612fa6351923e842a0723c48cdfb6719" - integrity sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q== - dependencies: - "@vitest/expect" "4.0.16" - "@vitest/mocker" "4.0.16" - "@vitest/pretty-format" "4.0.16" - "@vitest/runner" "4.0.16" - "@vitest/snapshot" "4.0.16" - "@vitest/spy" "4.0.16" - "@vitest/utils" "4.0.16" + version "4.0.18" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.0.18.tgz#56f966353eca0b50f4df7540cd4350ca6d454a05" + integrity sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ== + dependencies: + "@vitest/expect" "4.0.18" + "@vitest/mocker" "4.0.18" + "@vitest/pretty-format" "4.0.18" + "@vitest/runner" "4.0.18" + "@vitest/snapshot" "4.0.18" + "@vitest/spy" "4.0.18" + "@vitest/utils" "4.0.18" es-module-lexer "^1.7.0" expect-type "^1.2.2" magic-string "^0.30.21" @@ -5058,6 +5193,13 @@ w3c-xmlserializer@^5.0.0: dependencies: xml-name-validator "^5.0.0" +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + webidl-conversions@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz#0657e571fe6f06fcb15ca50ed1fdbcb495cd1686" @@ -5068,7 +5210,12 @@ whatwg-mimetype@^4.0.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== -whatwg-url@^15.0.0, whatwg-url@^15.1.0: +whatwg-mimetype@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz#d8232895dbd527ceaee74efd4162008fb8a8cf48" + integrity sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw== + +whatwg-url@^15.1.0: version "15.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-15.1.0.tgz#5c433439b9a5789eeb3806bbd0da89a8bd40b8d7" integrity sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g== @@ -5117,9 +5264,9 @@ which-collection@^1.0.2: is-weakset "^2.0.3" which-typed-array@^1.1.16, which-typed-array@^1.1.19: - version "1.1.19" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" - integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== + version "1.1.20" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122" + integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== dependencies: available-typed-arrays "^1.0.7" call-bind "^1.0.8" @@ -5180,6 +5327,6 @@ yocto-queue@^0.1.0: integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ== "zod@^3.25.0 || ^4.0.0": - version "4.3.5" - resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.5.tgz#aeb269a6f9fc259b1212c348c7c5432aaa474d2a" - integrity sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g== + version "4.3.6" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" + integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==