├── .github └── workflows │ ├── e2e-tests.yml │ └── github-pages.yaml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── CHANGELOG ├── LICENSE ├── README.md ├── e2e └── navigation-guard.spec.ts ├── example ├── .gitignore ├── next.config.mjs ├── package.json ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.module.css │ │ ├── page.tsx │ │ ├── page1 │ │ │ └── page.tsx │ │ ├── page2 │ │ │ └── page.tsx │ │ └── page3 │ │ │ └── page.tsx │ ├── components │ │ ├── BackButton.tsx │ │ ├── ForwardButton.tsx │ │ ├── NavigationGuardToggle.tsx │ │ ├── RefreshButton.tsx │ │ └── SharedPage.tsx │ └── pages │ │ ├── _app.tsx │ │ └── pages-router │ │ ├── page1.tsx │ │ ├── page2.tsx │ │ └── page3.tsx └── tsconfig.json ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── components │ ├── InterceptAppRouterProvider.tsx │ ├── InterceptPagesRouterProvider.tsx │ ├── NavigationGuardProvider.tsx │ └── NavigationGuardProviderContext.tsx ├── hooks │ ├── useInterceptPageUnload.ts │ ├── useInterceptPopState.ts │ ├── useInterceptedAppRouter.ts │ ├── useInterceptedPagesRouter.ts │ ├── useIsomorphicLayoutEffect.ts │ └── useNavigationGuard.ts ├── index.ts ├── types.ts └── utils │ ├── debug.ts │ └── historyAugmentation.tsx └── tsconfig.json /.github/workflows/e2e-tests.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | timeout-minutes: 60 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | next-version: ["14.0", "14.1", "14.2", "15.0", "15.1", "15.2", "15.3"] 16 | node-version: [20] 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - run: corepack enable 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: "pnpm" 27 | 28 | - name: Install dependencies 29 | run: pnpm install 30 | 31 | - name: Build library 32 | run: pnpm build 33 | 34 | - name: Update Next.js version in example 35 | run: pnpm add next@~${{ matrix.next-version }} --save-exact 36 | working-directory: example 37 | 38 | - name: Build example app 39 | run: pnpm build 40 | working-directory: example 41 | 42 | - name: Run example app 43 | run: pnpm start & 44 | working-directory: example 45 | 46 | - name: Install Playwright Browsers 47 | run: pnpm exec playwright install --with-deps 48 | 49 | - name: Wait for server ready 50 | run: curl --fail --retry 10 --retry-delay 1 --retry-all-errors --silent --output /dev/null --head http://localhost:3000 51 | 52 | - name: Run Playwright tests 53 | run: PORT=3000 pnpm playwright test 54 | 55 | - uses: actions/upload-artifact@v4 56 | if: ${{ !cancelled() }} 57 | with: 58 | name: playwright-report-next-${{ matrix.next-version }} 59 | path: playwright-report/ 60 | retention-days: 30 61 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Next.js example to GitHub Pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: false 22 | 23 | jobs: 24 | # Build job 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - uses: pnpm/action-setup@v4.0.0 31 | name: Install pnpm 32 | with: 33 | run_install: false 34 | - name: Setup Node 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: "20" 38 | cache: pnpm 39 | - name: Setup Pages 40 | uses: actions/configure-pages@v5 41 | with: 42 | # Automatically inject basePath in your Next.js configuration file and disable 43 | # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). 44 | # 45 | # You may remove this line if you want to manage the configuration yourself. 46 | static_site_generator: next 47 | generator_config_file: example/next.config.mjs 48 | - name: Restore cache 49 | uses: actions/cache@v4 50 | with: 51 | path: | 52 | example/.next/cache 53 | # Generate a new cache whenever packages or source files change. 54 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 55 | # If source files changed but packages didn't, rebuild from a prior cache. 56 | restore-keys: | 57 | ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- 58 | - name: Install dependencies 59 | run: pnpm install --frozen-lockfile 60 | - name: Build the library 61 | run: pnpm run build 62 | - name: Build with Next.js 63 | working-directory: example 64 | run: pnpm run build 65 | - name: Upload artifact 66 | uses: actions/upload-pages-artifact@v3 67 | with: 68 | path: ./example/out 69 | 70 | # Deployment job 71 | deploy: 72 | environment: 73 | name: github-pages 74 | url: ${{ steps.deployment.outputs.page_url }} 75 | runs-on: ubuntu-latest 76 | needs: build 77 | steps: 78 | - name: Deploy to GitHub Pages 79 | id: deployment 80 | uses: actions/deploy-pages@v4 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | 4 | # Playwright 5 | /test-results/ 6 | /playwright-report/ 7 | /blob-report/ 8 | /playwright/.cache/ 9 | 10 | /.claude/settings.local.json 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.2] - 2024-12-11 11 | 12 | ### Fixed 13 | 14 | - deps: Support Next.js 14 / 15 as peer dependencies ([#13](https://github.com/LayerXcom/next-navigation-guard/pulls/13)). @zachelrath 15 | 16 | ## [0.1.1] - 2024-10-07 17 | 18 | ### Fixed 19 | 20 | - Fix back/forward button does not change the contents of the page ([#2](https://github.com/LayerXcom/next-navigation-guard/issues/2)). 21 | 22 | ## [0.1.0] - 2024-09-18 23 | 24 | ### Added 25 | 26 | - The initial release of next-navigation-guard. 27 | 28 | [unreleased]: https://github.com/LayerXcom/next-navigation-guard/compare/v0.1.2...HEAD 29 | [0.1.2]: https://github.com/LayerXcom/next-navigation-guard/releases/tag/v0.1.2 30 | [0.1.1]: https://github.com/LayerXcom/next-navigation-guard/releases/tag/v0.1.1 31 | [0.1.0]: https://github.com/LayerXcom/next-navigation-guard/releases/tag/v0.1.0 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 LayerX Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # next-navigation-guard 2 | 3 | You use Next.js, and you want to show "You have unsaved changes that will be lost." dialog when user leaves page? 4 | This library is just for you! 5 | 6 | ## Demo 7 | 8 | [https://layerxcom.github.io/next-navigation-guard/](https://layerxcom.github.io/next-navigation-guard/) 9 | 10 | ## How does it work? 11 | 12 | - [English Slide](https://speakerdeck.com/ypresto/cancel-next-js-page-navigation-full-throttle) 13 | - [Japanese Slide](https://speakerdeck.com/ypresto/hack-to-prevent-page-navigation-in-next-js) 14 | 15 | ## Installation 16 | 17 | ```bash 18 | npm install next-navigation-guard 19 | # or 20 | yarn install next-navigation-guard 21 | # or 22 | pnpm install next-navigation-guard 23 | ``` 24 | 25 | - App Router: app/layout.tsx 26 | 27 | ```tsx 28 | 29 | 30 | {children} 31 | 32 | 33 | ``` 34 | 35 | - Page Router: page/_app.tsx 36 | 37 | ```tsx 38 | export default function MyApp({ Component, pageProps }: AppProps) { 39 | return ( 40 | 41 | 42 | 43 | ); 44 | } 45 | ``` 46 | 47 | ## Usage 48 | 49 | - window.confirm() 50 | 51 | ```tsx 52 | useNavigationGuard({ enabled: form.changed, confirm: () => window.confirm("You have unsaved changes that will be lost.") }) 53 | ``` 54 | 55 | - Custom dialog component 56 | 57 | ```tsx 58 | const navGuard = useNavigationGuard({ enabled: form.changed }) 59 | 60 | return ( 61 | <> 62 | 63 | 64 | 65 | You have unsaved changes that will be lost. 66 | 67 | 68 | Cancel 69 | Discard 70 | 71 | 72 | 73 | ) 74 | ``` 75 | 76 | See working example in example/ directory and its `NavigationGuardToggle` component. 77 | -------------------------------------------------------------------------------- /e2e/navigation-guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Page } from "@playwright/test"; 2 | 3 | // Helper function to wait for and handle beforeunload dialog 4 | async function waitForBeforeUnloadDialog( 5 | page: Page, 6 | action: "accept" | "dismiss" = "dismiss" 7 | ) { 8 | return new Promise((resolve) => { 9 | page.once("dialog", (dialog) => { 10 | expect(dialog.type()).toBe("beforeunload"); 11 | 12 | if (action === "accept") { 13 | dialog.accept(); 14 | } else { 15 | dialog.dismiss(); 16 | } 17 | resolve(); 18 | }); 19 | }); 20 | } 21 | 22 | // Define test parameters for both routers 23 | const routers = [ 24 | { 25 | name: "App Router", 26 | routerType: "appRouter", 27 | startUrl: "/page1", 28 | linkIndex: 0, // First set of links 29 | basePath: "", 30 | }, 31 | { 32 | name: "Pages Router", 33 | routerType: "pagesRouter", 34 | startUrl: "/pages-router/page1", 35 | linkIndex: 1, // Second set of links 36 | basePath: "/pages-router", 37 | }, 38 | ]; 39 | 40 | // Parameterized tests that run for both routers 41 | routers.forEach(({ name, routerType, startUrl, linkIndex, basePath }) => { 42 | test.describe(`Navigation Guard - ${name}`, () => { 43 | test("should navigate freely when guard is disabled", async ({ page }) => { 44 | // Navigate to page1 45 | await page.goto(startUrl); 46 | await expect( 47 | page.locator(`text=Current Page: ${routerType} 1`) 48 | ).toBeVisible(); 49 | 50 | // Navigate to page 2 51 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 52 | await expect( 53 | page.locator(`text=Current Page: ${routerType} 2`) 54 | ).toBeVisible(); 55 | await expect(page).toHaveURL(`${basePath}/page2`); 56 | 57 | // Navigate back to page 1 then to page 3 58 | await page.getByRole("link", { name: "Page1" }).nth(linkIndex).click(); 59 | await expect( 60 | page.locator(`text=Current Page: ${routerType} 1`) 61 | ).toBeVisible(); 62 | await expect(page).toHaveURL(`${basePath}/page1`); 63 | 64 | await page.getByRole("link", { name: "Page3" }).nth(linkIndex).click(); 65 | await expect( 66 | page.locator(`text=Current Page: ${routerType} 3`) 67 | ).toBeVisible(); 68 | await expect(page).toHaveURL(`${basePath}/page3`); 69 | }); 70 | 71 | test("should show sync confirmation dialog when guard is enabled", async ({ 72 | page, 73 | }) => { 74 | await page.goto(startUrl); 75 | await page.waitForSelector("text=Current Page:"); 76 | 77 | // Enable navigation guard 78 | await page 79 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 80 | .check(); 81 | 82 | // Set up dialog handler 83 | page.on("dialog", (dialog) => { 84 | expect(dialog.message()).toBe( 85 | "You have unsaved changes that will be lost." 86 | ); 87 | dialog.accept(); 88 | }); 89 | 90 | // Try to navigate 91 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 92 | await expect( 93 | page.locator(`text=Current Page: ${routerType} 2`) 94 | ).toBeVisible(); 95 | await expect(page).toHaveURL(`${basePath}/page2`); 96 | }); 97 | 98 | test("should prevent navigation when sync confirmation is cancelled", async ({ 99 | page, 100 | }) => { 101 | await page.goto(startUrl); 102 | await page.waitForSelector("text=Current Page:"); 103 | 104 | // Set up dialog handler to cancel BEFORE enabling guard 105 | page.once("dialog", (dialog) => { 106 | expect(dialog.message()).toBe( 107 | "You have unsaved changes that will be lost." 108 | ); 109 | dialog.dismiss(); 110 | }); 111 | 112 | // Enable navigation guard 113 | await page 114 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 115 | .check(); 116 | 117 | // Try to navigate 118 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 119 | 120 | // Should still be on page 1 121 | await page.waitForTimeout(1000); // Give it time to not navigate 122 | await expect( 123 | page.locator(`text=Current Page: ${routerType} 1`) 124 | ).toBeVisible(); 125 | await expect(page).toHaveURL(`${basePath}/page1`); 126 | }); 127 | 128 | test("should show async confirmation UI when async mode is selected", async ({ 129 | page, 130 | }) => { 131 | await page.goto(startUrl); 132 | await page.waitForSelector("text=Current Page:"); 133 | 134 | // Enable navigation guard and select async mode 135 | await page 136 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 137 | .check(); 138 | await page.getByRole("checkbox", { name: "Use Async Confirm" }).check(); 139 | 140 | // Try to navigate 141 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 142 | 143 | // Check that confirmation UI appears 144 | await expect( 145 | page.locator("text=You have unsaved changes that will be lost.") 146 | ).toBeVisible(); 147 | await expect(page.getByRole("button", { name: "OK" })).toBeVisible(); 148 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible(); 149 | }); 150 | 151 | test("should navigate when async confirmation is accepted", async ({ 152 | page, 153 | }) => { 154 | await page.goto(startUrl); 155 | await page.waitForSelector("text=Current Page:"); 156 | 157 | // Enable navigation guard and select async mode 158 | await page 159 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 160 | .check(); 161 | await page.getByRole("checkbox", { name: "Use Async Confirm" }).check(); 162 | 163 | // Try to navigate 164 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 165 | 166 | // Accept the confirmation 167 | await page.getByRole("button", { name: "OK" }).click(); 168 | 169 | // Should navigate to page 2 170 | await expect( 171 | page.locator(`text=Current Page: ${routerType} 2`) 172 | ).toBeVisible(); 173 | await expect(page).toHaveURL(`${basePath}/page2`); 174 | }); 175 | 176 | test("should prevent navigation when async confirmation is cancelled", async ({ 177 | page, 178 | }) => { 179 | await page.goto(startUrl); 180 | await page.waitForSelector("text=Current Page:"); 181 | 182 | // Enable navigation guard and select async mode 183 | await page 184 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 185 | .check(); 186 | await page.getByRole("checkbox", { name: "Use Async Confirm" }).check(); 187 | 188 | // Try to navigate 189 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 190 | 191 | // Cancel the confirmation 192 | await page.getByRole("button", { name: "Cancel" }).click(); 193 | 194 | // Should still be on page 1 195 | await page.waitForTimeout(1000); 196 | await expect( 197 | page.locator(`text=Current Page: ${routerType} 1`) 198 | ).toBeVisible(); 199 | await expect(page).toHaveURL(`${basePath}/page1`); 200 | }); 201 | 202 | test("should guard browser back button navigation", async ({ page }) => { 203 | // Navigate to page2 first 204 | await page.goto(startUrl); 205 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 206 | await expect( 207 | page.locator(`text=Current Page: ${routerType} 2`) 208 | ).toBeVisible(); 209 | 210 | // Enable navigation guard 211 | await page 212 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 213 | .check(); 214 | 215 | // Set up dialog handler to cancel 216 | page.once("dialog", (dialog) => { 217 | expect(dialog.message()).toBe( 218 | "You have unsaved changes that will be lost." 219 | ); 220 | dialog.dismiss(); 221 | }); 222 | 223 | // Try to go back using browser back button 224 | await page.goBack(); 225 | 226 | // Should still be on page 2 227 | await page.waitForTimeout(1000); 228 | await expect( 229 | page.locator(`text=Current Page: ${routerType} 2`) 230 | ).toBeVisible(); 231 | await expect(page).toHaveURL(`${basePath}/page2`); 232 | }); 233 | 234 | test("should guard browser forward button navigation", async ({ page }) => { 235 | // Navigate to page2 then back to page1 236 | await page.goto(startUrl); 237 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 238 | await expect( 239 | page.locator(`text=Current Page: ${routerType} 2`) 240 | ).toBeVisible(); 241 | 242 | await page.goBack(); 243 | await expect( 244 | page.locator(`text=Current Page: ${routerType} 1`) 245 | ).toBeVisible(); 246 | await expect(page).toHaveURL(`${basePath}/page1`); 247 | 248 | // Enable navigation guard 249 | await page 250 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 251 | .check(); 252 | 253 | // Set up dialog handler to cancel 254 | page.once("dialog", (dialog) => { 255 | expect(dialog.message()).toBe( 256 | "You have unsaved changes that will be lost." 257 | ); 258 | dialog.dismiss(); 259 | }); 260 | 261 | // Try to go forward using browser forward button 262 | await page.goForward(); 263 | 264 | // Should still be on page 1 265 | await page.waitForTimeout(1000); 266 | await expect( 267 | page.locator(`text=Current Page: ${routerType} 1`) 268 | ).toBeVisible(); 269 | await expect(page).toHaveURL(`${basePath}/page1`); 270 | }); 271 | 272 | test("should guard router.back() navigation", async ({ page }) => { 273 | // Navigate to page2 first 274 | await page.goto(startUrl); 275 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 276 | await expect( 277 | page.locator(`text=Current Page: ${routerType} 2`) 278 | ).toBeVisible(); 279 | 280 | // Enable navigation guard 281 | await page 282 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 283 | .check(); 284 | 285 | // Set up dialog handler to cancel 286 | page.once("dialog", (dialog) => { 287 | expect(dialog.message()).toBe( 288 | "You have unsaved changes that will be lost." 289 | ); 290 | dialog.dismiss(); 291 | }); 292 | 293 | // Try to go back using router.back() button 294 | await page.getByRole("button", { name: "router.back()" }).click(); 295 | 296 | // Should still be on page 2 297 | await page.waitForTimeout(1000); 298 | await expect( 299 | page.locator(`text=Current Page: ${routerType} 2`) 300 | ).toBeVisible(); 301 | await expect(page).toHaveURL(`${basePath}/page2`); 302 | }); 303 | 304 | test("should guard router.forward() navigation", async ({ page }) => { 305 | // Navigate to page2 then back to page1 306 | await page.goto(startUrl); 307 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 308 | await expect( 309 | page.locator(`text=Current Page: ${routerType} 2`) 310 | ).toBeVisible(); 311 | 312 | await page.getByRole("button", { name: "router.back()" }).click(); 313 | await expect( 314 | page.locator(`text=Current Page: ${routerType} 1`) 315 | ).toBeVisible(); 316 | await expect(page).toHaveURL(`${basePath}/page1`); 317 | 318 | // Enable navigation guard 319 | await page 320 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 321 | .check(); 322 | 323 | // Set up dialog handler to cancel 324 | page.once("dialog", (dialog) => { 325 | expect(dialog.message()).toBe( 326 | "You have unsaved changes that will be lost." 327 | ); 328 | dialog.dismiss(); 329 | }); 330 | 331 | // Try to go forward using router.forward() button 332 | await page.getByRole("button", { name: "router.forward()" }).click(); 333 | 334 | // Should still be on page 1 335 | await page.waitForTimeout(1000); 336 | await expect( 337 | page.locator(`text=Current Page: ${routerType} 1`) 338 | ).toBeVisible(); 339 | await expect(page).toHaveURL(`${basePath}/page1`); 340 | }); 341 | 342 | test("should guard page refresh", async ({ page }) => { 343 | await page.goto(startUrl); 344 | 345 | // Enable navigation guard 346 | await page 347 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 348 | .check(); 349 | 350 | // Set up dialog handler to cancel 351 | page.once("dialog", (dialog) => { 352 | expect(dialog.message()).toBe( 353 | "You have unsaved changes that will be lost." 354 | ); 355 | dialog.dismiss(); 356 | }); 357 | 358 | // Try to refresh using router.refresh() button 359 | await page.getByRole("button", { name: "router.refresh()" }).click(); 360 | 361 | // Should still be on the same page with guard enabled 362 | await page.waitForTimeout(1000); 363 | await expect( 364 | page.locator(`text=Current Page: ${routerType} 1`) 365 | ).toBeVisible(); 366 | await expect(page).toHaveURL(startUrl); 367 | await expect( 368 | page.getByRole("checkbox", { name: "Enable Navigation Guard" }) 369 | ).toBeChecked(); 370 | }); 371 | 372 | test("should guard tab close/navigation away", async ({ page }) => { 373 | await page.goto(startUrl); 374 | 375 | // Enable navigation guard 376 | await page 377 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 378 | .check(); 379 | 380 | // Test navigation to external URL 381 | const dialogPromise = waitForBeforeUnloadDialog(page, "dismiss"); 382 | 383 | // Start navigation but don't await it 384 | const navigationPromise = page 385 | .goto("https://example.com", { waitUntil: "commit" }) 386 | .catch(() => { 387 | // Navigation will be cancelled, so we catch the error 388 | }); 389 | 390 | // Wait for dialog to be handled 391 | await dialogPromise; 392 | 393 | // Wait a bit for navigation to be cancelled 394 | await page.waitForTimeout(500); 395 | 396 | // Should still be on the original page 397 | await expect( 398 | page.locator(`text=Current Page: ${routerType} 1`) 399 | ).toBeVisible(); 400 | await expect(page).toHaveURL(startUrl); 401 | 402 | // Test closing tab/window 403 | // Note: We can't actually test page.close() as it will close the page 404 | // Instead, we'll test that beforeunload dialog appears when trying to close 405 | // This is typically tested manually or with browser-specific APIs 406 | 407 | // Test navigation via window.location 408 | const dialogPromise2 = waitForBeforeUnloadDialog(page, "dismiss"); 409 | await page.evaluate(() => { 410 | window.location.href = "https://example.com"; 411 | }); 412 | await dialogPromise2; 413 | 414 | // Should still be on the original page 415 | await page.waitForTimeout(500); 416 | await expect( 417 | page.locator(`text=Current Page: ${routerType} 1`) 418 | ).toBeVisible(); 419 | await expect(page).toHaveURL(startUrl); 420 | }); 421 | 422 | test("should allow navigation when guard accepts all navigation types", async ({ 423 | page, 424 | }) => { 425 | // Test browser back 426 | await page.goto(startUrl); 427 | await page.getByRole("link", { name: "Page2" }).nth(linkIndex).click(); 428 | await expect( 429 | page.locator(`text=Current Page: ${routerType} 2`) 430 | ).toBeVisible(); 431 | 432 | await page 433 | .getByRole("checkbox", { name: "Enable Navigation Guard" }) 434 | .check(); 435 | 436 | page.once("dialog", (dialog) => { 437 | dialog.accept(); 438 | }); 439 | 440 | await page.goBack(); 441 | await expect( 442 | page.locator(`text=Current Page: ${routerType} 1`) 443 | ).toBeVisible(); 444 | await expect(page).toHaveURL(`${basePath}/page1`); 445 | 446 | // Test router.forward() 447 | page.once("dialog", (dialog) => { 448 | dialog.accept(); 449 | }); 450 | 451 | await page.getByRole("button", { name: "router.forward()" }).click(); 452 | await expect( 453 | page.locator(`text=Current Page: ${routerType} 2`) 454 | ).toBeVisible(); 455 | await expect(page).toHaveURL(`${basePath}/page2`); 456 | }); 457 | }); 458 | }); 459 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /example/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-navigation-guard-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "14.2.11", 13 | "next-navigation-guard": "link:..", 14 | "react": "^18", 15 | "react-dom": "^18" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20", 19 | "@types/react": "^18", 20 | "@types/react-dom": "^18", 21 | "typescript": "^5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayerXcom/next-navigation-guard/d53d47395fd76a7ff9d8577480835c37af17a24a/example/src/app/favicon.ico -------------------------------------------------------------------------------- /example/src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayerXcom/next-navigation-guard/d53d47395fd76a7ff9d8577480835c37af17a24a/example/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /example/src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LayerXcom/next-navigation-guard/d53d47395fd76a7ff9d8577480835c37af17a24a/example/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /example/src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | font-family: Arial, Helvetica, sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | * { 28 | box-sizing: border-box; 29 | padding: 0; 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | text-decoration: none; 36 | } 37 | 38 | @media (prefers-color-scheme: dark) { 39 | html { 40 | color-scheme: dark; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { NavigationGuardProvider } from "next-navigation-guard"; 5 | 6 | const geistSans = localFont({ 7 | src: "./fonts/GeistVF.woff", 8 | variable: "--font-geist-sans", 9 | weight: "100 900", 10 | }); 11 | const geistMono = localFont({ 12 | src: "./fonts/GeistMonoVF.woff", 13 | variable: "--font-geist-mono", 14 | weight: "100 900", 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "next-navigation-guard Example", 19 | description: 20 | "Demo for next-navigation-guard library which provides a navigation guard for Next.js.", 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /example/src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | --gray-rgb: 0, 0, 0; 3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08); 4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05); 5 | 6 | --button-primary-hover: #383838; 7 | --button-secondary-hover: #f2f2f2; 8 | 9 | display: grid; 10 | grid-template-rows: 20px 1fr 20px; 11 | align-items: center; 12 | justify-items: center; 13 | min-height: 100svh; 14 | padding: 80px; 15 | gap: 64px; 16 | font-family: var(--font-geist-sans); 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | .page { 21 | --gray-rgb: 255, 255, 255; 22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145); 23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06); 24 | 25 | --button-primary-hover: #ccc; 26 | --button-secondary-hover: #1a1a1a; 27 | } 28 | } 29 | 30 | .main { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 32px; 34 | grid-row-start: 2; 35 | } 36 | 37 | .main ol { 38 | font-family: var(--font-geist-mono); 39 | padding-left: 0; 40 | margin: 0; 41 | font-size: 14px; 42 | line-height: 24px; 43 | letter-spacing: -0.01em; 44 | list-style-position: inside; 45 | } 46 | 47 | .main li:not(:last-of-type) { 48 | margin-bottom: 8px; 49 | } 50 | 51 | .main code { 52 | font-family: inherit; 53 | background: var(--gray-alpha-100); 54 | padding: 2px 4px; 55 | border-radius: 4px; 56 | font-weight: 600; 57 | } 58 | 59 | .ctas { 60 | display: flex; 61 | gap: 16px; 62 | } 63 | 64 | .ctas a { 65 | appearance: none; 66 | border-radius: 128px; 67 | height: 48px; 68 | padding: 0 20px; 69 | border: none; 70 | border: 1px solid transparent; 71 | transition: background 0.2s, color 0.2s, border-color 0.2s; 72 | cursor: pointer; 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | font-size: 16px; 77 | line-height: 20px; 78 | font-weight: 500; 79 | } 80 | 81 | a.primary { 82 | background: var(--foreground); 83 | color: var(--background); 84 | gap: 8px; 85 | } 86 | 87 | a.secondary { 88 | border-color: var(--gray-alpha-200); 89 | min-width: 180px; 90 | } 91 | 92 | .footer { 93 | grid-row-start: 3; 94 | display: flex; 95 | gap: 24px; 96 | } 97 | 98 | .footer a { 99 | display: flex; 100 | align-items: center; 101 | gap: 8px; 102 | } 103 | 104 | .footer img { 105 | flex-shrink: 0; 106 | } 107 | 108 | /* Enable hover only on non-touch devices */ 109 | @media (hover: hover) and (pointer: fine) { 110 | a.primary:hover { 111 | background: var(--button-primary-hover); 112 | border-color: transparent; 113 | } 114 | 115 | a.secondary:hover { 116 | background: var(--button-secondary-hover); 117 | border-color: transparent; 118 | } 119 | 120 | .footer a:hover { 121 | text-decoration: underline; 122 | text-underline-offset: 4px; 123 | } 124 | } 125 | 126 | @media (max-width: 600px) { 127 | .page { 128 | padding: 32px; 129 | padding-bottom: 80px; 130 | } 131 | 132 | .main { 133 | align-items: center; 134 | } 135 | 136 | .main ol { 137 | text-align: center; 138 | } 139 | 140 | .ctas { 141 | flex-direction: column; 142 | } 143 | 144 | .ctas a { 145 | font-size: 14px; 146 | height: 40px; 147 | padding: 0 16px; 148 | } 149 | 150 | a.secondary { 151 | min-width: auto; 152 | } 153 | 154 | .footer { 155 | flex-wrap: wrap; 156 | align-items: center; 157 | justify-content: center; 158 | } 159 | } 160 | 161 | @media (prefers-color-scheme: dark) { 162 | .logo { 163 | filter: invert(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /example/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import styles from "./page.module.css"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 |

next-navigation-guard Example

9 | 10 |
11 |
    12 |
  1. 13 | App Router 14 |
  2. 15 |
  3. 16 | Pages Router 17 |
  4. 18 |
19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /example/src/app/page1/page.tsx: -------------------------------------------------------------------------------- 1 | import { SharedPage } from "@/components/SharedPage"; 2 | 3 | export default function Page1() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /example/src/app/page2/page.tsx: -------------------------------------------------------------------------------- 1 | import { SharedPage } from "@/components/SharedPage"; 2 | 3 | export default function Page2() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /example/src/app/page3/page.tsx: -------------------------------------------------------------------------------- 1 | import { SharedPage } from "@/components/SharedPage"; 2 | 3 | export default function Page3() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /example/src/components/BackButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | export function BackButton() { 6 | const router = useRouter(); 7 | return ( 8 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /example/src/components/ForwardButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | export function ForwardButton() { 6 | const router = useRouter(); 7 | return ( 8 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /example/src/components/NavigationGuardToggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useNavigationGuard } from "next-navigation-guard"; 4 | import React, { useState } from "react"; 5 | 6 | export function NavigationGuardToggle(props: { confirm: string }) { 7 | const [enabled, setEnabled] = useState(false); 8 | const [isAsync, setIsAsync] = useState(false); 9 | 10 | const navGuard = useNavigationGuard({ 11 | enabled, 12 | confirm: isAsync ? undefined : () => window.confirm(props.confirm), 13 | }); 14 | 15 | return ( 16 |
17 |
18 | 26 |
27 | 28 |
29 | 37 |
38 | 39 | {navGuard.active && ( 40 |
41 |
{props.confirm}
42 | 43 | 44 | 47 |
48 | )} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /example/src/components/RefreshButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | export function RefreshButton() { 6 | const router = useRouter(); 7 | return ( 8 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /example/src/components/SharedPage.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import styles from "../app/page.module.css"; 3 | import { RefreshButton } from "./RefreshButton"; 4 | import { NavigationGuardToggle } from "./NavigationGuardToggle"; 5 | import { BackButton } from "./BackButton"; 6 | import { ForwardButton } from "./ForwardButton"; 7 | 8 | export function SharedPage({ 9 | current, 10 | mode, 11 | }: { 12 | current: number; 13 | mode: "appRouter" | "pagesRouter"; 14 | }) { 15 | return ( 16 |
17 |
18 |

next-navigation-guard Example

19 |
20 | Current Page: {mode} {current} 21 |
22 |
23 |
24 |
App Router
25 |
    26 |
  1. 27 | Page1 28 |
  2. 29 |
  3. 30 | Page2 31 |
  4. 32 |
  5. 33 | Page3 34 |
  6. 35 |
36 |
37 |
38 |
Pages Router
39 |
    40 |
  1. 41 | Page1 42 |
  2. 43 |
  3. 44 | Page2 45 |
  4. 46 |
  5. 47 | Page3 48 |
  6. 49 |
50 |
51 |
52 | 53 |
54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /example/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | import { NavigationGuardProvider } from "next-navigation-guard"; 3 | 4 | export default function MyApp({ Component, pageProps }: AppProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /example/src/pages/pages-router/page1.tsx: -------------------------------------------------------------------------------- 1 | import { SharedPage } from "@/components/SharedPage"; 2 | 3 | export default function Page1() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /example/src/pages/pages-router/page2.tsx: -------------------------------------------------------------------------------- 1 | import { SharedPage } from "@/components/SharedPage"; 2 | 3 | export default function Page2() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /example/src/pages/pages-router/page3.tsx: -------------------------------------------------------------------------------- 1 | import { SharedPage } from "@/components/SharedPage"; 2 | 3 | export default function Page3() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ], 23 | "paths": { 24 | "@/*": [ 25 | "./src/*" 26 | ] 27 | }, 28 | "allowJs": true 29 | }, 30 | "include": [ 31 | "next-env.d.ts", 32 | "**/*.ts", 33 | "**/*.tsx", 34 | ".next/types/**/*.ts" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-navigation-guard", 3 | "version": "0.1.2", 4 | "description": "Navigation Guard for Next.js App Router and Pages Router.", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | "import": "./dist/index.js", 10 | "require": "./dist/index.js" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "sideEffects": false, 16 | "scripts": { 17 | "build": "tsc -p .", 18 | "watch": "tsc --watch -p .", 19 | "test": "echo \"Error: no test specified\" && exit 1", 20 | "check:packaging": "pnpm dlx @arethetypeswrong/cli --pack .", 21 | "e2e": "playwright test", 22 | "e2e:ui": "playwright test --ui", 23 | "e2e:install": "playwright install" 24 | }, 25 | "keywords": [], 26 | "author": "ypresto ", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/LayerXcom/next-navigation-guard.git" 31 | }, 32 | "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1", 33 | "peerDependencies": { 34 | "next": "^14 || ^15", 35 | "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" 36 | }, 37 | "devDependencies": { 38 | "@playwright/test": "^1.52.0", 39 | "@types/node": "^20", 40 | "@types/react": "^18.3.5", 41 | "next": "^14.2.11", 42 | "playwright": "^1.52.0", 43 | "react": "^18.3.1", 44 | "tsup": "^8.2.4", 45 | "typescript": "^5.6.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | const PORT = process.env.PORT || 30000; 4 | 5 | export default defineConfig({ 6 | testDir: "./e2e", 7 | fullyParallel: true, 8 | forbidOnly: !!process.env.CI, 9 | retries: process.env.CI ? 2 : 0, 10 | workers: process.env.CI ? 2 : undefined, 11 | reporter: "html", 12 | use: { 13 | baseURL: `http://localhost:${PORT}`, 14 | trace: "on-first-retry", 15 | }, 16 | 17 | projects: [ 18 | { 19 | name: "chromium", 20 | use: { ...devices["Desktop Chrome"] }, 21 | }, 22 | { 23 | name: "firefox", 24 | use: { ...devices["Desktop Firefox"] }, 25 | }, 26 | { 27 | name: "webkit", 28 | use: { ...devices["Desktop Safari"] }, 29 | }, 30 | ], 31 | 32 | webServer: process.env.CI 33 | ? undefined 34 | : { 35 | command: `cd example && PORT=${PORT} pnpm dev`, 36 | port: Number(PORT), 37 | reuseExistingServer: false, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@playwright/test': 12 | specifier: ^1.52.0 13 | version: 1.52.0 14 | '@types/node': 15 | specifier: ^20 16 | version: 20.16.5 17 | '@types/react': 18 | specifier: ^18.3.5 19 | version: 18.3.5 20 | next: 21 | specifier: ^14.2.11 22 | version: 14.2.11(@playwright/test@1.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 23 | playwright: 24 | specifier: ^1.52.0 25 | version: 1.52.0 26 | react: 27 | specifier: ^18.3.1 28 | version: 18.3.1 29 | tsup: 30 | specifier: ^8.2.4 31 | version: 8.2.4(postcss@8.4.31)(typescript@5.6.2) 32 | typescript: 33 | specifier: ^5.6.2 34 | version: 5.6.2 35 | 36 | example: 37 | dependencies: 38 | next: 39 | specifier: 14.2.11 40 | version: 14.2.11(@playwright/test@1.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 41 | next-navigation-guard: 42 | specifier: link:.. 43 | version: link:.. 44 | react: 45 | specifier: ^18 46 | version: 18.3.1 47 | react-dom: 48 | specifier: ^18 49 | version: 18.3.1(react@18.3.1) 50 | devDependencies: 51 | '@types/node': 52 | specifier: ^20 53 | version: 20.16.5 54 | '@types/react': 55 | specifier: ^18 56 | version: 18.3.5 57 | '@types/react-dom': 58 | specifier: ^18 59 | version: 18.3.0 60 | typescript: 61 | specifier: ^5 62 | version: 5.6.2 63 | 64 | packages: 65 | 66 | '@esbuild/aix-ppc64@0.23.1': 67 | resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} 68 | engines: {node: '>=18'} 69 | cpu: [ppc64] 70 | os: [aix] 71 | 72 | '@esbuild/android-arm64@0.23.1': 73 | resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} 74 | engines: {node: '>=18'} 75 | cpu: [arm64] 76 | os: [android] 77 | 78 | '@esbuild/android-arm@0.23.1': 79 | resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} 80 | engines: {node: '>=18'} 81 | cpu: [arm] 82 | os: [android] 83 | 84 | '@esbuild/android-x64@0.23.1': 85 | resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} 86 | engines: {node: '>=18'} 87 | cpu: [x64] 88 | os: [android] 89 | 90 | '@esbuild/darwin-arm64@0.23.1': 91 | resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} 92 | engines: {node: '>=18'} 93 | cpu: [arm64] 94 | os: [darwin] 95 | 96 | '@esbuild/darwin-x64@0.23.1': 97 | resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} 98 | engines: {node: '>=18'} 99 | cpu: [x64] 100 | os: [darwin] 101 | 102 | '@esbuild/freebsd-arm64@0.23.1': 103 | resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} 104 | engines: {node: '>=18'} 105 | cpu: [arm64] 106 | os: [freebsd] 107 | 108 | '@esbuild/freebsd-x64@0.23.1': 109 | resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} 110 | engines: {node: '>=18'} 111 | cpu: [x64] 112 | os: [freebsd] 113 | 114 | '@esbuild/linux-arm64@0.23.1': 115 | resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} 116 | engines: {node: '>=18'} 117 | cpu: [arm64] 118 | os: [linux] 119 | 120 | '@esbuild/linux-arm@0.23.1': 121 | resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} 122 | engines: {node: '>=18'} 123 | cpu: [arm] 124 | os: [linux] 125 | 126 | '@esbuild/linux-ia32@0.23.1': 127 | resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} 128 | engines: {node: '>=18'} 129 | cpu: [ia32] 130 | os: [linux] 131 | 132 | '@esbuild/linux-loong64@0.23.1': 133 | resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} 134 | engines: {node: '>=18'} 135 | cpu: [loong64] 136 | os: [linux] 137 | 138 | '@esbuild/linux-mips64el@0.23.1': 139 | resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} 140 | engines: {node: '>=18'} 141 | cpu: [mips64el] 142 | os: [linux] 143 | 144 | '@esbuild/linux-ppc64@0.23.1': 145 | resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} 146 | engines: {node: '>=18'} 147 | cpu: [ppc64] 148 | os: [linux] 149 | 150 | '@esbuild/linux-riscv64@0.23.1': 151 | resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} 152 | engines: {node: '>=18'} 153 | cpu: [riscv64] 154 | os: [linux] 155 | 156 | '@esbuild/linux-s390x@0.23.1': 157 | resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} 158 | engines: {node: '>=18'} 159 | cpu: [s390x] 160 | os: [linux] 161 | 162 | '@esbuild/linux-x64@0.23.1': 163 | resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} 164 | engines: {node: '>=18'} 165 | cpu: [x64] 166 | os: [linux] 167 | 168 | '@esbuild/netbsd-x64@0.23.1': 169 | resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} 170 | engines: {node: '>=18'} 171 | cpu: [x64] 172 | os: [netbsd] 173 | 174 | '@esbuild/openbsd-arm64@0.23.1': 175 | resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} 176 | engines: {node: '>=18'} 177 | cpu: [arm64] 178 | os: [openbsd] 179 | 180 | '@esbuild/openbsd-x64@0.23.1': 181 | resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} 182 | engines: {node: '>=18'} 183 | cpu: [x64] 184 | os: [openbsd] 185 | 186 | '@esbuild/sunos-x64@0.23.1': 187 | resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} 188 | engines: {node: '>=18'} 189 | cpu: [x64] 190 | os: [sunos] 191 | 192 | '@esbuild/win32-arm64@0.23.1': 193 | resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} 194 | engines: {node: '>=18'} 195 | cpu: [arm64] 196 | os: [win32] 197 | 198 | '@esbuild/win32-ia32@0.23.1': 199 | resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} 200 | engines: {node: '>=18'} 201 | cpu: [ia32] 202 | os: [win32] 203 | 204 | '@esbuild/win32-x64@0.23.1': 205 | resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} 206 | engines: {node: '>=18'} 207 | cpu: [x64] 208 | os: [win32] 209 | 210 | '@isaacs/cliui@8.0.2': 211 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 212 | engines: {node: '>=12'} 213 | 214 | '@jridgewell/gen-mapping@0.3.5': 215 | resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 216 | engines: {node: '>=6.0.0'} 217 | 218 | '@jridgewell/resolve-uri@3.1.2': 219 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 220 | engines: {node: '>=6.0.0'} 221 | 222 | '@jridgewell/set-array@1.2.1': 223 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 224 | engines: {node: '>=6.0.0'} 225 | 226 | '@jridgewell/sourcemap-codec@1.5.0': 227 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 228 | 229 | '@jridgewell/trace-mapping@0.3.25': 230 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 231 | 232 | '@next/env@14.2.11': 233 | resolution: {integrity: sha512-HYsQRSIXwiNqvzzYThrBwq6RhXo3E0n8j8nQnAs8i4fCEo2Zf/3eS0IiRA8XnRg9Ha0YnpkyJZIZg1qEwemrHw==} 234 | 235 | '@next/swc-darwin-arm64@14.2.11': 236 | resolution: {integrity: sha512-eiY9u7wEJZWp/Pga07Qy3ZmNEfALmmSS1HtsJF3y1QEyaExu7boENz11fWqDmZ3uvcyAxCMhTrA1jfVxITQW8g==} 237 | engines: {node: '>= 10'} 238 | cpu: [arm64] 239 | os: [darwin] 240 | 241 | '@next/swc-darwin-x64@14.2.11': 242 | resolution: {integrity: sha512-lnB0zYCld4yE0IX3ANrVMmtAbziBb7MYekcmR6iE9bujmgERl6+FK+b0MBq0pl304lYe7zO4yxJus9H/Af8jbg==} 243 | engines: {node: '>= 10'} 244 | cpu: [x64] 245 | os: [darwin] 246 | 247 | '@next/swc-linux-arm64-gnu@14.2.11': 248 | resolution: {integrity: sha512-Ulo9TZVocYmUAtzvZ7FfldtwUoQY0+9z3BiXZCLSUwU2bp7GqHA7/bqrfsArDlUb2xeGwn3ZuBbKtNK8TR0A8w==} 249 | engines: {node: '>= 10'} 250 | cpu: [arm64] 251 | os: [linux] 252 | 253 | '@next/swc-linux-arm64-musl@14.2.11': 254 | resolution: {integrity: sha512-fH377DnKGyUnkWlmUpFF1T90m0dADBfK11dF8sOQkiELF9M+YwDRCGe8ZyDzvQcUd20Rr5U7vpZRrAxKwd3Rzg==} 255 | engines: {node: '>= 10'} 256 | cpu: [arm64] 257 | os: [linux] 258 | 259 | '@next/swc-linux-x64-gnu@14.2.11': 260 | resolution: {integrity: sha512-a0TH4ZZp4NS0LgXP/488kgvWelNpwfgGTUCDXVhPGH6pInb7yIYNgM4kmNWOxBFt+TIuOH6Pi9NnGG4XWFUyXQ==} 261 | engines: {node: '>= 10'} 262 | cpu: [x64] 263 | os: [linux] 264 | 265 | '@next/swc-linux-x64-musl@14.2.11': 266 | resolution: {integrity: sha512-DYYZcO4Uir2gZxA4D2JcOAKVs8ZxbOFYPpXSVIgeoQbREbeEHxysVsg3nY4FrQy51e5opxt5mOHl/LzIyZBoKA==} 267 | engines: {node: '>= 10'} 268 | cpu: [x64] 269 | os: [linux] 270 | 271 | '@next/swc-win32-arm64-msvc@14.2.11': 272 | resolution: {integrity: sha512-PwqHeKG3/kKfPpM6of1B9UJ+Er6ySUy59PeFu0Un0LBzJTRKKAg2V6J60Yqzp99m55mLa+YTbU6xj61ImTv9mg==} 273 | engines: {node: '>= 10'} 274 | cpu: [arm64] 275 | os: [win32] 276 | 277 | '@next/swc-win32-ia32-msvc@14.2.11': 278 | resolution: {integrity: sha512-0U7PWMnOYIvM74GY6rbH6w7v+vNPDVH1gUhlwHpfInJnNe5LkmUZqhp7FNWeNa5wbVgRcRi1F1cyxp4dmeLLvA==} 279 | engines: {node: '>= 10'} 280 | cpu: [ia32] 281 | os: [win32] 282 | 283 | '@next/swc-win32-x64-msvc@14.2.11': 284 | resolution: {integrity: sha512-gQpS7mcgovWoaTG1FbS5/ojF7CGfql1Q0ZLsMrhcsi2Sr9HEqsUZ70MPJyaYBXbk6iEAP7UXMD9HC8KY1qNwvA==} 285 | engines: {node: '>= 10'} 286 | cpu: [x64] 287 | os: [win32] 288 | 289 | '@nodelib/fs.scandir@2.1.5': 290 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 291 | engines: {node: '>= 8'} 292 | 293 | '@nodelib/fs.stat@2.0.5': 294 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 295 | engines: {node: '>= 8'} 296 | 297 | '@nodelib/fs.walk@1.2.8': 298 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 299 | engines: {node: '>= 8'} 300 | 301 | '@pkgjs/parseargs@0.11.0': 302 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 303 | engines: {node: '>=14'} 304 | 305 | '@playwright/test@1.52.0': 306 | resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==} 307 | engines: {node: '>=18'} 308 | hasBin: true 309 | 310 | '@rollup/rollup-android-arm-eabi@4.21.3': 311 | resolution: {integrity: sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==} 312 | cpu: [arm] 313 | os: [android] 314 | 315 | '@rollup/rollup-android-arm64@4.21.3': 316 | resolution: {integrity: sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==} 317 | cpu: [arm64] 318 | os: [android] 319 | 320 | '@rollup/rollup-darwin-arm64@4.21.3': 321 | resolution: {integrity: sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==} 322 | cpu: [arm64] 323 | os: [darwin] 324 | 325 | '@rollup/rollup-darwin-x64@4.21.3': 326 | resolution: {integrity: sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==} 327 | cpu: [x64] 328 | os: [darwin] 329 | 330 | '@rollup/rollup-linux-arm-gnueabihf@4.21.3': 331 | resolution: {integrity: sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==} 332 | cpu: [arm] 333 | os: [linux] 334 | 335 | '@rollup/rollup-linux-arm-musleabihf@4.21.3': 336 | resolution: {integrity: sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==} 337 | cpu: [arm] 338 | os: [linux] 339 | 340 | '@rollup/rollup-linux-arm64-gnu@4.21.3': 341 | resolution: {integrity: sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==} 342 | cpu: [arm64] 343 | os: [linux] 344 | 345 | '@rollup/rollup-linux-arm64-musl@4.21.3': 346 | resolution: {integrity: sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==} 347 | cpu: [arm64] 348 | os: [linux] 349 | 350 | '@rollup/rollup-linux-powerpc64le-gnu@4.21.3': 351 | resolution: {integrity: sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==} 352 | cpu: [ppc64] 353 | os: [linux] 354 | 355 | '@rollup/rollup-linux-riscv64-gnu@4.21.3': 356 | resolution: {integrity: sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==} 357 | cpu: [riscv64] 358 | os: [linux] 359 | 360 | '@rollup/rollup-linux-s390x-gnu@4.21.3': 361 | resolution: {integrity: sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==} 362 | cpu: [s390x] 363 | os: [linux] 364 | 365 | '@rollup/rollup-linux-x64-gnu@4.21.3': 366 | resolution: {integrity: sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==} 367 | cpu: [x64] 368 | os: [linux] 369 | 370 | '@rollup/rollup-linux-x64-musl@4.21.3': 371 | resolution: {integrity: sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==} 372 | cpu: [x64] 373 | os: [linux] 374 | 375 | '@rollup/rollup-win32-arm64-msvc@4.21.3': 376 | resolution: {integrity: sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==} 377 | cpu: [arm64] 378 | os: [win32] 379 | 380 | '@rollup/rollup-win32-ia32-msvc@4.21.3': 381 | resolution: {integrity: sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==} 382 | cpu: [ia32] 383 | os: [win32] 384 | 385 | '@rollup/rollup-win32-x64-msvc@4.21.3': 386 | resolution: {integrity: sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==} 387 | cpu: [x64] 388 | os: [win32] 389 | 390 | '@swc/counter@0.1.3': 391 | resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} 392 | 393 | '@swc/helpers@0.5.5': 394 | resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} 395 | 396 | '@types/estree@1.0.5': 397 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 398 | 399 | '@types/node@20.16.5': 400 | resolution: {integrity: sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==} 401 | 402 | '@types/prop-types@15.7.12': 403 | resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} 404 | 405 | '@types/react-dom@18.3.0': 406 | resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} 407 | 408 | '@types/react@18.3.5': 409 | resolution: {integrity: sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==} 410 | 411 | ansi-regex@5.0.1: 412 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 413 | engines: {node: '>=8'} 414 | 415 | ansi-regex@6.1.0: 416 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 417 | engines: {node: '>=12'} 418 | 419 | ansi-styles@4.3.0: 420 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 421 | engines: {node: '>=8'} 422 | 423 | ansi-styles@6.2.1: 424 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 425 | engines: {node: '>=12'} 426 | 427 | any-promise@1.3.0: 428 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 429 | 430 | anymatch@3.1.3: 431 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 432 | engines: {node: '>= 8'} 433 | 434 | array-union@2.1.0: 435 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 436 | engines: {node: '>=8'} 437 | 438 | balanced-match@1.0.2: 439 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 440 | 441 | binary-extensions@2.3.0: 442 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 443 | engines: {node: '>=8'} 444 | 445 | brace-expansion@2.0.1: 446 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 447 | 448 | braces@3.0.3: 449 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 450 | engines: {node: '>=8'} 451 | 452 | bundle-require@5.0.0: 453 | resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==} 454 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 455 | peerDependencies: 456 | esbuild: '>=0.18' 457 | 458 | busboy@1.6.0: 459 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} 460 | engines: {node: '>=10.16.0'} 461 | 462 | cac@6.7.14: 463 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 464 | engines: {node: '>=8'} 465 | 466 | caniuse-lite@1.0.30001660: 467 | resolution: {integrity: sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==} 468 | 469 | chokidar@3.6.0: 470 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 471 | engines: {node: '>= 8.10.0'} 472 | 473 | client-only@0.0.1: 474 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} 475 | 476 | color-convert@2.0.1: 477 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 478 | engines: {node: '>=7.0.0'} 479 | 480 | color-name@1.1.4: 481 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 482 | 483 | commander@4.1.1: 484 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 485 | engines: {node: '>= 6'} 486 | 487 | consola@3.2.3: 488 | resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} 489 | engines: {node: ^14.18.0 || >=16.10.0} 490 | 491 | cross-spawn@7.0.3: 492 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 493 | engines: {node: '>= 8'} 494 | 495 | csstype@3.1.3: 496 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 497 | 498 | debug@4.3.7: 499 | resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} 500 | engines: {node: '>=6.0'} 501 | peerDependencies: 502 | supports-color: '*' 503 | peerDependenciesMeta: 504 | supports-color: 505 | optional: true 506 | 507 | dir-glob@3.0.1: 508 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 509 | engines: {node: '>=8'} 510 | 511 | eastasianwidth@0.2.0: 512 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 513 | 514 | emoji-regex@8.0.0: 515 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 516 | 517 | emoji-regex@9.2.2: 518 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 519 | 520 | esbuild@0.23.1: 521 | resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} 522 | engines: {node: '>=18'} 523 | hasBin: true 524 | 525 | execa@5.1.1: 526 | resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} 527 | engines: {node: '>=10'} 528 | 529 | fast-glob@3.3.2: 530 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 531 | engines: {node: '>=8.6.0'} 532 | 533 | fastq@1.17.1: 534 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 535 | 536 | fill-range@7.1.1: 537 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 538 | engines: {node: '>=8'} 539 | 540 | foreground-child@3.3.0: 541 | resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 542 | engines: {node: '>=14'} 543 | 544 | fsevents@2.3.2: 545 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 546 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 547 | os: [darwin] 548 | 549 | fsevents@2.3.3: 550 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 551 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 552 | os: [darwin] 553 | 554 | get-stream@6.0.1: 555 | resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} 556 | engines: {node: '>=10'} 557 | 558 | glob-parent@5.1.2: 559 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 560 | engines: {node: '>= 6'} 561 | 562 | glob@10.4.5: 563 | resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 564 | hasBin: true 565 | 566 | globby@11.1.0: 567 | resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 568 | engines: {node: '>=10'} 569 | 570 | graceful-fs@4.2.11: 571 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 572 | 573 | human-signals@2.1.0: 574 | resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} 575 | engines: {node: '>=10.17.0'} 576 | 577 | ignore@5.3.2: 578 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 579 | engines: {node: '>= 4'} 580 | 581 | is-binary-path@2.1.0: 582 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 583 | engines: {node: '>=8'} 584 | 585 | is-extglob@2.1.1: 586 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 587 | engines: {node: '>=0.10.0'} 588 | 589 | is-fullwidth-code-point@3.0.0: 590 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 591 | engines: {node: '>=8'} 592 | 593 | is-glob@4.0.3: 594 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 595 | engines: {node: '>=0.10.0'} 596 | 597 | is-number@7.0.0: 598 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 599 | engines: {node: '>=0.12.0'} 600 | 601 | is-stream@2.0.1: 602 | resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 603 | engines: {node: '>=8'} 604 | 605 | isexe@2.0.0: 606 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 607 | 608 | jackspeak@3.4.3: 609 | resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 610 | 611 | joycon@3.1.1: 612 | resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} 613 | engines: {node: '>=10'} 614 | 615 | js-tokens@4.0.0: 616 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 617 | 618 | lilconfig@3.1.2: 619 | resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} 620 | engines: {node: '>=14'} 621 | 622 | lines-and-columns@1.2.4: 623 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 624 | 625 | load-tsconfig@0.2.5: 626 | resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} 627 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 628 | 629 | lodash.sortby@4.7.0: 630 | resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} 631 | 632 | loose-envify@1.4.0: 633 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 634 | hasBin: true 635 | 636 | lru-cache@10.4.3: 637 | resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 638 | 639 | merge-stream@2.0.0: 640 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 641 | 642 | merge2@1.4.1: 643 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 644 | engines: {node: '>= 8'} 645 | 646 | micromatch@4.0.8: 647 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 648 | engines: {node: '>=8.6'} 649 | 650 | mimic-fn@2.1.0: 651 | resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} 652 | engines: {node: '>=6'} 653 | 654 | minimatch@9.0.5: 655 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 656 | engines: {node: '>=16 || 14 >=14.17'} 657 | 658 | minipass@7.1.2: 659 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 660 | engines: {node: '>=16 || 14 >=14.17'} 661 | 662 | ms@2.1.3: 663 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 664 | 665 | mz@2.7.0: 666 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 667 | 668 | nanoid@3.3.7: 669 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 670 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 671 | hasBin: true 672 | 673 | next@14.2.11: 674 | resolution: {integrity: sha512-8MDFqHBhdmR2wdfaWc8+lW3A/hppFe1ggQ9vgIu/g2/2QEMYJrPoQP6b+VNk56gIug/bStysAmrpUKtj3XN8Bw==} 675 | engines: {node: '>=18.17.0'} 676 | hasBin: true 677 | peerDependencies: 678 | '@opentelemetry/api': ^1.1.0 679 | '@playwright/test': ^1.41.2 680 | react: ^18.2.0 681 | react-dom: ^18.2.0 682 | sass: ^1.3.0 683 | peerDependenciesMeta: 684 | '@opentelemetry/api': 685 | optional: true 686 | '@playwright/test': 687 | optional: true 688 | sass: 689 | optional: true 690 | 691 | normalize-path@3.0.0: 692 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 693 | engines: {node: '>=0.10.0'} 694 | 695 | npm-run-path@4.0.1: 696 | resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} 697 | engines: {node: '>=8'} 698 | 699 | object-assign@4.1.1: 700 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 701 | engines: {node: '>=0.10.0'} 702 | 703 | onetime@5.1.2: 704 | resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} 705 | engines: {node: '>=6'} 706 | 707 | package-json-from-dist@1.0.0: 708 | resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} 709 | 710 | path-key@3.1.1: 711 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 712 | engines: {node: '>=8'} 713 | 714 | path-scurry@1.11.1: 715 | resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 716 | engines: {node: '>=16 || 14 >=14.18'} 717 | 718 | path-type@4.0.0: 719 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 720 | engines: {node: '>=8'} 721 | 722 | picocolors@1.1.0: 723 | resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} 724 | 725 | picomatch@2.3.1: 726 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 727 | engines: {node: '>=8.6'} 728 | 729 | pirates@4.0.6: 730 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 731 | engines: {node: '>= 6'} 732 | 733 | playwright-core@1.52.0: 734 | resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} 735 | engines: {node: '>=18'} 736 | hasBin: true 737 | 738 | playwright@1.52.0: 739 | resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} 740 | engines: {node: '>=18'} 741 | hasBin: true 742 | 743 | postcss-load-config@6.0.1: 744 | resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} 745 | engines: {node: '>= 18'} 746 | peerDependencies: 747 | jiti: '>=1.21.0' 748 | postcss: '>=8.0.9' 749 | tsx: ^4.8.1 750 | yaml: ^2.4.2 751 | peerDependenciesMeta: 752 | jiti: 753 | optional: true 754 | postcss: 755 | optional: true 756 | tsx: 757 | optional: true 758 | yaml: 759 | optional: true 760 | 761 | postcss@8.4.31: 762 | resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} 763 | engines: {node: ^10 || ^12 || >=14} 764 | 765 | punycode@2.3.1: 766 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 767 | engines: {node: '>=6'} 768 | 769 | queue-microtask@1.2.3: 770 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 771 | 772 | react-dom@18.3.1: 773 | resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} 774 | peerDependencies: 775 | react: ^18.3.1 776 | 777 | react@18.3.1: 778 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 779 | engines: {node: '>=0.10.0'} 780 | 781 | readdirp@3.6.0: 782 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 783 | engines: {node: '>=8.10.0'} 784 | 785 | resolve-from@5.0.0: 786 | resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 787 | engines: {node: '>=8'} 788 | 789 | reusify@1.0.4: 790 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 791 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 792 | 793 | rollup@4.21.3: 794 | resolution: {integrity: sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==} 795 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 796 | hasBin: true 797 | 798 | run-parallel@1.2.0: 799 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 800 | 801 | scheduler@0.23.2: 802 | resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} 803 | 804 | shebang-command@2.0.0: 805 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 806 | engines: {node: '>=8'} 807 | 808 | shebang-regex@3.0.0: 809 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 810 | engines: {node: '>=8'} 811 | 812 | signal-exit@3.0.7: 813 | resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 814 | 815 | signal-exit@4.1.0: 816 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 817 | engines: {node: '>=14'} 818 | 819 | slash@3.0.0: 820 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 821 | engines: {node: '>=8'} 822 | 823 | source-map-js@1.2.1: 824 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 825 | engines: {node: '>=0.10.0'} 826 | 827 | source-map@0.8.0-beta.0: 828 | resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} 829 | engines: {node: '>= 8'} 830 | 831 | streamsearch@1.1.0: 832 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} 833 | engines: {node: '>=10.0.0'} 834 | 835 | string-width@4.2.3: 836 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 837 | engines: {node: '>=8'} 838 | 839 | string-width@5.1.2: 840 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 841 | engines: {node: '>=12'} 842 | 843 | strip-ansi@6.0.1: 844 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 845 | engines: {node: '>=8'} 846 | 847 | strip-ansi@7.1.0: 848 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 849 | engines: {node: '>=12'} 850 | 851 | strip-final-newline@2.0.0: 852 | resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} 853 | engines: {node: '>=6'} 854 | 855 | styled-jsx@5.1.1: 856 | resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} 857 | engines: {node: '>= 12.0.0'} 858 | peerDependencies: 859 | '@babel/core': '*' 860 | babel-plugin-macros: '*' 861 | react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' 862 | peerDependenciesMeta: 863 | '@babel/core': 864 | optional: true 865 | babel-plugin-macros: 866 | optional: true 867 | 868 | sucrase@3.35.0: 869 | resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 870 | engines: {node: '>=16 || 14 >=14.17'} 871 | hasBin: true 872 | 873 | thenify-all@1.6.0: 874 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 875 | engines: {node: '>=0.8'} 876 | 877 | thenify@3.3.1: 878 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 879 | 880 | to-regex-range@5.0.1: 881 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 882 | engines: {node: '>=8.0'} 883 | 884 | tr46@1.0.1: 885 | resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} 886 | 887 | tree-kill@1.2.2: 888 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 889 | hasBin: true 890 | 891 | ts-interface-checker@0.1.13: 892 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 893 | 894 | tslib@2.8.1: 895 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 896 | 897 | tsup@8.2.4: 898 | resolution: {integrity: sha512-akpCPePnBnC/CXgRrcy72ZSntgIEUa1jN0oJbbvpALWKNOz1B7aM+UVDWGRGIO/T/PZugAESWDJUAb5FD48o8Q==} 899 | engines: {node: '>=18'} 900 | hasBin: true 901 | peerDependencies: 902 | '@microsoft/api-extractor': ^7.36.0 903 | '@swc/core': ^1 904 | postcss: ^8.4.12 905 | typescript: '>=4.5.0' 906 | peerDependenciesMeta: 907 | '@microsoft/api-extractor': 908 | optional: true 909 | '@swc/core': 910 | optional: true 911 | postcss: 912 | optional: true 913 | typescript: 914 | optional: true 915 | 916 | typescript@5.6.2: 917 | resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} 918 | engines: {node: '>=14.17'} 919 | hasBin: true 920 | 921 | undici-types@6.19.8: 922 | resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 923 | 924 | webidl-conversions@4.0.2: 925 | resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} 926 | 927 | whatwg-url@7.1.0: 928 | resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} 929 | 930 | which@2.0.2: 931 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 932 | engines: {node: '>= 8'} 933 | hasBin: true 934 | 935 | wrap-ansi@7.0.0: 936 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 937 | engines: {node: '>=10'} 938 | 939 | wrap-ansi@8.1.0: 940 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 941 | engines: {node: '>=12'} 942 | 943 | snapshots: 944 | 945 | '@esbuild/aix-ppc64@0.23.1': 946 | optional: true 947 | 948 | '@esbuild/android-arm64@0.23.1': 949 | optional: true 950 | 951 | '@esbuild/android-arm@0.23.1': 952 | optional: true 953 | 954 | '@esbuild/android-x64@0.23.1': 955 | optional: true 956 | 957 | '@esbuild/darwin-arm64@0.23.1': 958 | optional: true 959 | 960 | '@esbuild/darwin-x64@0.23.1': 961 | optional: true 962 | 963 | '@esbuild/freebsd-arm64@0.23.1': 964 | optional: true 965 | 966 | '@esbuild/freebsd-x64@0.23.1': 967 | optional: true 968 | 969 | '@esbuild/linux-arm64@0.23.1': 970 | optional: true 971 | 972 | '@esbuild/linux-arm@0.23.1': 973 | optional: true 974 | 975 | '@esbuild/linux-ia32@0.23.1': 976 | optional: true 977 | 978 | '@esbuild/linux-loong64@0.23.1': 979 | optional: true 980 | 981 | '@esbuild/linux-mips64el@0.23.1': 982 | optional: true 983 | 984 | '@esbuild/linux-ppc64@0.23.1': 985 | optional: true 986 | 987 | '@esbuild/linux-riscv64@0.23.1': 988 | optional: true 989 | 990 | '@esbuild/linux-s390x@0.23.1': 991 | optional: true 992 | 993 | '@esbuild/linux-x64@0.23.1': 994 | optional: true 995 | 996 | '@esbuild/netbsd-x64@0.23.1': 997 | optional: true 998 | 999 | '@esbuild/openbsd-arm64@0.23.1': 1000 | optional: true 1001 | 1002 | '@esbuild/openbsd-x64@0.23.1': 1003 | optional: true 1004 | 1005 | '@esbuild/sunos-x64@0.23.1': 1006 | optional: true 1007 | 1008 | '@esbuild/win32-arm64@0.23.1': 1009 | optional: true 1010 | 1011 | '@esbuild/win32-ia32@0.23.1': 1012 | optional: true 1013 | 1014 | '@esbuild/win32-x64@0.23.1': 1015 | optional: true 1016 | 1017 | '@isaacs/cliui@8.0.2': 1018 | dependencies: 1019 | string-width: 5.1.2 1020 | string-width-cjs: string-width@4.2.3 1021 | strip-ansi: 7.1.0 1022 | strip-ansi-cjs: strip-ansi@6.0.1 1023 | wrap-ansi: 8.1.0 1024 | wrap-ansi-cjs: wrap-ansi@7.0.0 1025 | 1026 | '@jridgewell/gen-mapping@0.3.5': 1027 | dependencies: 1028 | '@jridgewell/set-array': 1.2.1 1029 | '@jridgewell/sourcemap-codec': 1.5.0 1030 | '@jridgewell/trace-mapping': 0.3.25 1031 | 1032 | '@jridgewell/resolve-uri@3.1.2': {} 1033 | 1034 | '@jridgewell/set-array@1.2.1': {} 1035 | 1036 | '@jridgewell/sourcemap-codec@1.5.0': {} 1037 | 1038 | '@jridgewell/trace-mapping@0.3.25': 1039 | dependencies: 1040 | '@jridgewell/resolve-uri': 3.1.2 1041 | '@jridgewell/sourcemap-codec': 1.5.0 1042 | 1043 | '@next/env@14.2.11': {} 1044 | 1045 | '@next/swc-darwin-arm64@14.2.11': 1046 | optional: true 1047 | 1048 | '@next/swc-darwin-x64@14.2.11': 1049 | optional: true 1050 | 1051 | '@next/swc-linux-arm64-gnu@14.2.11': 1052 | optional: true 1053 | 1054 | '@next/swc-linux-arm64-musl@14.2.11': 1055 | optional: true 1056 | 1057 | '@next/swc-linux-x64-gnu@14.2.11': 1058 | optional: true 1059 | 1060 | '@next/swc-linux-x64-musl@14.2.11': 1061 | optional: true 1062 | 1063 | '@next/swc-win32-arm64-msvc@14.2.11': 1064 | optional: true 1065 | 1066 | '@next/swc-win32-ia32-msvc@14.2.11': 1067 | optional: true 1068 | 1069 | '@next/swc-win32-x64-msvc@14.2.11': 1070 | optional: true 1071 | 1072 | '@nodelib/fs.scandir@2.1.5': 1073 | dependencies: 1074 | '@nodelib/fs.stat': 2.0.5 1075 | run-parallel: 1.2.0 1076 | 1077 | '@nodelib/fs.stat@2.0.5': {} 1078 | 1079 | '@nodelib/fs.walk@1.2.8': 1080 | dependencies: 1081 | '@nodelib/fs.scandir': 2.1.5 1082 | fastq: 1.17.1 1083 | 1084 | '@pkgjs/parseargs@0.11.0': 1085 | optional: true 1086 | 1087 | '@playwright/test@1.52.0': 1088 | dependencies: 1089 | playwright: 1.52.0 1090 | 1091 | '@rollup/rollup-android-arm-eabi@4.21.3': 1092 | optional: true 1093 | 1094 | '@rollup/rollup-android-arm64@4.21.3': 1095 | optional: true 1096 | 1097 | '@rollup/rollup-darwin-arm64@4.21.3': 1098 | optional: true 1099 | 1100 | '@rollup/rollup-darwin-x64@4.21.3': 1101 | optional: true 1102 | 1103 | '@rollup/rollup-linux-arm-gnueabihf@4.21.3': 1104 | optional: true 1105 | 1106 | '@rollup/rollup-linux-arm-musleabihf@4.21.3': 1107 | optional: true 1108 | 1109 | '@rollup/rollup-linux-arm64-gnu@4.21.3': 1110 | optional: true 1111 | 1112 | '@rollup/rollup-linux-arm64-musl@4.21.3': 1113 | optional: true 1114 | 1115 | '@rollup/rollup-linux-powerpc64le-gnu@4.21.3': 1116 | optional: true 1117 | 1118 | '@rollup/rollup-linux-riscv64-gnu@4.21.3': 1119 | optional: true 1120 | 1121 | '@rollup/rollup-linux-s390x-gnu@4.21.3': 1122 | optional: true 1123 | 1124 | '@rollup/rollup-linux-x64-gnu@4.21.3': 1125 | optional: true 1126 | 1127 | '@rollup/rollup-linux-x64-musl@4.21.3': 1128 | optional: true 1129 | 1130 | '@rollup/rollup-win32-arm64-msvc@4.21.3': 1131 | optional: true 1132 | 1133 | '@rollup/rollup-win32-ia32-msvc@4.21.3': 1134 | optional: true 1135 | 1136 | '@rollup/rollup-win32-x64-msvc@4.21.3': 1137 | optional: true 1138 | 1139 | '@swc/counter@0.1.3': {} 1140 | 1141 | '@swc/helpers@0.5.5': 1142 | dependencies: 1143 | '@swc/counter': 0.1.3 1144 | tslib: 2.8.1 1145 | 1146 | '@types/estree@1.0.5': {} 1147 | 1148 | '@types/node@20.16.5': 1149 | dependencies: 1150 | undici-types: 6.19.8 1151 | 1152 | '@types/prop-types@15.7.12': {} 1153 | 1154 | '@types/react-dom@18.3.0': 1155 | dependencies: 1156 | '@types/react': 18.3.5 1157 | 1158 | '@types/react@18.3.5': 1159 | dependencies: 1160 | '@types/prop-types': 15.7.12 1161 | csstype: 3.1.3 1162 | 1163 | ansi-regex@5.0.1: {} 1164 | 1165 | ansi-regex@6.1.0: {} 1166 | 1167 | ansi-styles@4.3.0: 1168 | dependencies: 1169 | color-convert: 2.0.1 1170 | 1171 | ansi-styles@6.2.1: {} 1172 | 1173 | any-promise@1.3.0: {} 1174 | 1175 | anymatch@3.1.3: 1176 | dependencies: 1177 | normalize-path: 3.0.0 1178 | picomatch: 2.3.1 1179 | 1180 | array-union@2.1.0: {} 1181 | 1182 | balanced-match@1.0.2: {} 1183 | 1184 | binary-extensions@2.3.0: {} 1185 | 1186 | brace-expansion@2.0.1: 1187 | dependencies: 1188 | balanced-match: 1.0.2 1189 | 1190 | braces@3.0.3: 1191 | dependencies: 1192 | fill-range: 7.1.1 1193 | 1194 | bundle-require@5.0.0(esbuild@0.23.1): 1195 | dependencies: 1196 | esbuild: 0.23.1 1197 | load-tsconfig: 0.2.5 1198 | 1199 | busboy@1.6.0: 1200 | dependencies: 1201 | streamsearch: 1.1.0 1202 | 1203 | cac@6.7.14: {} 1204 | 1205 | caniuse-lite@1.0.30001660: {} 1206 | 1207 | chokidar@3.6.0: 1208 | dependencies: 1209 | anymatch: 3.1.3 1210 | braces: 3.0.3 1211 | glob-parent: 5.1.2 1212 | is-binary-path: 2.1.0 1213 | is-glob: 4.0.3 1214 | normalize-path: 3.0.0 1215 | readdirp: 3.6.0 1216 | optionalDependencies: 1217 | fsevents: 2.3.3 1218 | 1219 | client-only@0.0.1: {} 1220 | 1221 | color-convert@2.0.1: 1222 | dependencies: 1223 | color-name: 1.1.4 1224 | 1225 | color-name@1.1.4: {} 1226 | 1227 | commander@4.1.1: {} 1228 | 1229 | consola@3.2.3: {} 1230 | 1231 | cross-spawn@7.0.3: 1232 | dependencies: 1233 | path-key: 3.1.1 1234 | shebang-command: 2.0.0 1235 | which: 2.0.2 1236 | 1237 | csstype@3.1.3: {} 1238 | 1239 | debug@4.3.7: 1240 | dependencies: 1241 | ms: 2.1.3 1242 | 1243 | dir-glob@3.0.1: 1244 | dependencies: 1245 | path-type: 4.0.0 1246 | 1247 | eastasianwidth@0.2.0: {} 1248 | 1249 | emoji-regex@8.0.0: {} 1250 | 1251 | emoji-regex@9.2.2: {} 1252 | 1253 | esbuild@0.23.1: 1254 | optionalDependencies: 1255 | '@esbuild/aix-ppc64': 0.23.1 1256 | '@esbuild/android-arm': 0.23.1 1257 | '@esbuild/android-arm64': 0.23.1 1258 | '@esbuild/android-x64': 0.23.1 1259 | '@esbuild/darwin-arm64': 0.23.1 1260 | '@esbuild/darwin-x64': 0.23.1 1261 | '@esbuild/freebsd-arm64': 0.23.1 1262 | '@esbuild/freebsd-x64': 0.23.1 1263 | '@esbuild/linux-arm': 0.23.1 1264 | '@esbuild/linux-arm64': 0.23.1 1265 | '@esbuild/linux-ia32': 0.23.1 1266 | '@esbuild/linux-loong64': 0.23.1 1267 | '@esbuild/linux-mips64el': 0.23.1 1268 | '@esbuild/linux-ppc64': 0.23.1 1269 | '@esbuild/linux-riscv64': 0.23.1 1270 | '@esbuild/linux-s390x': 0.23.1 1271 | '@esbuild/linux-x64': 0.23.1 1272 | '@esbuild/netbsd-x64': 0.23.1 1273 | '@esbuild/openbsd-arm64': 0.23.1 1274 | '@esbuild/openbsd-x64': 0.23.1 1275 | '@esbuild/sunos-x64': 0.23.1 1276 | '@esbuild/win32-arm64': 0.23.1 1277 | '@esbuild/win32-ia32': 0.23.1 1278 | '@esbuild/win32-x64': 0.23.1 1279 | 1280 | execa@5.1.1: 1281 | dependencies: 1282 | cross-spawn: 7.0.3 1283 | get-stream: 6.0.1 1284 | human-signals: 2.1.0 1285 | is-stream: 2.0.1 1286 | merge-stream: 2.0.0 1287 | npm-run-path: 4.0.1 1288 | onetime: 5.1.2 1289 | signal-exit: 3.0.7 1290 | strip-final-newline: 2.0.0 1291 | 1292 | fast-glob@3.3.2: 1293 | dependencies: 1294 | '@nodelib/fs.stat': 2.0.5 1295 | '@nodelib/fs.walk': 1.2.8 1296 | glob-parent: 5.1.2 1297 | merge2: 1.4.1 1298 | micromatch: 4.0.8 1299 | 1300 | fastq@1.17.1: 1301 | dependencies: 1302 | reusify: 1.0.4 1303 | 1304 | fill-range@7.1.1: 1305 | dependencies: 1306 | to-regex-range: 5.0.1 1307 | 1308 | foreground-child@3.3.0: 1309 | dependencies: 1310 | cross-spawn: 7.0.3 1311 | signal-exit: 4.1.0 1312 | 1313 | fsevents@2.3.2: 1314 | optional: true 1315 | 1316 | fsevents@2.3.3: 1317 | optional: true 1318 | 1319 | get-stream@6.0.1: {} 1320 | 1321 | glob-parent@5.1.2: 1322 | dependencies: 1323 | is-glob: 4.0.3 1324 | 1325 | glob@10.4.5: 1326 | dependencies: 1327 | foreground-child: 3.3.0 1328 | jackspeak: 3.4.3 1329 | minimatch: 9.0.5 1330 | minipass: 7.1.2 1331 | package-json-from-dist: 1.0.0 1332 | path-scurry: 1.11.1 1333 | 1334 | globby@11.1.0: 1335 | dependencies: 1336 | array-union: 2.1.0 1337 | dir-glob: 3.0.1 1338 | fast-glob: 3.3.2 1339 | ignore: 5.3.2 1340 | merge2: 1.4.1 1341 | slash: 3.0.0 1342 | 1343 | graceful-fs@4.2.11: {} 1344 | 1345 | human-signals@2.1.0: {} 1346 | 1347 | ignore@5.3.2: {} 1348 | 1349 | is-binary-path@2.1.0: 1350 | dependencies: 1351 | binary-extensions: 2.3.0 1352 | 1353 | is-extglob@2.1.1: {} 1354 | 1355 | is-fullwidth-code-point@3.0.0: {} 1356 | 1357 | is-glob@4.0.3: 1358 | dependencies: 1359 | is-extglob: 2.1.1 1360 | 1361 | is-number@7.0.0: {} 1362 | 1363 | is-stream@2.0.1: {} 1364 | 1365 | isexe@2.0.0: {} 1366 | 1367 | jackspeak@3.4.3: 1368 | dependencies: 1369 | '@isaacs/cliui': 8.0.2 1370 | optionalDependencies: 1371 | '@pkgjs/parseargs': 0.11.0 1372 | 1373 | joycon@3.1.1: {} 1374 | 1375 | js-tokens@4.0.0: {} 1376 | 1377 | lilconfig@3.1.2: {} 1378 | 1379 | lines-and-columns@1.2.4: {} 1380 | 1381 | load-tsconfig@0.2.5: {} 1382 | 1383 | lodash.sortby@4.7.0: {} 1384 | 1385 | loose-envify@1.4.0: 1386 | dependencies: 1387 | js-tokens: 4.0.0 1388 | 1389 | lru-cache@10.4.3: {} 1390 | 1391 | merge-stream@2.0.0: {} 1392 | 1393 | merge2@1.4.1: {} 1394 | 1395 | micromatch@4.0.8: 1396 | dependencies: 1397 | braces: 3.0.3 1398 | picomatch: 2.3.1 1399 | 1400 | mimic-fn@2.1.0: {} 1401 | 1402 | minimatch@9.0.5: 1403 | dependencies: 1404 | brace-expansion: 2.0.1 1405 | 1406 | minipass@7.1.2: {} 1407 | 1408 | ms@2.1.3: {} 1409 | 1410 | mz@2.7.0: 1411 | dependencies: 1412 | any-promise: 1.3.0 1413 | object-assign: 4.1.1 1414 | thenify-all: 1.6.0 1415 | 1416 | nanoid@3.3.7: {} 1417 | 1418 | next@14.2.11(@playwright/test@1.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 1419 | dependencies: 1420 | '@next/env': 14.2.11 1421 | '@swc/helpers': 0.5.5 1422 | busboy: 1.6.0 1423 | caniuse-lite: 1.0.30001660 1424 | graceful-fs: 4.2.11 1425 | postcss: 8.4.31 1426 | react: 18.3.1 1427 | react-dom: 18.3.1(react@18.3.1) 1428 | styled-jsx: 5.1.1(react@18.3.1) 1429 | optionalDependencies: 1430 | '@next/swc-darwin-arm64': 14.2.11 1431 | '@next/swc-darwin-x64': 14.2.11 1432 | '@next/swc-linux-arm64-gnu': 14.2.11 1433 | '@next/swc-linux-arm64-musl': 14.2.11 1434 | '@next/swc-linux-x64-gnu': 14.2.11 1435 | '@next/swc-linux-x64-musl': 14.2.11 1436 | '@next/swc-win32-arm64-msvc': 14.2.11 1437 | '@next/swc-win32-ia32-msvc': 14.2.11 1438 | '@next/swc-win32-x64-msvc': 14.2.11 1439 | '@playwright/test': 1.52.0 1440 | transitivePeerDependencies: 1441 | - '@babel/core' 1442 | - babel-plugin-macros 1443 | 1444 | normalize-path@3.0.0: {} 1445 | 1446 | npm-run-path@4.0.1: 1447 | dependencies: 1448 | path-key: 3.1.1 1449 | 1450 | object-assign@4.1.1: {} 1451 | 1452 | onetime@5.1.2: 1453 | dependencies: 1454 | mimic-fn: 2.1.0 1455 | 1456 | package-json-from-dist@1.0.0: {} 1457 | 1458 | path-key@3.1.1: {} 1459 | 1460 | path-scurry@1.11.1: 1461 | dependencies: 1462 | lru-cache: 10.4.3 1463 | minipass: 7.1.2 1464 | 1465 | path-type@4.0.0: {} 1466 | 1467 | picocolors@1.1.0: {} 1468 | 1469 | picomatch@2.3.1: {} 1470 | 1471 | pirates@4.0.6: {} 1472 | 1473 | playwright-core@1.52.0: {} 1474 | 1475 | playwright@1.52.0: 1476 | dependencies: 1477 | playwright-core: 1.52.0 1478 | optionalDependencies: 1479 | fsevents: 2.3.2 1480 | 1481 | postcss-load-config@6.0.1(postcss@8.4.31): 1482 | dependencies: 1483 | lilconfig: 3.1.2 1484 | optionalDependencies: 1485 | postcss: 8.4.31 1486 | 1487 | postcss@8.4.31: 1488 | dependencies: 1489 | nanoid: 3.3.7 1490 | picocolors: 1.1.0 1491 | source-map-js: 1.2.1 1492 | 1493 | punycode@2.3.1: {} 1494 | 1495 | queue-microtask@1.2.3: {} 1496 | 1497 | react-dom@18.3.1(react@18.3.1): 1498 | dependencies: 1499 | loose-envify: 1.4.0 1500 | react: 18.3.1 1501 | scheduler: 0.23.2 1502 | 1503 | react@18.3.1: 1504 | dependencies: 1505 | loose-envify: 1.4.0 1506 | 1507 | readdirp@3.6.0: 1508 | dependencies: 1509 | picomatch: 2.3.1 1510 | 1511 | resolve-from@5.0.0: {} 1512 | 1513 | reusify@1.0.4: {} 1514 | 1515 | rollup@4.21.3: 1516 | dependencies: 1517 | '@types/estree': 1.0.5 1518 | optionalDependencies: 1519 | '@rollup/rollup-android-arm-eabi': 4.21.3 1520 | '@rollup/rollup-android-arm64': 4.21.3 1521 | '@rollup/rollup-darwin-arm64': 4.21.3 1522 | '@rollup/rollup-darwin-x64': 4.21.3 1523 | '@rollup/rollup-linux-arm-gnueabihf': 4.21.3 1524 | '@rollup/rollup-linux-arm-musleabihf': 4.21.3 1525 | '@rollup/rollup-linux-arm64-gnu': 4.21.3 1526 | '@rollup/rollup-linux-arm64-musl': 4.21.3 1527 | '@rollup/rollup-linux-powerpc64le-gnu': 4.21.3 1528 | '@rollup/rollup-linux-riscv64-gnu': 4.21.3 1529 | '@rollup/rollup-linux-s390x-gnu': 4.21.3 1530 | '@rollup/rollup-linux-x64-gnu': 4.21.3 1531 | '@rollup/rollup-linux-x64-musl': 4.21.3 1532 | '@rollup/rollup-win32-arm64-msvc': 4.21.3 1533 | '@rollup/rollup-win32-ia32-msvc': 4.21.3 1534 | '@rollup/rollup-win32-x64-msvc': 4.21.3 1535 | fsevents: 2.3.3 1536 | 1537 | run-parallel@1.2.0: 1538 | dependencies: 1539 | queue-microtask: 1.2.3 1540 | 1541 | scheduler@0.23.2: 1542 | dependencies: 1543 | loose-envify: 1.4.0 1544 | 1545 | shebang-command@2.0.0: 1546 | dependencies: 1547 | shebang-regex: 3.0.0 1548 | 1549 | shebang-regex@3.0.0: {} 1550 | 1551 | signal-exit@3.0.7: {} 1552 | 1553 | signal-exit@4.1.0: {} 1554 | 1555 | slash@3.0.0: {} 1556 | 1557 | source-map-js@1.2.1: {} 1558 | 1559 | source-map@0.8.0-beta.0: 1560 | dependencies: 1561 | whatwg-url: 7.1.0 1562 | 1563 | streamsearch@1.1.0: {} 1564 | 1565 | string-width@4.2.3: 1566 | dependencies: 1567 | emoji-regex: 8.0.0 1568 | is-fullwidth-code-point: 3.0.0 1569 | strip-ansi: 6.0.1 1570 | 1571 | string-width@5.1.2: 1572 | dependencies: 1573 | eastasianwidth: 0.2.0 1574 | emoji-regex: 9.2.2 1575 | strip-ansi: 7.1.0 1576 | 1577 | strip-ansi@6.0.1: 1578 | dependencies: 1579 | ansi-regex: 5.0.1 1580 | 1581 | strip-ansi@7.1.0: 1582 | dependencies: 1583 | ansi-regex: 6.1.0 1584 | 1585 | strip-final-newline@2.0.0: {} 1586 | 1587 | styled-jsx@5.1.1(react@18.3.1): 1588 | dependencies: 1589 | client-only: 0.0.1 1590 | react: 18.3.1 1591 | 1592 | sucrase@3.35.0: 1593 | dependencies: 1594 | '@jridgewell/gen-mapping': 0.3.5 1595 | commander: 4.1.1 1596 | glob: 10.4.5 1597 | lines-and-columns: 1.2.4 1598 | mz: 2.7.0 1599 | pirates: 4.0.6 1600 | ts-interface-checker: 0.1.13 1601 | 1602 | thenify-all@1.6.0: 1603 | dependencies: 1604 | thenify: 3.3.1 1605 | 1606 | thenify@3.3.1: 1607 | dependencies: 1608 | any-promise: 1.3.0 1609 | 1610 | to-regex-range@5.0.1: 1611 | dependencies: 1612 | is-number: 7.0.0 1613 | 1614 | tr46@1.0.1: 1615 | dependencies: 1616 | punycode: 2.3.1 1617 | 1618 | tree-kill@1.2.2: {} 1619 | 1620 | ts-interface-checker@0.1.13: {} 1621 | 1622 | tslib@2.8.1: {} 1623 | 1624 | tsup@8.2.4(postcss@8.4.31)(typescript@5.6.2): 1625 | dependencies: 1626 | bundle-require: 5.0.0(esbuild@0.23.1) 1627 | cac: 6.7.14 1628 | chokidar: 3.6.0 1629 | consola: 3.2.3 1630 | debug: 4.3.7 1631 | esbuild: 0.23.1 1632 | execa: 5.1.1 1633 | globby: 11.1.0 1634 | joycon: 3.1.1 1635 | picocolors: 1.1.0 1636 | postcss-load-config: 6.0.1(postcss@8.4.31) 1637 | resolve-from: 5.0.0 1638 | rollup: 4.21.3 1639 | source-map: 0.8.0-beta.0 1640 | sucrase: 3.35.0 1641 | tree-kill: 1.2.2 1642 | optionalDependencies: 1643 | postcss: 8.4.31 1644 | typescript: 5.6.2 1645 | transitivePeerDependencies: 1646 | - jiti 1647 | - supports-color 1648 | - tsx 1649 | - yaml 1650 | 1651 | typescript@5.6.2: {} 1652 | 1653 | undici-types@6.19.8: {} 1654 | 1655 | webidl-conversions@4.0.2: {} 1656 | 1657 | whatwg-url@7.1.0: 1658 | dependencies: 1659 | lodash.sortby: 4.7.0 1660 | tr46: 1.0.1 1661 | webidl-conversions: 4.0.2 1662 | 1663 | which@2.0.2: 1664 | dependencies: 1665 | isexe: 2.0.0 1666 | 1667 | wrap-ansi@7.0.0: 1668 | dependencies: 1669 | ansi-styles: 4.3.0 1670 | string-width: 4.2.3 1671 | strip-ansi: 6.0.1 1672 | 1673 | wrap-ansi@8.1.0: 1674 | dependencies: 1675 | ansi-styles: 6.2.1 1676 | string-width: 5.1.2 1677 | strip-ansi: 7.1.0 1678 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '.' 3 | - 'example' 4 | -------------------------------------------------------------------------------- /src/components/InterceptAppRouterProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AppRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime"; 4 | import React, { MutableRefObject } from "react"; 5 | import { useInterceptedAppRouter } from "../hooks/useInterceptedAppRouter"; 6 | import { GuardDef } from "../types"; 7 | 8 | export function InterceptAppRouterProvider({ 9 | guardMapRef, 10 | children, 11 | }: { 12 | guardMapRef: MutableRefObject>; 13 | children: React.ReactNode; 14 | }) { 15 | const interceptedRouter = useInterceptedAppRouter({ guardMapRef }); 16 | if (!interceptedRouter) { 17 | return <>{children}; 18 | } 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/InterceptPagesRouterProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { RouterContext } from "next/dist/shared/lib/router-context.shared-runtime"; 4 | import React, { MutableRefObject } from "react"; 5 | import { useInterceptedPagesRouter } from "../hooks/useInterceptedPagesRouter"; 6 | import { GuardDef, NavigationGuardCallback } from "../types"; 7 | 8 | export function InterceptPagesRouterProvider({ 9 | guardMapRef, 10 | children, 11 | }: { 12 | guardMapRef: MutableRefObject>; 13 | children: React.ReactNode; 14 | }) { 15 | const interceptedRouter = useInterceptedPagesRouter({ guardMapRef }); 16 | if (!interceptedRouter) { 17 | return <>{children}; 18 | } 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/NavigationGuardProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useRef } from "react"; 4 | import { useInterceptPageUnload } from "../hooks/useInterceptPageUnload"; 5 | import { useInterceptPopState } from "../hooks/useInterceptPopState"; 6 | import { GuardDef } from "../types"; 7 | import { InterceptAppRouterProvider } from "./InterceptAppRouterProvider"; 8 | import { InterceptPagesRouterProvider } from "./InterceptPagesRouterProvider"; 9 | import { NavigationGuardProviderContext } from "./NavigationGuardProviderContext"; 10 | 11 | export function NavigationGuardProvider({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | const guardMapRef = useRef(new Map()); 17 | 18 | useInterceptPopState({ guardMapRef }); 19 | useInterceptPageUnload({ guardMapRef }); 20 | 21 | return ( 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/NavigationGuardProviderContext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { MutableRefObject } from "react"; 4 | import { GuardDef } from "../types"; 5 | 6 | export const NavigationGuardProviderContext = React.createContext< 7 | MutableRefObject> | undefined 8 | >(undefined); 9 | NavigationGuardProviderContext.displayName = "NavigationGuardProviderContext"; 10 | -------------------------------------------------------------------------------- /src/hooks/useInterceptPageUnload.ts: -------------------------------------------------------------------------------- 1 | import { GuardDef } from "../types"; 2 | import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; 3 | 4 | export function useInterceptPageUnload({ 5 | guardMapRef, 6 | }: { 7 | guardMapRef: React.MutableRefObject>; 8 | }) { 9 | useIsomorphicLayoutEffect(() => { 10 | const handleBeforeUnload = (event: BeforeUnloadEvent) => { 11 | for (const def of guardMapRef.current.values()) { 12 | // We does not support confirm() on beforeunload as 13 | // we cannot wait for async Promise resolution on beforeunload. 14 | const enabled = def.enabled({ to: "", type: "beforeunload" }); 15 | if (enabled) { 16 | event.preventDefault(); 17 | // As MDN says, custom message has already been unsupported in majority of browsers. 18 | // Chrome requires returnValue to be set. 19 | event.returnValue = ""; 20 | return; 21 | } 22 | } 23 | }; 24 | window.addEventListener("beforeunload", handleBeforeUnload); 25 | return () => { 26 | window.removeEventListener("beforeunload", handleBeforeUnload); 27 | }; 28 | }, []); 29 | } 30 | -------------------------------------------------------------------------------- /src/hooks/useInterceptPopState.ts: -------------------------------------------------------------------------------- 1 | import { RouterContext } from "next/dist/shared/lib/router-context.shared-runtime"; 2 | import { useContext } from "react"; 3 | import { GuardDef, RenderedState } from "../types"; 4 | import { DEBUG } from "../utils/debug"; 5 | import { 6 | newToken, 7 | setupHistoryAugmentationOnce, 8 | } from "../utils/historyAugmentation"; 9 | import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; 10 | 11 | // Based on https://github.com/vercel/next.js/discussions/47020#discussioncomment-7826121 12 | 13 | const renderedStateRef: { current: RenderedState } = { 14 | current: { index: -1, token: "" }, 15 | }; 16 | 17 | export function useInterceptPopState({ 18 | guardMapRef, 19 | }: { 20 | guardMapRef: React.MutableRefObject>; 21 | }) { 22 | const pagesRouter = useContext(RouterContext); 23 | 24 | useIsomorphicLayoutEffect(() => { 25 | // NOTE: Called before Next.js router setup which is useEffect(). 26 | // https://github.com/vercel/next.js/blob/50b9966ba9377fd07a27e3f80aecd131fa346482/packages/next/src/client/components/app-router.tsx#L518 27 | const { writeState } = setupHistoryAugmentationOnce({ renderedStateRef }); 28 | 29 | const handlePopState = createHandlePopState(guardMapRef, writeState); 30 | 31 | if (pagesRouter) { 32 | pagesRouter.beforePopState(() => handlePopState(history.state)); 33 | 34 | return () => { 35 | pagesRouter.beforePopState(() => true); 36 | }; 37 | } else { 38 | const onPopState = (event: PopStateEvent) => { 39 | if (!handlePopState(event.state)) { 40 | event.stopImmediatePropagation(); 41 | } 42 | }; 43 | 44 | // NOTE: Called before Next.js router setup which is useEffect(). 45 | // https://github.com/vercel/next.js/blob/50b9966ba9377fd07a27e3f80aecd131fa346482/packages/next/src/client/components/app-router.tsx#L518 46 | // NOTE: capture on popstate listener is not working on Chrome. 47 | window.addEventListener("popstate", onPopState); 48 | 49 | return () => { 50 | window.removeEventListener("popstate", onPopState); 51 | }; 52 | } 53 | }, [pagesRouter]); 54 | } 55 | 56 | function createHandlePopState( 57 | guardMapRef: React.MutableRefObject>, 58 | writeState: () => void 59 | ) { 60 | let dispatchedState: unknown; 61 | 62 | return (nextState: any): boolean => { 63 | const token: string | undefined = nextState.__next_navigation_guard_token; 64 | const nextIndex: number = 65 | Number(nextState.__next_navigation_guard_stack_index) || 0; 66 | 67 | if (!token || token !== renderedStateRef.current.token) { 68 | if (DEBUG) 69 | console.log( 70 | `useInterceptPopState(): token mismatch, skip handling (current: ${renderedStateRef.current.token}, next: ${token})` 71 | ); 72 | renderedStateRef.current.token = token || newToken(); 73 | renderedStateRef.current.index = token ? nextIndex : 0; 74 | writeState(); 75 | return true; 76 | } 77 | 78 | const delta = nextIndex - renderedStateRef.current.index; 79 | // When go(-delta) is called, delta should be zero. 80 | if (delta === 0) { 81 | if (DEBUG) 82 | console.log( 83 | `useInterceptPopState(): discard popstate event: delta is 0` 84 | ); 85 | return false; 86 | } 87 | 88 | if (DEBUG) 89 | console.log( 90 | `useInterceptPopState(): __next_navigation_guard_stack_index is ${nextState.__next_navigation_guard_stack_index}` 91 | ); 92 | 93 | const to = location.pathname + location.search; 94 | 95 | const defs = [...guardMapRef.current.values()]; 96 | 97 | if (nextState === dispatchedState || defs.length === 0) { 98 | if (DEBUG) 99 | console.log( 100 | `useInterceptPopState(): Accept popstate event, index: ${nextIndex}` 101 | ); 102 | dispatchedState = null; 103 | renderedStateRef.current.index = nextIndex; 104 | return true; 105 | } 106 | 107 | if (DEBUG) 108 | console.log( 109 | `useInterceptPopState(): Suspend popstate event, index: ${nextIndex}` 110 | ); 111 | 112 | // Wait for all callbacks to be resolved 113 | (async () => { 114 | let i = -1; 115 | 116 | for (const def of defs) { 117 | i++; 118 | 119 | if (!def.enabled({ to, type: "popstate" })) continue; 120 | if (DEBUG) { 121 | console.log( 122 | `useInterceptPopState(): confirmation for listener index ${i}` 123 | ); 124 | } 125 | 126 | const confirm = await def.callback({ to, type: "popstate" }); 127 | // TODO: check cancel while waiting for navigation guard 128 | if (!confirm) { 129 | if (DEBUG) { 130 | console.log( 131 | `useInterceptPopState(): Cancel popstate event, go(): ${ 132 | renderedStateRef.current.index 133 | } - ${nextIndex} = ${-delta}` 134 | ); 135 | } 136 | if (delta !== 0) { 137 | // discard event 138 | window.history.go(-delta); 139 | } 140 | return; 141 | } 142 | } 143 | 144 | if (DEBUG) { 145 | console.log( 146 | `useInterceptPopState(): Accept popstate event, ${nextIndex}` 147 | ); 148 | } 149 | // accept 150 | dispatchedState = nextState; 151 | window.dispatchEvent(new PopStateEvent("popstate", { state: nextState })); 152 | })(); 153 | 154 | // Return false to call stopImmediatePropagation() 155 | return false; 156 | }; 157 | } 158 | -------------------------------------------------------------------------------- /src/hooks/useInterceptedAppRouter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AppRouterContext, 3 | AppRouterInstance, 4 | } from "next/dist/shared/lib/app-router-context.shared-runtime"; 5 | import { MutableRefObject, useContext, useMemo } from "react"; 6 | import { GuardDef } from "../types"; 7 | 8 | export function useInterceptedAppRouter({ 9 | guardMapRef, 10 | }: { 11 | guardMapRef: MutableRefObject>; 12 | }) { 13 | const origRouter = useContext(AppRouterContext); 14 | 15 | return useMemo((): AppRouterInstance | null => { 16 | if (!origRouter) return null; 17 | 18 | const guarded = async ( 19 | type: "push" | "replace" | "refresh", 20 | to: string, 21 | accepted: () => void 22 | ) => { 23 | const defs = [...guardMapRef.current.values()]; 24 | for (const { enabled, callback } of defs) { 25 | if (!enabled({ to, type })) continue; 26 | 27 | const confirm = await callback({ to, type }); 28 | if (!confirm) return; 29 | } 30 | accepted(); 31 | }; 32 | 33 | return { 34 | ...origRouter, 35 | push: (href, ...args) => { 36 | guarded("push", href, () => origRouter.push(href, ...args)); 37 | }, 38 | replace: (href, ...args) => { 39 | guarded("replace", href, () => origRouter.replace(href, ...args)); 40 | }, 41 | refresh: (...args) => { 42 | guarded("refresh", location.href, () => origRouter.refresh(...args)); 43 | }, 44 | }; 45 | }, [origRouter]); 46 | } 47 | -------------------------------------------------------------------------------- /src/hooks/useInterceptedPagesRouter.ts: -------------------------------------------------------------------------------- 1 | import { NextRouter } from "next/dist/client/router"; 2 | import { RouterContext } from "next/dist/shared/lib/router-context.shared-runtime"; 3 | import { Url } from "next/dist/shared/lib/router/router"; 4 | import { MutableRefObject, useContext, useMemo } from "react"; 5 | import { GuardDef } from "../types"; 6 | 7 | export function useInterceptedPagesRouter({ 8 | guardMapRef, 9 | }: { 10 | guardMapRef: MutableRefObject>; 11 | }) { 12 | const origRouter = useContext(RouterContext); 13 | 14 | return useMemo((): NextRouter | null => { 15 | if (!origRouter) return null; 16 | 17 | const guarded = async ( 18 | type: "push" | "replace" | "refresh", 19 | toUrl: Url, 20 | accepted: () => Promise 21 | ): Promise => { 22 | const to = typeof toUrl === "string" ? toUrl : toUrl.href ?? ""; 23 | const defs = [...guardMapRef.current.values()]; 24 | for (const { enabled, callback } of defs) { 25 | if (!enabled({ to, type })) continue; 26 | 27 | const confirm = await callback({ to, type }); 28 | if (!confirm) return false; 29 | } 30 | 31 | return await accepted(); 32 | }; 33 | 34 | return { 35 | ...origRouter, 36 | push: (href, ...args) => { 37 | return guarded("push", href, () => origRouter.push(href, ...args)); 38 | }, 39 | replace: (href, ...args) => { 40 | return guarded("replace", href, () => 41 | origRouter.replace(href, ...args) 42 | ); 43 | }, 44 | reload: (...args) => { 45 | guarded("refresh", location.href, async () => { 46 | origRouter.reload(...args); // void 47 | return true; 48 | }); 49 | }, 50 | }; 51 | }, [origRouter]); 52 | } 53 | -------------------------------------------------------------------------------- /src/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useEffect } from "react"; 2 | 3 | export const useIsomorphicLayoutEffect = 4 | typeof window !== "undefined" ? useLayoutEffect : useEffect; 5 | -------------------------------------------------------------------------------- /src/hooks/useNavigationGuard.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useId, useState } from "react"; 2 | import { NavigationGuardProviderContext } from "../components/NavigationGuardProviderContext"; 3 | import { NavigationGuardCallback, NavigationGuardOptions } from "../types"; 4 | import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; 5 | 6 | // Should memoize callback func 7 | export function useNavigationGuard(options: NavigationGuardOptions) { 8 | const callbackId = useId(); 9 | const guardMapRef = useContext(NavigationGuardProviderContext); 10 | if (!guardMapRef) 11 | throw new Error( 12 | "useNavigationGuard must be used within a NavigationGuardProvider" 13 | ); 14 | 15 | const [pendingState, setPendingState] = useState<{ 16 | resolve: (accepted: boolean) => void; 17 | } | null>(null); 18 | 19 | useIsomorphicLayoutEffect(() => { 20 | const callback: NavigationGuardCallback = (params) => { 21 | if (options.confirm) { 22 | return options.confirm(params); 23 | } 24 | 25 | return new Promise((resolve) => { 26 | setPendingState({ resolve }); 27 | }); 28 | }; 29 | 30 | const enabled = options.enabled; 31 | 32 | guardMapRef.current.set(callbackId, { 33 | enabled: typeof enabled === "function" ? enabled : () => enabled ?? true, 34 | callback, 35 | }); 36 | 37 | return () => { 38 | guardMapRef.current.delete(callbackId); 39 | }; 40 | }, [options.confirm, options.enabled]); 41 | 42 | const active = pendingState !== null; 43 | 44 | const accept = useCallback(() => { 45 | if (!pendingState) return; 46 | pendingState.resolve(true); 47 | setPendingState(null); 48 | }, [pendingState]); 49 | 50 | const reject = useCallback(() => { 51 | if (!pendingState) return; 52 | pendingState.resolve(false); 53 | setPendingState(null); 54 | }, [pendingState]); 55 | 56 | return { active, accept, reject }; 57 | } 58 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { useNavigationGuard } from "./hooks/useNavigationGuard"; 2 | export { NavigationGuardProvider } from "./components/NavigationGuardProvider"; 3 | export type { NavigationGuardCallback as NavigationGuard } from "./types"; 4 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface NavigationGuardOptions { 2 | /** @default true */ 3 | enabled?: boolean | ((params: NavigationGuardParams) => boolean); 4 | confirm?: NavigationGuardCallback; 5 | } 6 | 7 | export interface NavigationGuardParams { 8 | to: string; 9 | type: "push" | "replace" | "refresh" | "popstate" | "beforeunload"; 10 | } 11 | 12 | /** 13 | * true will allow the navigation, false will prevent it. 14 | * When beforeunload event is fired, and returned Promise is async (not immediately resolved), 15 | * it will be treated as if it's resolved with false. 16 | */ 17 | export type NavigationGuardCallback = ( 18 | params: NavigationGuardParams 19 | ) => boolean | Promise; 20 | 21 | export interface GuardDef { 22 | enabled: (params: NavigationGuardParams) => boolean; 23 | callback: NavigationGuardCallback; 24 | } 25 | 26 | export interface RenderedState { 27 | index: number; 28 | token: string | null; // Prevent from two unrelated index numbers used for calculating delta. 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/debug.ts: -------------------------------------------------------------------------------- 1 | export const DEBUG = false; 2 | -------------------------------------------------------------------------------- /src/utils/historyAugmentation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { RenderedState } from "../types"; 4 | import { DEBUG } from "./debug"; 5 | 6 | let setupDone = false; 7 | let writeState = () => {}; 8 | 9 | export function newToken() { 10 | return Math.random().toString(36).substring(2); 11 | } 12 | 13 | // Next.js also modifies history.pushState and history.replaceState in useEffect. 14 | // And it's order seems to be not guaranteed. 15 | // So we setup only once before Next.js does. 16 | export function setupHistoryAugmentationOnce({ 17 | renderedStateRef, 18 | }: { 19 | renderedStateRef: { current: RenderedState }; 20 | }): { writeState: () => void } { 21 | if (setupDone) return { writeState }; 22 | 23 | if (DEBUG) console.log("setupHistoryAugmentationOnce: setup"); 24 | 25 | const originalPushState = window.history.pushState; 26 | const originalReplaceState = window.history.replaceState; 27 | if (DEBUG) { 28 | (window as any).__next_navigation_guard_debug_history_aug = { 29 | originalPushState, 30 | originalReplaceState, 31 | }; 32 | } 33 | 34 | renderedStateRef.current.index = 35 | parseInt(window.history.state.__next_navigation_guard_stack_index) || 0; 36 | renderedStateRef.current.token = 37 | String(window.history.state.__next_navigation_guard_token ?? "") || 38 | newToken(); 39 | 40 | if (DEBUG) 41 | console.log( 42 | `setupHistoryAugmentationOnce: initial currentIndex is ${renderedStateRef.current.index}, token is ${renderedStateRef.current.token}` 43 | ); 44 | 45 | writeState = () => { 46 | if (DEBUG) 47 | console.log( 48 | `setupHistoryAugmentationOnce: write state by replaceState(): currentIndex is ${renderedStateRef.current.index}, token is ${renderedStateRef.current.token}` 49 | ); 50 | 51 | const modifiedState = { 52 | ...window.history.state, 53 | __next_navigation_guard_token: renderedStateRef.current.token, 54 | __next_navigation_guard_stack_index: renderedStateRef.current.index, 55 | }; 56 | 57 | originalReplaceState.call( 58 | window.history, 59 | modifiedState, 60 | "", 61 | window.location.href 62 | ); 63 | }; 64 | 65 | if ( 66 | window.history.state.__next_navigation_guard_stack_index == null || 67 | window.history.state.__next_navigation_guard_token == null 68 | ) { 69 | writeState(); 70 | } 71 | 72 | window.history.pushState = function (state, unused, url) { 73 | // If current state is not managed by this library, reset the state. 74 | if (!renderedStateRef.current.token) { 75 | renderedStateRef.current.token = newToken(); 76 | renderedStateRef.current.index = -1; 77 | } 78 | 79 | ++renderedStateRef.current.index; 80 | 81 | if (DEBUG) 82 | console.log( 83 | `setupHistoryAugmentationOnce: push: currentIndex is ${renderedStateRef.current.index}, token is ${renderedStateRef.current.token}` 84 | ); 85 | 86 | const modifiedState = { 87 | ...state, 88 | __next_navigation_guard_token: renderedStateRef.current.token, 89 | __next_navigation_guard_stack_index: renderedStateRef.current.index, 90 | }; 91 | originalPushState.call(this, modifiedState, unused, url); 92 | }; 93 | 94 | window.history.replaceState = function (state, unused, url) { 95 | // If current state is not managed by this library, reset the state. 96 | if (!renderedStateRef.current.token) { 97 | renderedStateRef.current.token = newToken(); 98 | renderedStateRef.current.index = 0; 99 | } 100 | 101 | if (DEBUG) 102 | console.log( 103 | `setupHistoryAugmentationOnce: replace: currentIndex is ${renderedStateRef.current.index}, token is ${renderedStateRef.current.token}` 104 | ); 105 | 106 | const modifiedState = { 107 | ...state, 108 | __next_navigation_guard_token: renderedStateRef.current.token, 109 | __next_navigation_guard_stack_index: renderedStateRef.current.index, 110 | }; 111 | originalReplaceState.call(this, modifiedState, unused, url); 112 | }; 113 | 114 | setupDone = true; 115 | 116 | return { writeState }; 117 | } 118 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", // minimum Next v12 -> Node 12.22.0 w/ ESM support 4 | "lib": ["es2019", "dom", "dom.iterable"], 5 | "types": [], 6 | "jsx": "react", 7 | "module": "node16", 8 | "moduleResolution": "node16", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "noUncheckedIndexedAccess": true, 13 | "skipLibCheck": true, 14 | "outDir": "./dist", 15 | "declaration": true, 16 | "declarationMap": true 17 | }, 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------