diff --git a/e2e/react-router/view-transitions/.gitignore b/e2e/react-router/view-transitions/.gitignore new file mode 100644 index 00000000000..a6ea47e5085 --- /dev/null +++ b/e2e/react-router/view-transitions/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/react-router/view-transitions/.vscode/settings.json b/e2e/react-router/view-transitions/.vscode/settings.json new file mode 100644 index 00000000000..00b5278e580 --- /dev/null +++ b/e2e/react-router/view-transitions/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/e2e/react-router/view-transitions/README.md b/e2e/react-router/view-transitions/README.md new file mode 100644 index 00000000000..115199d292c --- /dev/null +++ b/e2e/react-router/view-transitions/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm start` or `yarn start` diff --git a/e2e/react-router/view-transitions/index.html b/e2e/react-router/view-transitions/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/react-router/view-transitions/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/react-router/view-transitions/package.json b/e2e/react-router/view-transitions/package.json new file mode 100644 index 00000000000..2065eb409fa --- /dev/null +++ b/e2e/react-router/view-transitions/package.json @@ -0,0 +1,34 @@ +{ + "name": "tanstack-e2e-router-react-example-view-transitions", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" + }, + "dependencies": { + "@tailwindcss/postcss": "^4.1.15", + "@tanstack/react-router": "workspace:^", + "@tanstack/react-router-devtools": "workspace:^", + "@tanstack/router-plugin": "workspace:^", + "postcss": "^8.5.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "redaxios": "^0.5.1", + "tailwindcss": "^4.1.15", + "zod": "^3.24.2" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/e2e/react-router/view-transitions/playwright.config.ts b/e2e/react-router/view-transitions/playwright.config.ts new file mode 100644 index 00000000000..9ca8b233fd3 --- /dev/null +++ b/e2e/react-router/view-transitions/playwright.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from '@playwright/test' +import { + getDummyServerPort, + getTestServerPort, +} from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(packageJson.name) +const EXTERNAL_PORT = await getDummyServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + globalSetup: './tests/setup/global.setup.ts', + globalTeardown: './tests/setup/global.teardown.ts', + + webServer: { + command: `VITE_NODE_ENV="test" VITE_SERVER_PORT=${PORT} VITE_EXTERNAL_PORT=${EXTERNAL_PORT} pnpm build && pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-router/view-transitions/postcss.config.mjs b/e2e/react-router/view-transitions/postcss.config.mjs new file mode 100644 index 00000000000..a7f73a2d1d7 --- /dev/null +++ b/e2e/react-router/view-transitions/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/e2e/react-router/view-transitions/src/main.tsx b/e2e/react-router/view-transitions/src/main.tsx new file mode 100644 index 00000000000..065d69a2cf8 --- /dev/null +++ b/e2e/react-router/view-transitions/src/main.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { RouterProvider, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, + /* + Using defaultViewTransition would prevent the need to + manually add `viewTransition: true` to every navigation. + + If defaultViewTransition.types is a function, it will be called with the + location change info and should return an array of view transition types. + This is useful if you want to have different view transitions depending on + the navigation's specifics. + + An example use case is sliding in a direction based on the index of the + previous and next routes when navigating via browser history back and forth. + */ + // defaultViewTransition: true + // OR + // defaultViewTransition: { + // types: ({ fromLocation, toLocation }) => { + // let direction = 'none' + + // if (fromLocation) { + // const fromIndex = fromLocation.state.__TSR_index + // const toIndex = toLocation.state.__TSR_index + + // direction = fromIndex > toIndex ? 'right' : 'left' + // } + + // return [`slide-${direction}`] + // }, + // }, +}) + +// Register things for typesafety +declare module '@tanstack/react-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement) + root.render() +} diff --git a/e2e/react-router/view-transitions/src/posts.tsx b/e2e/react-router/view-transitions/src/posts.tsx new file mode 100644 index 00000000000..594d5a075ed --- /dev/null +++ b/e2e/react-router/view-transitions/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/react-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 0)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 0)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/react-router/view-transitions/src/routeTree.gen.ts b/e2e/react-router/view-transitions/src/routeTree.gen.ts new file mode 100644 index 00000000000..20ee76a1a55 --- /dev/null +++ b/e2e/react-router/view-transitions/src/routeTree.gen.ts @@ -0,0 +1,171 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as HowItWorksRouteImport } from './routes/how-it-works' +import { Route as ExploreRouteImport } from './routes/explore' +import { Route as PostsRouteRouteImport } from './routes/posts.route' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PostsIndexRouteImport } from './routes/posts.index' +import { Route as PostsPostIdRouteImport } from './routes/posts.$postId' + +const HowItWorksRoute = HowItWorksRouteImport.update({ + id: '/how-it-works', + path: '/how-it-works', + getParentRoute: () => rootRouteImport, +} as any) +const ExploreRoute = ExploreRouteImport.update({ + id: '/explore', + path: '/explore', + getParentRoute: () => rootRouteImport, +} as any) +const PostsRouteRoute = PostsRouteRouteImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PostsIndexRoute = PostsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => PostsRouteRoute, +} as any) +const PostsPostIdRoute = PostsPostIdRouteImport.update({ + id: '/$postId', + path: '/$postId', + getParentRoute: () => PostsRouteRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/posts': typeof PostsRouteRouteWithChildren + '/explore': typeof ExploreRoute + '/how-it-works': typeof HowItWorksRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/explore': typeof ExploreRoute + '/how-it-works': typeof HowItWorksRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts': typeof PostsIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/posts': typeof PostsRouteRouteWithChildren + '/explore': typeof ExploreRoute + '/how-it-works': typeof HowItWorksRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/posts' + | '/explore' + | '/how-it-works' + | '/posts/$postId' + | '/posts/' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/explore' | '/how-it-works' | '/posts/$postId' | '/posts' + id: + | '__root__' + | '/' + | '/posts' + | '/explore' + | '/how-it-works' + | '/posts/$postId' + | '/posts/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + PostsRouteRoute: typeof PostsRouteRouteWithChildren + ExploreRoute: typeof ExploreRoute + HowItWorksRoute: typeof HowItWorksRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/how-it-works': { + id: '/how-it-works' + path: '/how-it-works' + fullPath: '/how-it-works' + preLoaderRoute: typeof HowItWorksRouteImport + parentRoute: typeof rootRouteImport + } + '/explore': { + id: '/explore' + path: '/explore' + fullPath: '/explore' + preLoaderRoute: typeof ExploreRouteImport + parentRoute: typeof rootRouteImport + } + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof PostsRouteRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/posts/': { + id: '/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof PostsIndexRouteImport + parentRoute: typeof PostsRouteRoute + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof PostsPostIdRouteImport + parentRoute: typeof PostsRouteRoute + } + } +} + +interface PostsRouteRouteChildren { + PostsPostIdRoute: typeof PostsPostIdRoute + PostsIndexRoute: typeof PostsIndexRoute +} + +const PostsRouteRouteChildren: PostsRouteRouteChildren = { + PostsPostIdRoute: PostsPostIdRoute, + PostsIndexRoute: PostsIndexRoute, +} + +const PostsRouteRouteWithChildren = PostsRouteRoute._addFileChildren( + PostsRouteRouteChildren, +) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + PostsRouteRoute: PostsRouteRouteWithChildren, + ExploreRoute: ExploreRoute, + HowItWorksRoute: HowItWorksRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/e2e/react-router/view-transitions/src/routes/__root.tsx b/e2e/react-router/view-transitions/src/routes/__root.tsx new file mode 100644 index 00000000000..247b313a4f9 --- /dev/null +++ b/e2e/react-router/view-transitions/src/routes/__root.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' +import { Link, Outlet, createRootRoute } from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> +
+ + Home + {' '} + + Posts + {' '} +
+
+ + {/* Start rendering router matches */} + + + ) +} diff --git a/e2e/react-router/view-transitions/src/routes/explore.tsx b/e2e/react-router/view-transitions/src/routes/explore.tsx new file mode 100644 index 00000000000..1bb60ed1fca --- /dev/null +++ b/e2e/react-router/view-transitions/src/routes/explore.tsx @@ -0,0 +1,31 @@ +import { createFileRoute } from '@tanstack/react-router' +import { Link } from '@tanstack/react-router' + +export const Route = createFileRoute('/explore')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

+ Explore the CSS to see how to use active-view-transition-type to create + new viewTransitions to use with Tanstack Router. +

+

+ Disclaimer: View Transition Types may not be supported in all browsers + and will fall back to the default browser transition if not available. +

+
+ + <- Previous Page + +
+
+ ) +} diff --git a/e2e/react-router/view-transitions/src/routes/how-it-works.tsx b/e2e/react-router/view-transitions/src/routes/how-it-works.tsx new file mode 100644 index 00000000000..3ae9a88d527 --- /dev/null +++ b/e2e/react-router/view-transitions/src/routes/how-it-works.tsx @@ -0,0 +1,32 @@ +import { createFileRoute } from '@tanstack/react-router' +import { Link } from '@tanstack/react-router' + +export const Route = createFileRoute('/how-it-works')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

This example demonstrates a variety of custom page transitions.

+
+ + <- Previous Page + + + Next Page -> + +
+
+ ) +} diff --git a/e2e/react-router/view-transitions/src/routes/index.tsx b/e2e/react-router/view-transitions/src/routes/index.tsx new file mode 100644 index 00000000000..19f597dc7b3 --- /dev/null +++ b/e2e/react-router/view-transitions/src/routes/index.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from '@tanstack/react-router' +import * as React from 'react' +import { Link } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome To The View Transitions Example!

+
+ + Next Page -> + +
+
+ ) +} diff --git a/e2e/react-router/view-transitions/src/routes/posts.$postId.tsx b/e2e/react-router/view-transitions/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..956ca50ecd4 --- /dev/null +++ b/e2e/react-router/view-transitions/src/routes/posts.$postId.tsx @@ -0,0 +1,29 @@ +import { createFileRoute } from '@tanstack/react-router' +import * as React from 'react' +import { ErrorComponent } from '@tanstack/react-router' +import { fetchPost } from '../posts' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, +}) + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post.title}

+
{post.body}
+
+ ) +} diff --git a/e2e/react-router/view-transitions/src/routes/posts.index.tsx b/e2e/react-router/view-transitions/src/routes/posts.index.tsx new file mode 100644 index 00000000000..c6b65f18923 --- /dev/null +++ b/e2e/react-router/view-transitions/src/routes/posts.index.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/react-router' +import * as React from 'react' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/react-router/view-transitions/src/routes/posts.route.tsx b/e2e/react-router/view-transitions/src/routes/posts.route.tsx new file mode 100644 index 00000000000..88296c3c54a --- /dev/null +++ b/e2e/react-router/view-transitions/src/routes/posts.route.tsx @@ -0,0 +1,44 @@ +import { createFileRoute } from '@tanstack/react-router' +import * as React from 'react' +import { Link, Outlet } from '@tanstack/react-router' +import { fetchPosts } from '../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsLayoutComponent, +}) + +function PostsLayoutComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+
+ +
+
+ ) +} diff --git a/e2e/react-router/view-transitions/src/styles.css b/e2e/react-router/view-transitions/src/styles.css new file mode 100644 index 00000000000..818667133d4 --- /dev/null +++ b/e2e/react-router/view-transitions/src/styles.css @@ -0,0 +1,114 @@ +@import 'tailwindcss'; + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} + +/* Slide Left Transition */ +html:active-view-transition-type(slide-left) { + &::view-transition-old(main-content) { + animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-out-left; + } + &::view-transition-new(main-content) { + animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-in-left; + } +} + +@keyframes slide-out-left { + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } +} + +@keyframes slide-in-left { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +/* Slide Right Transition */ +html:active-view-transition-type(slide-right) { + &::view-transition-old(main-content) { + animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-out-right; + } + &::view-transition-new(main-content) { + animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-in-right; + } +} + +@keyframes slide-out-right { + from { + transform: translateX(0); + } + to { + transform: translateX(100%); + } +} + +@keyframes slide-in-right { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +/* Warp/Rotate Transition */ +html:active-view-transition-type(warp) { + &::view-transition-old(post) { + animation: 400ms ease-out both warp-out; + } + + &::view-transition-new(post) { + animation: 400ms ease-out both warp-in; + } +} + +@keyframes warp-out { + from { + opacity: 1; + filter: blur(0) brightness(1); + transform: scale(1) rotate(0deg); + } + to { + opacity: 0; + filter: blur(15px) brightness(1.8); + transform: scale(1.1) rotate(90deg); + } +} + +@keyframes warp-in { + from { + opacity: 0; + filter: blur(15px) brightness(1.8); + transform: scale(0.9) rotate(-45deg); + } + to { + opacity: 1; + filter: blur(0) brightness(1); + transform: scale(1) rotate(0deg); + } +} diff --git a/e2e/react-router/view-transitions/tests/app.spec.ts b/e2e/react-router/view-transitions/tests/app.spec.ts new file mode 100644 index 00000000000..472bd6ed0bb --- /dev/null +++ b/e2e/react-router/view-transitions/tests/app.spec.ts @@ -0,0 +1,10 @@ +import { test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('placeholder test', async ({ page }) => { + // This is a placeholder test + await page.waitForLoadState('networkidle') +}) diff --git a/e2e/react-router/view-transitions/tests/setup/global.setup.ts b/e2e/react-router/view-transitions/tests/setup/global.setup.ts new file mode 100644 index 00000000000..3593d10ab90 --- /dev/null +++ b/e2e/react-router/view-transitions/tests/setup/global.setup.ts @@ -0,0 +1,6 @@ +import { e2eStartDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function setup() { + await e2eStartDummyServer(packageJson.name) +} diff --git a/e2e/react-router/view-transitions/tests/setup/global.teardown.ts b/e2e/react-router/view-transitions/tests/setup/global.teardown.ts new file mode 100644 index 00000000000..62fd79911cc --- /dev/null +++ b/e2e/react-router/view-transitions/tests/setup/global.teardown.ts @@ -0,0 +1,6 @@ +import { e2eStopDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function teardown() { + await e2eStopDummyServer(packageJson.name) +} diff --git a/e2e/react-router/view-transitions/tsconfig.json b/e2e/react-router/view-transitions/tsconfig.json new file mode 100644 index 00000000000..ad08d6c2bc3 --- /dev/null +++ b/e2e/react-router/view-transitions/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "skipLibCheck": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/react-router/view-transitions/vite.config.js b/e2e/react-router/view-transitions/vite.config.js new file mode 100644 index 00000000000..47e327b7462 --- /dev/null +++ b/e2e/react-router/view-transitions/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + tanstackRouter({ + target: 'react', + autoCodeSplitting: true, + }), + react(), + ], +}) diff --git a/e2e/solid-router/view-transitions/.gitignore b/e2e/solid-router/view-transitions/.gitignore new file mode 100644 index 00000000000..a6ea47e5085 --- /dev/null +++ b/e2e/solid-router/view-transitions/.gitignore @@ -0,0 +1,10 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-router/view-transitions/.vscode/settings.json b/e2e/solid-router/view-transitions/.vscode/settings.json new file mode 100644 index 00000000000..00b5278e580 --- /dev/null +++ b/e2e/solid-router/view-transitions/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/e2e/solid-router/view-transitions/README.md b/e2e/solid-router/view-transitions/README.md new file mode 100644 index 00000000000..115199d292c --- /dev/null +++ b/e2e/solid-router/view-transitions/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm start` or `yarn start` diff --git a/e2e/solid-router/view-transitions/index.html b/e2e/solid-router/view-transitions/index.html new file mode 100644 index 00000000000..9b6335c0ac1 --- /dev/null +++ b/e2e/solid-router/view-transitions/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/e2e/solid-router/view-transitions/package.json b/e2e/solid-router/view-transitions/package.json new file mode 100644 index 00000000000..aa42d2ac382 --- /dev/null +++ b/e2e/solid-router/view-transitions/package.json @@ -0,0 +1,31 @@ +{ + "name": "tanstack-e2e-router-solid-example-view-transitions", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "dev:e2e": "vite", + "build": "vite build && tsc --noEmit", + "serve": "vite preview", + "start": "vite", + "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" + }, + "dependencies": { + "@tailwindcss/postcss": "^4.1.15", + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-router-devtools": "workspace:^", + "@tanstack/router-plugin": "workspace:^", + "postcss": "^8.5.1", + "solid-js": "^1.9.10", + "redaxios": "^0.5.1", + "tailwindcss": "^4.1.15", + "zod": "^3.24.2" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "vite-plugin-solid": "^2.11.10", + "typescript": "^5.7.2", + "vite": "^7.1.7" + } +} diff --git a/e2e/solid-router/view-transitions/playwright.config.ts b/e2e/solid-router/view-transitions/playwright.config.ts new file mode 100644 index 00000000000..9ca8b233fd3 --- /dev/null +++ b/e2e/solid-router/view-transitions/playwright.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from '@playwright/test' +import { + getDummyServerPort, + getTestServerPort, +} from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(packageJson.name) +const EXTERNAL_PORT = await getDummyServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + globalSetup: './tests/setup/global.setup.ts', + globalTeardown: './tests/setup/global.teardown.ts', + + webServer: { + command: `VITE_NODE_ENV="test" VITE_SERVER_PORT=${PORT} VITE_EXTERNAL_PORT=${EXTERNAL_PORT} pnpm build && pnpm serve --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-router/view-transitions/postcss.config.mjs b/e2e/solid-router/view-transitions/postcss.config.mjs new file mode 100644 index 00000000000..a7f73a2d1d7 --- /dev/null +++ b/e2e/solid-router/view-transitions/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/e2e/solid-router/view-transitions/src/main.tsx b/e2e/solid-router/view-transitions/src/main.tsx new file mode 100644 index 00000000000..740315c011e --- /dev/null +++ b/e2e/solid-router/view-transitions/src/main.tsx @@ -0,0 +1,53 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' +import './styles.css' + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultStaleTime: 5000, + scrollRestoration: true, + /* + Using defaultViewTransition would prevent the need to + manually add `viewTransition: true` to every navigation. + + If defaultViewTransition.types is a function, it will be called with the + location change info and should return an array of view transition types. + This is useful if you want to have different view transitions depending on + the navigation's specifics. + + An example use case is sliding in a direction based on the index of the + previous and next routes when navigating via browser history back and forth. + */ + // defaultViewTransition: true + // OR + // defaultViewTransition: { + // types: ({ fromLocation, toLocation }) => { + // let direction = 'none' + + // if (fromLocation) { + // const fromIndex = fromLocation.state.__TSR_index + // const toIndex = toLocation.state.__TSR_index + + // direction = fromIndex > toIndex ? 'right' : 'left' + // } + + // return [`slide-${direction}`] + // }, + // }, +}) + +// Register things for typesafety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('app')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/e2e/solid-router/view-transitions/src/posts.tsx b/e2e/solid-router/view-transitions/src/posts.tsx new file mode 100644 index 00000000000..e306f15d180 --- /dev/null +++ b/e2e/solid-router/view-transitions/src/posts.tsx @@ -0,0 +1,32 @@ +import { notFound } from '@tanstack/solid-router' +import axios from 'redaxios' + +export type PostType = { + id: string + title: string + body: string +} + +export const fetchPost = async (postId: string) => { + console.info(`Fetching post with id ${postId}...`) + await new Promise((r) => setTimeout(r, 0)) + const post = await axios + .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + .then((r) => r.data) + .catch((err) => { + if (err.status === 404) { + throw notFound() + } + throw err + }) + + return post +} + +export const fetchPosts = async () => { + console.info('Fetching posts...') + await new Promise((r) => setTimeout(r, 0)) + return axios + .get>('https://jsonplaceholder.typicode.com/posts') + .then((r) => r.data.slice(0, 10)) +} diff --git a/e2e/solid-router/view-transitions/src/routeTree.gen.ts b/e2e/solid-router/view-transitions/src/routeTree.gen.ts new file mode 100644 index 00000000000..114e34ed9dd --- /dev/null +++ b/e2e/solid-router/view-transitions/src/routeTree.gen.ts @@ -0,0 +1,171 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as HowItWorksRouteImport } from './routes/how-it-works' +import { Route as ExploreRouteImport } from './routes/explore' +import { Route as PostsRouteRouteImport } from './routes/posts.route' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PostsIndexRouteImport } from './routes/posts.index' +import { Route as PostsPostIdRouteImport } from './routes/posts.$postId' + +const HowItWorksRoute = HowItWorksRouteImport.update({ + id: '/how-it-works', + path: '/how-it-works', + getParentRoute: () => rootRouteImport, +} as any) +const ExploreRoute = ExploreRouteImport.update({ + id: '/explore', + path: '/explore', + getParentRoute: () => rootRouteImport, +} as any) +const PostsRouteRoute = PostsRouteRouteImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PostsIndexRoute = PostsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => PostsRouteRoute, +} as any) +const PostsPostIdRoute = PostsPostIdRouteImport.update({ + id: '/$postId', + path: '/$postId', + getParentRoute: () => PostsRouteRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/posts': typeof PostsRouteRouteWithChildren + '/explore': typeof ExploreRoute + '/how-it-works': typeof HowItWorksRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/explore': typeof ExploreRoute + '/how-it-works': typeof HowItWorksRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts': typeof PostsIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/posts': typeof PostsRouteRouteWithChildren + '/explore': typeof ExploreRoute + '/how-it-works': typeof HowItWorksRoute + '/posts/$postId': typeof PostsPostIdRoute + '/posts/': typeof PostsIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/posts' + | '/explore' + | '/how-it-works' + | '/posts/$postId' + | '/posts/' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/explore' | '/how-it-works' | '/posts/$postId' | '/posts' + id: + | '__root__' + | '/' + | '/posts' + | '/explore' + | '/how-it-works' + | '/posts/$postId' + | '/posts/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + PostsRouteRoute: typeof PostsRouteRouteWithChildren + ExploreRoute: typeof ExploreRoute + HowItWorksRoute: typeof HowItWorksRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/how-it-works': { + id: '/how-it-works' + path: '/how-it-works' + fullPath: '/how-it-works' + preLoaderRoute: typeof HowItWorksRouteImport + parentRoute: typeof rootRouteImport + } + '/explore': { + id: '/explore' + path: '/explore' + fullPath: '/explore' + preLoaderRoute: typeof ExploreRouteImport + parentRoute: typeof rootRouteImport + } + '/posts': { + id: '/posts' + path: '/posts' + fullPath: '/posts' + preLoaderRoute: typeof PostsRouteRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/posts/': { + id: '/posts/' + path: '/' + fullPath: '/posts/' + preLoaderRoute: typeof PostsIndexRouteImport + parentRoute: typeof PostsRouteRoute + } + '/posts/$postId': { + id: '/posts/$postId' + path: '/$postId' + fullPath: '/posts/$postId' + preLoaderRoute: typeof PostsPostIdRouteImport + parentRoute: typeof PostsRouteRoute + } + } +} + +interface PostsRouteRouteChildren { + PostsPostIdRoute: typeof PostsPostIdRoute + PostsIndexRoute: typeof PostsIndexRoute +} + +const PostsRouteRouteChildren: PostsRouteRouteChildren = { + PostsPostIdRoute: PostsPostIdRoute, + PostsIndexRoute: PostsIndexRoute, +} + +const PostsRouteRouteWithChildren = PostsRouteRoute._addFileChildren( + PostsRouteRouteChildren, +) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + PostsRouteRoute: PostsRouteRouteWithChildren, + ExploreRoute: ExploreRoute, + HowItWorksRoute: HowItWorksRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/e2e/solid-router/view-transitions/src/routes/__root.tsx b/e2e/solid-router/view-transitions/src/routes/__root.tsx new file mode 100644 index 00000000000..2409a923113 --- /dev/null +++ b/e2e/solid-router/view-transitions/src/routes/__root.tsx @@ -0,0 +1,52 @@ +import { + HeadContent, + Link, + Outlet, + createRootRoute, +} from '@tanstack/solid-router' +import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' + +export const Route = createRootRoute({ + component: RootComponent, + notFoundComponent: () => { + return ( +
+

This is the notFoundComponent configured on root route

+ Start Over +
+ ) + }, +}) + +function RootComponent() { + return ( + <> + +
+ + Home + {' '} + + Posts + {' '} +
+
+ + {/* Start rendering router matches */} + + + ) +} diff --git a/e2e/solid-router/view-transitions/src/routes/explore.tsx b/e2e/solid-router/view-transitions/src/routes/explore.tsx new file mode 100644 index 00000000000..d0a45656198 --- /dev/null +++ b/e2e/solid-router/view-transitions/src/routes/explore.tsx @@ -0,0 +1,30 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/explore')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

+ Explore the CSS to see how to use active-view-transition-type to create + new viewTransitions to use with Tanstack Router. +

+

+ Disclaimer: View Transition Types may not be supported in all browsers + and will fall back to the default browser transition if not available. +

+
+ + <- Previous Page + +
+
+ ) +} diff --git a/e2e/solid-router/view-transitions/src/routes/how-it-works.tsx b/e2e/solid-router/view-transitions/src/routes/how-it-works.tsx new file mode 100644 index 00000000000..ac217070ad6 --- /dev/null +++ b/e2e/solid-router/view-transitions/src/routes/how-it-works.tsx @@ -0,0 +1,31 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/how-it-works')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

This example demonstrates a variety of custom page transitions.

+
+ + <- Previous Page + + + Next Page -> + +
+
+ ) +} diff --git a/e2e/solid-router/view-transitions/src/routes/index.tsx b/e2e/solid-router/view-transitions/src/routes/index.tsx new file mode 100644 index 00000000000..76a70bc5dd0 --- /dev/null +++ b/e2e/solid-router/view-transitions/src/routes/index.tsx @@ -0,0 +1,23 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Welcome To The View Transitions Example!

+
+ + Next Page -> + +
+
+ ) +} diff --git a/e2e/solid-router/view-transitions/src/routes/posts.$postId.tsx b/e2e/solid-router/view-transitions/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..55f8871d03f --- /dev/null +++ b/e2e/solid-router/view-transitions/src/routes/posts.$postId.tsx @@ -0,0 +1,27 @@ +import { ErrorComponent, createFileRoute } from '@tanstack/solid-router' +import { fetchPost } from '../posts' +import type { ErrorComponentProps } from '@tanstack/solid-router' + +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} + +export const Route = createFileRoute('/posts/$postId')({ + loader: async ({ params: { postId } }) => fetchPost(postId), + errorComponent: PostErrorComponent, + notFoundComponent: () => { + return

Post not found

+ }, + component: PostComponent, +}) + +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post().title}

+
{post().body}
+
+ ) +} diff --git a/e2e/solid-router/view-transitions/src/routes/posts.index.tsx b/e2e/solid-router/view-transitions/src/routes/posts.index.tsx new file mode 100644 index 00000000000..33d0386c195 --- /dev/null +++ b/e2e/solid-router/view-transitions/src/routes/posts.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/e2e/solid-router/view-transitions/src/routes/posts.route.tsx b/e2e/solid-router/view-transitions/src/routes/posts.route.tsx new file mode 100644 index 00000000000..a54b99f36a9 --- /dev/null +++ b/e2e/solid-router/view-transitions/src/routes/posts.route.tsx @@ -0,0 +1,42 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/solid-router' +import { fetchPosts } from '../posts' + +export const Route = createFileRoute('/posts')({ + loader: fetchPosts, + component: PostsLayoutComponent, +}) + +function PostsLayoutComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts(), { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+
+ +
+
+ ) +} diff --git a/e2e/solid-router/view-transitions/src/styles.css b/e2e/solid-router/view-transitions/src/styles.css new file mode 100644 index 00000000000..818667133d4 --- /dev/null +++ b/e2e/solid-router/view-transitions/src/styles.css @@ -0,0 +1,114 @@ +@import 'tailwindcss'; + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +html { + color-scheme: light dark; +} +* { + @apply border-gray-200 dark:border-gray-800; +} +body { + @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; +} + +/* Slide Left Transition */ +html:active-view-transition-type(slide-left) { + &::view-transition-old(main-content) { + animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-out-left; + } + &::view-transition-new(main-content) { + animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-in-left; + } +} + +@keyframes slide-out-left { + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } +} + +@keyframes slide-in-left { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +/* Slide Right Transition */ +html:active-view-transition-type(slide-right) { + &::view-transition-old(main-content) { + animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-out-right; + } + &::view-transition-new(main-content) { + animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-in-right; + } +} + +@keyframes slide-out-right { + from { + transform: translateX(0); + } + to { + transform: translateX(100%); + } +} + +@keyframes slide-in-right { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +/* Warp/Rotate Transition */ +html:active-view-transition-type(warp) { + &::view-transition-old(post) { + animation: 400ms ease-out both warp-out; + } + + &::view-transition-new(post) { + animation: 400ms ease-out both warp-in; + } +} + +@keyframes warp-out { + from { + opacity: 1; + filter: blur(0) brightness(1); + transform: scale(1) rotate(0deg); + } + to { + opacity: 0; + filter: blur(15px) brightness(1.8); + transform: scale(1.1) rotate(90deg); + } +} + +@keyframes warp-in { + from { + opacity: 0; + filter: blur(15px) brightness(1.8); + transform: scale(0.9) rotate(-45deg); + } + to { + opacity: 1; + filter: blur(0) brightness(1); + transform: scale(1) rotate(0deg); + } +} diff --git a/e2e/solid-router/view-transitions/tests/app.spec.ts b/e2e/solid-router/view-transitions/tests/app.spec.ts new file mode 100644 index 00000000000..472bd6ed0bb --- /dev/null +++ b/e2e/solid-router/view-transitions/tests/app.spec.ts @@ -0,0 +1,10 @@ +import { test } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('placeholder test', async ({ page }) => { + // This is a placeholder test + await page.waitForLoadState('networkidle') +}) diff --git a/e2e/solid-router/view-transitions/tests/setup/global.setup.ts b/e2e/solid-router/view-transitions/tests/setup/global.setup.ts new file mode 100644 index 00000000000..3593d10ab90 --- /dev/null +++ b/e2e/solid-router/view-transitions/tests/setup/global.setup.ts @@ -0,0 +1,6 @@ +import { e2eStartDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function setup() { + await e2eStartDummyServer(packageJson.name) +} diff --git a/e2e/solid-router/view-transitions/tests/setup/global.teardown.ts b/e2e/solid-router/view-transitions/tests/setup/global.teardown.ts new file mode 100644 index 00000000000..62fd79911cc --- /dev/null +++ b/e2e/solid-router/view-transitions/tests/setup/global.teardown.ts @@ -0,0 +1,6 @@ +import { e2eStopDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function teardown() { + await e2eStopDummyServer(packageJson.name) +} diff --git a/e2e/solid-router/view-transitions/tsconfig.json b/e2e/solid-router/view-transitions/tsconfig.json new file mode 100644 index 00000000000..e011e786b59 --- /dev/null +++ b/e2e/solid-router/view-transitions/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "skipLibCheck": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/solid-router/view-transitions/vite.config.js b/e2e/solid-router/view-transitions/vite.config.js new file mode 100644 index 00000000000..74e9ca3db36 --- /dev/null +++ b/e2e/solid-router/view-transitions/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' +import { tanstackRouter } from '@tanstack/router-plugin/vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + tanstackRouter({ + target: 'solid', + autoCodeSplitting: true, + }), + solid(), + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d60640f6027..918d9c70403 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -982,6 +982,61 @@ importers: specifier: ^7.1.7 version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + e2e/react-router/view-transitions: + dependencies: + '@tailwindcss/postcss': + specifier: ^4.1.15 + version: 4.1.15 + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-router-devtools': + specifier: workspace:^ + version: link:../../../packages/react-router-devtools + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../../packages/router-plugin + postcss: + specifier: ^8.5.1 + version: 8.5.6 + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + tailwindcss: + specifier: ^4.1.15 + version: 4.1.17 + zod: + specifier: ^3.24.2 + version: 3.25.57 + devDependencies: + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/react': + specifier: ^19.2.2 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.2.2 + version: 19.2.2(@types/react@19.2.2) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + e2e/react-start/basic: dependencies: '@tanstack/react-router': @@ -2675,6 +2730,52 @@ importers: specifier: ^2.11.10 version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + e2e/solid-router/view-transitions: + dependencies: + '@tailwindcss/postcss': + specifier: ^4.1.15 + version: 4.1.15 + '@tanstack/router-plugin': + specifier: workspace:* + version: link:../../../packages/router-plugin + '@tanstack/solid-router': + specifier: workspace:^ + version: link:../../../packages/solid-router + '@tanstack/solid-router-devtools': + specifier: workspace:^ + version: link:../../../packages/solid-router-devtools + postcss: + specifier: ^8.5.1 + version: 8.5.6 + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + solid-js: + specifier: 1.9.10 + version: 1.9.10 + tailwindcss: + specifier: ^4.1.15 + version: 4.1.17 + zod: + specifier: ^3.24.2 + version: 3.25.57 + devDependencies: + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + e2e/solid-start/basic: dependencies: '@tanstack/solid-router':