diff --git a/apps/example/src/routes/__root.tsx b/apps/example/src/routes/__root.tsx index e0c35b6..85221d7 100644 --- a/apps/example/src/routes/__root.tsx +++ b/apps/example/src/routes/__root.tsx @@ -3,6 +3,7 @@ import { Outlet, Scripts, createRootRoute, + useLocation, } from '@tanstack/react-router'; import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; import React from 'react'; @@ -19,13 +20,14 @@ import logoColor100w from '~/images/logo-color-100w.png'; import { TbBrandBluesky, TbBrandTwitter } from 'react-icons/tb'; import { SearchButton } from '~/components/SearchButton'; -import { I18nToggle } from '@tanstack-dev/components'; +import { I18nToggle, getI18nLinks } from '@tanstack-dev/components'; import { BiSolidCheckShield } from 'react-icons/bi'; import { MdSupport, MdLibraryBooks, MdLineAxis } from 'react-icons/md'; import { ThemeToggle } from '~/components/ThemeToggle'; export const Route = createRootRoute({ - head: () => ({ + loader: ({ location }) => ({ href: location.href }), + head: (ctx) => ({ meta: [ { charSet: 'utf-8', @@ -42,6 +44,7 @@ export const Route = createRootRoute({ ], links: [ { rel: 'stylesheet', href: appCss }, + { rel: 'stylesheet', href: carbonStyles, @@ -66,11 +69,18 @@ export const Route = createRootRoute({ { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, { rel: 'icon', href: '/favicon.ico' }, { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, - { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: '' }, + { + rel: 'preconnect', + href: 'https://fonts.gstatic.com', + crossOrigin: '', + }, { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', }, + ...getI18nLinks({ + href: ctx.loaderData.href, + }), ], scripts: [], }), @@ -80,7 +90,6 @@ export const Route = createRootRoute({ function RootDocument() { const detailsRef = React.useRef(null!); const linkClasses = `flex items-center justify-between group px-2 py-1 rounded-lg hover:bg-gray-500 hover:bg-opacity-10 font-black`; - const items = ( <> {libraries.map((library, i) => { @@ -285,7 +294,7 @@ function RootDocument() { > - +
diff --git a/package.json b/package.json index 2f2c1df..789bb1b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "tanstack dev", "scripts": { - "test": "pnpm run -r test", + "test": "pnpm run --filter @tanstack-dev/components test", "dev": "pnpm run --parallel --filter @tanstack-dev/components --filter example dev" }, "author": "zongyanqi@gmail.com", diff --git a/packages/ui/package.json b/packages/ui/package.json index dfc075c..49f4d04 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -15,6 +15,7 @@ "prepublishOnly": "pnpm build" }, "dependencies": { + "@tanstack/react-router": "1.109.2", "jsdom": "^24.0.0", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/packages/ui/src/__tests__/I18nToggle.test.tsx b/packages/ui/src/__tests__/I18nToggle.test.tsx index b4f84d1..32e7be4 100644 --- a/packages/ui/src/__tests__/I18nToggle.test.tsx +++ b/packages/ui/src/__tests__/I18nToggle.test.tsx @@ -1,30 +1,288 @@ -import { render, screen, cleanup } from '@testing-library/react'; -import { describe, it, expect, afterEach } from 'vitest'; +import { + render, + screen, + fireEvent, + cleanup, + act, +} from '@testing-library/react'; +import { describe, it, expect, afterEach, vi } from 'vitest'; import { I18nToggle } from '../index'; +import { LanguageOption } from '../utils/i18n'; + +// Mock the TanStack Router's useLocation hook +vi.mock('@tanstack/react-router', () => ({ + useLocation: () => 'https://tanstack.com/mocked-path', +})); describe('I18nToggle Component', () => { + // Helper functions + const openDropdown = (container: HTMLElement) => { + const button = container.querySelector('[aria-label="Change language"]'); + if (button) { + fireEvent.click(button); + } else { + throw new Error('Button not found'); + } + }; + + const renderComponent = (props = {}) => { + cleanup(); // Ensure DOM is clean before each render + return render(); + }; + afterEach(() => { - cleanup(); + cleanup(); // Cleanup after each test + vi.clearAllMocks(); + }); + + describe('Rendering', () => { + it('renders with correct aria-label', () => { + renderComponent(); + const button = screen.getByRole('button', { name: /change language/i }); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('aria-label', 'Change language'); + }); + + it('applies custom className when provided', () => { + const customClass = 'test-class'; + renderComponent({ className: customClass }); + const button = screen.getByRole('button', { name: /change language/i }); + expect(button).toHaveClass(customClass); + }); + + it('forwards additional props to the button', () => { + renderComponent({ 'data-testid': 'test-id' }); + const button = screen.getByTestId('test-id'); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('aria-label', 'Change language'); + }); + }); + + describe('Dropdown behavior', () => { + it('keeps dropdown hidden initially', () => { + const { container } = renderComponent(); + // Check for the CSS class 'hidden' instead of checking for absence in the DOM + const dropdown = container.querySelector('.absolute'); + expect(dropdown).toHaveClass('hidden'); + }); + + it('opens dropdown menu when clicked', () => { + const { container } = renderComponent(); + openDropdown(container); + + // The menu should be in the document and visible (not having 'hidden' class) + const menu = screen.getByRole('menu'); + const dropdown = container.querySelector('.absolute'); + expect(dropdown).not.toHaveClass('hidden'); + expect(menu).toBeInTheDocument(); + expect(screen.getByText('English')).toBeInTheDocument(); + expect(screen.getByText('简体中文')).toBeInTheDocument(); + }); + + it('closes dropdown when a language option is clicked', () => { + const { container } = renderComponent(); + openDropdown(container); + + const englishLink = screen.getByText('English'); + fireEvent.click(englishLink); + + // Check that the dropdown has the 'hidden' class + const dropdown = container.querySelector('.absolute'); + expect(dropdown).toHaveClass('hidden'); + }); + + it('closes dropdown when clicking outside', async () => { + // Create a div to serve as the outside element + const outsideDiv = document.createElement('div'); + outsideDiv.setAttribute('data-testid', 'outside-element'); + document.body.appendChild(outsideDiv); + + // Render the component + const { container } = renderComponent(); + + // Open the dropdown + openDropdown(container); + + // Verify dropdown is open + const dropdown = container.querySelector('.absolute'); + expect(dropdown).not.toHaveClass('hidden'); + + // Simulate a mousedown event outside the dropdown + act(() => { + const mouseDownEvent = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true, + }); + outsideDiv.dispatchEvent(mouseDownEvent); + }); + + // Wait for state to update - even though React Testing Library usually doesn't need this, + // sometimes with event handlers there can be timing issues + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Check that the dropdown has the 'hidden' class now + expect(dropdown).toHaveClass('hidden'); + + // Clean up + document.body.removeChild(outsideDiv); + }); }); - it('renders with correct aria-label', () => { - render(); - const button = screen.getByRole('button', { name: /change language/i }); - expect(button).toBeInTheDocument(); - expect(button).toHaveAttribute('aria-label', 'Change language'); + describe('Language options', () => { + it('renders with custom language options when provided', () => { + const customOptions: LanguageOption[] = [ + { + label: 'Français', + domain: 'https://fr.tanstack.com', + langCode: 'fr', + }, + { label: 'Español', domain: 'https://es.tanstack.com', langCode: 'es' }, + ]; + + const { container } = renderComponent({ languageOptions: customOptions }); + openDropdown(container); + + expect(screen.getByText('Français')).toBeInTheDocument(); + expect(screen.getByText('Español')).toBeInTheDocument(); + expect(screen.queryByText('English')).not.toBeInTheDocument(); + expect(screen.queryByText('简体中文')).not.toBeInTheDocument(); + }); }); - it('applies custom className when provided', () => { - const customClass = 'test-class'; - render(); - const button = screen.getByRole('button', { name: /change language/i }); - expect(button).toHaveClass(customClass); + describe('URL handling', () => { + it('constructs correct URLs with custom language options and href prop', () => { + const customOptions: LanguageOption[] = [ + { label: 'Deutsch', domain: 'https://de.tanstack.com', langCode: 'de' }, + ]; + const testHref = 'https://tanstack.com/some/path'; + + const { container } = renderComponent({ + href: testHref, + languageOptions: customOptions, + }); + openDropdown(container); + + const germanLink = screen.getByText('Deutsch'); + expect(germanLink).toHaveAttribute( + 'href', + 'https://de.tanstack.com/some/path' + ); + }); + + it('constructs correct URLs with full href prop', () => { + const testHref = 'https://tanstack.com/query/latest/docs/react/overview'; + const { container } = renderComponent({ href: testHref }); + openDropdown(container); + + const englishLink = screen.getByText('English'); + const chineseLink = screen.getByText('简体中文'); + + expect(englishLink).toHaveAttribute( + 'href', + 'https://tanstack.com/query/latest/docs/react/overview' + ); + expect(chineseLink).toHaveAttribute( + 'href', + 'https://zh-hans.tanstack.dev/query/latest/docs/react/overview' + ); + }); + + it('constructs correct URLs when href is a path', () => { + const testHref = '/query/latest/docs/react/overview'; + const { container } = renderComponent({ href: testHref }); + openDropdown(container); + + const englishLink = screen.getByText('English'); + const chineseLink = screen.getByText('简体中文'); + + expect(englishLink).toHaveAttribute( + 'href', + 'https://tanstack.com/query/latest/docs/react/overview' + ); + expect(chineseLink).toHaveAttribute( + 'href', + 'https://zh-hans.tanstack.dev/query/latest/docs/react/overview' + ); + }); + + it('handles href with query parameters and hash correctly', () => { + const testHref = '/some/path?query=param#hash'; + const { container } = renderComponent({ href: testHref }); + openDropdown(container); + + const englishLink = screen.getByText('English'); + expect(englishLink).toHaveAttribute( + 'href', + 'https://tanstack.com/some/path?query=param#hash' + ); + }); + + it('handles href that is just a slash (root path)', () => { + const testHref = '/'; + const { container } = renderComponent({ href: testHref }); + openDropdown(container); + + const englishLink = screen.getByText('English'); + expect(englishLink).toHaveAttribute('href', 'https://tanstack.com/'); + }); }); - it('forwards additional props to the button', () => { - render(); - const button = screen.getByTestId('test-id'); - expect(button).toBeInTheDocument(); - expect(button).toHaveAttribute('aria-label', 'Change language'); + describe('Active language highlighting', () => { + it('highlights the active language correctly based on href domain', () => { + const { container } = renderComponent({ + href: 'https://zh-hans.tanstack.dev/some/page', + currentLanguage: 'zh-Hans', + }); + openDropdown(container); + + const englishLink = screen.getByText('English').closest('a'); + const chineseLink = screen.getByText('简体中文').closest('a'); + + expect(englishLink).not.toHaveClass('bg-gray-100'); + expect(chineseLink).toHaveClass('bg-gray-100'); + expect(chineseLink).toHaveClass('text-gray-900'); + }); + + it('highlights the active language correctly with custom options', () => { + const customOptions: LanguageOption[] = [ + { label: '日本語', domain: 'https://ja.tanstack.com', langCode: 'ja' }, + { label: '한국어', domain: 'https://ko.tanstack.com', langCode: 'ko' }, + ]; + + const { container } = renderComponent({ + href: 'https://ko.tanstack.com/another/path', + languageOptions: customOptions, + currentLanguage: 'ko', + }); + openDropdown(container); + + const japaneseLink = screen.getByText('日本語').closest('a'); + const koreanLink = screen.getByText('한국어').closest('a'); + + expect(japaneseLink).not.toHaveClass('bg-gray-100'); + expect(koreanLink).toHaveClass('bg-gray-100'); + }); + + it('does not highlight active language when href is a path and currentLanguage is not provided', () => { + const { container } = renderComponent({ href: '/some/page' }); + openDropdown(container); + + const englishLink = screen.getByText('English').closest('a'); + const chineseLink = screen.getByText('简体中文').closest('a'); + + expect(englishLink).not.toHaveClass('bg-gray-100'); + expect(chineseLink).not.toHaveClass('bg-gray-100'); + }); + + it('correctly highlights when currentLanguage prop is provided', () => { + const { container } = renderComponent({ currentLanguage: 'zh-Hans' }); + openDropdown(container); + + const englishLink = screen.getByText('English').closest('a'); + const chineseLink = screen.getByText('简体中文').closest('a'); + + expect(englishLink).not.toHaveClass('bg-gray-100'); + expect(chineseLink).toHaveClass('bg-gray-100'); + }); }); }); diff --git a/packages/ui/src/__tests__/i18n.test.ts b/packages/ui/src/__tests__/i18n.test.ts new file mode 100644 index 0000000..00c0616 --- /dev/null +++ b/packages/ui/src/__tests__/i18n.test.ts @@ -0,0 +1,177 @@ +import { describe, it, expect } from 'vitest'; +import { getLanguageUrl, getI18nLinks, LanguageOption } from '../utils/i18n'; + +describe('i18n utilities', () => { + describe('getLanguageUrl', () => { + it('returns the domain when no href is provided', () => { + const domain = 'https://tanstack.com'; + expect(getLanguageUrl(domain)).toBe(domain); + }); + + it('appends path to domain when href starts with /', () => { + const domain = 'https://tanstack.com'; + const href = '/query/latest/docs'; + expect(getLanguageUrl(domain, href)).toBe( + 'https://tanstack.com/query/latest/docs' + ); + }); + + it('extracts path from full URL and appends to domain', () => { + const domain = 'https://zh-hans.tanstack.dev'; + const href = 'https://tanstack.com/query/latest/docs?param=value#hash'; + + expect(getLanguageUrl(domain, href)).toBe( + 'https://zh-hans.tanstack.dev/query/latest/docs?param=value#hash' + ); + }); + + it('preserves query parameters and hash fragments', () => { + const domain = 'https://tanstack.com'; + const href = 'https://example.com/page?query=test&sort=asc#section-2'; + + expect(getLanguageUrl(domain, href)).toBe( + 'https://tanstack.com/page?query=test&sort=asc#section-2' + ); + }); + + it('returns domain for invalid URLs', () => { + const domain = 'https://tanstack.com'; + const invalidHref = 'not-a-url-or-path'; + + expect(getLanguageUrl(domain, invalidHref)).toBe(domain); + }); + + it('handles empty paths correctly', () => { + const domain = 'https://tanstack.com'; + const href = 'https://example.com'; + + expect(getLanguageUrl(domain, href)).toBe('https://tanstack.com/'); + }); + + it('handles root path correctly', () => { + const domain = 'https://tanstack.com'; + const href = '/'; + + expect(getLanguageUrl(domain, href)).toBe('https://tanstack.com/'); + }); + }); + + describe('getI18nLinks', () => { + it('generates correct alternate links for default language options', () => { + const href = '/query/latest/docs'; + const links = getI18nLinks({ href }); + + expect(links).toHaveLength(3); // 2 language options + x-default + + // Check English link + expect(links[0]).toEqual({ + rel: 'alternate', + hrefLang: 'en', + href: 'https://tanstack.com/query/latest/docs', + }); + + // Check Chinese link + expect(links[1]).toEqual({ + rel: 'alternate', + hrefLang: 'zh-Hans', + href: 'https://zh-hans.tanstack.dev/query/latest/docs', + }); + + // Check x-default link + expect(links[2]).toEqual({ + rel: 'alternate', + hrefLang: 'x-default', + href: 'https://tanstack.com/query/latest/docs', + }); + }); + + it('generates correct links with custom language options', () => { + const href = '/some/page'; + const customOptions: LanguageOption[] = [ + { + label: 'Français', + domain: 'https://fr.tanstack.com', + langCode: 'fr', + }, + { label: 'Español', domain: 'https://es.tanstack.com', langCode: 'es' }, + { label: 'English', domain: 'https://tanstack.com', langCode: 'en' }, + ]; + + const links = getI18nLinks({ href, languageOptions: customOptions }); + + expect(links).toHaveLength(4); // 3 language options + x-default + + // Check French link + expect(links[0]).toEqual({ + rel: 'alternate', + hrefLang: 'fr', + href: 'https://fr.tanstack.com/some/page', + }); + + // Check Spanish link + expect(links[1]).toEqual({ + rel: 'alternate', + hrefLang: 'es', + href: 'https://es.tanstack.com/some/page', + }); + + // Check English link + expect(links[2]).toEqual({ + rel: 'alternate', + hrefLang: 'en', + href: 'https://tanstack.com/some/page', + }); + + // Check x-default link (should be English) + expect(links[3]).toEqual({ + rel: 'alternate', + hrefLang: 'x-default', + href: 'https://tanstack.com/some/page', + }); + }); + + it('uses first option as x-default when English is not available', () => { + const href = '/some/path'; + const customOptions: LanguageOption[] = [ + { + label: 'Français', + domain: 'https://fr.tanstack.com', + langCode: 'fr', + }, + { label: 'Español', domain: 'https://es.tanstack.com', langCode: 'es' }, + ]; + + const links = getI18nLinks({ href, languageOptions: customOptions }); + + expect(links).toHaveLength(3); // 2 language options + x-default + + // Check x-default link (should be first option, French) + expect(links[2]).toEqual({ + rel: 'alternate', + hrefLang: 'x-default', + href: 'https://fr.tanstack.com/some/path', + }); + }); + + it('handles full URLs correctly', () => { + const href = + 'https://tanstack.com/query/latest/docs?param=value#section-1'; + const links = getI18nLinks({ href }); + + // Check links have correct paths with query params and hash + expect(links[0].href).toBe( + 'https://tanstack.com/query/latest/docs?param=value#section-1' + ); + expect(links[1].href).toBe( + 'https://zh-hans.tanstack.dev/query/latest/docs?param=value#section-1' + ); + }); + + it('works with an empty language options array', () => { + const href = '/path'; + const links = getI18nLinks({ href, languageOptions: [] }); + + expect(links).toEqual([]); + }); + }); +}); diff --git a/packages/ui/src/components/I18nToggle.tsx b/packages/ui/src/components/I18nToggle.tsx index 486e640..f62b3f2 100644 --- a/packages/ui/src/components/I18nToggle.tsx +++ b/packages/ui/src/components/I18nToggle.tsx @@ -1,19 +1,109 @@ import { MdOutlineTranslate } from 'react-icons/md'; -import { ComponentProps } from 'react'; +import { ComponentProps, useState, useRef, useEffect } from 'react'; +import { useLocation } from '@tanstack/react-router'; +import { + defaultLanguageOptions, + getLanguageUrl, + LanguageCode, + LanguageOption, +} from '../utils/i18n'; interface I18nToggleProps extends ComponentProps<'button'> { className?: string; + href?: string; // Current URL to maintain path when switching languages + languageOptions?: LanguageOption[]; + currentLanguage?: LanguageCode; } -const I18nToggle = ({ className, ...props }: I18nToggleProps) => { +const I18nToggle = ({ + className, + href: hrefProp, + languageOptions = defaultLanguageOptions, + currentLanguage: currentLanguageProp, + ...props +}: I18nToggleProps) => { + const routerLocation = useLocation({ + select: (location) => { + return location.href; + }, + }); + + const href = hrefProp || routerLocation; + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + const [detectedLanguage, setDetectedLanguage] = useState( + null + ); + + // Detect language from html.meta when currentLanguage is not provided + useEffect(() => { + if (typeof document !== 'undefined') { + const htmlLang = document.documentElement.lang as LanguageCode; + if (htmlLang) { + setDetectedLanguage(htmlLang); + } + } + }, []); + + // Use currentLanguageProp if provided, otherwise use the detected language + const currentLanguage = currentLanguageProp || detectedLanguage; + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + return ( - +
+ + +
+
+ {languageOptions.map((option) => { + const isActive = currentLanguage + ? option.langCode === currentLanguage + : false; + return ( + setIsOpen(false)} + > + {option.label} + + ); + })} +
+
+
); }; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index b0679e6..a4742d1 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1 +1,2 @@ export { default as I18nToggle } from './components/I18nToggle'; +export { getI18nLinks } from './utils/i18n' \ No newline at end of file diff --git a/packages/ui/src/utils/i18n.ts b/packages/ui/src/utils/i18n.ts new file mode 100644 index 0000000..1924954 --- /dev/null +++ b/packages/ui/src/utils/i18n.ts @@ -0,0 +1,65 @@ +export interface LanguageOption { + label: string; + domain: string; + langCode: string; // Added for hreflang +} + +export const defaultLanguageOptions: LanguageOption[] = [ + { label: 'English', domain: 'https://tanstack.com', langCode: 'en' }, + { + label: '简体中文', + domain: 'https://zh-hans.tanstack.dev', + langCode: 'zh-Hans', + }, +]; + +export type LanguageCode = 'en' | 'zh-Hans'; + +// Get path from current URL if href is provided +export const getLanguageUrl = (domain: string, href?: string) => { + if (!href) return domain; + + // Check if href is a full URL or just a path + if (href.startsWith('/')) { + return `${domain}${href}`; + } + + try { + const currentUrl = new URL(href); + const path = currentUrl.pathname + currentUrl.search + currentUrl.hash; + return `${domain}${path}`; + } catch (e) { + // If href is not a valid URL (and not a path starting with '/'), return the domain + // This case might occur if href is an invalid string that doesn't represent a URL or a path. + return domain; + } +}; + +export function getI18nLinks({ + href, + languageOptions = defaultLanguageOptions, +}: { + href: string; + languageOptions?: LanguageOption[]; +}) { + const links = languageOptions.map((option) => ({ + rel: 'alternate', + hrefLang: option.langCode, + href: getLanguageUrl(option.domain, href), + })); + + // Add x-default link (usually the first option or a specified default) + // Assuming the first option in defaultLanguageOptions is the default (English) + // https://developers.google.com/search/blog/2013/04/x-default-hreflang-for-international-pages + const defaultOption = + languageOptions.find((opt) => opt.langCode === 'en') || languageOptions[0]; + if (defaultOption) { + links.push({ + rel: 'alternate', + hrefLang: 'x-default', + href: getLanguageUrl(defaultOption.domain, href), + }); + } + + return links; +} diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index c5ae078..15ac1ff 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' -import { resolve } from 'path' -import dts from 'vite-plugin-dts' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import { resolve } from 'path'; +import dts from 'vite-plugin-dts'; // https://vitejs.dev/config/ export default defineConfig({ @@ -17,17 +17,23 @@ export default defineConfig({ lib: { entry: resolve(__dirname, 'src/index.ts'), name: 'TanstackDevComponents', - fileName: 'index' + fileName: 'index', }, rollupOptions: { - external: ['react', 'react-dom', 'react/jsx-runtime'], + external: [ + 'react', + 'react-dom', + 'react/jsx-runtime', + '@tanstack/react-router', + ], output: { globals: { react: 'React', 'react-dom': 'ReactDOM', - 'react/jsx-runtime': 'jsxRuntime' - } - } - } - } -}) + 'react/jsx-runtime': 'jsxRuntime', + '@tanstack/react-router': 'TanstackReactRouter', + }, + }, + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b43fc6c..94907d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: packages/ui: dependencies: + '@tanstack/react-router': + specifier: 1.109.2 + version: 1.109.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) jsdom: specifier: ^24.0.0 version: 24.0.0 @@ -1100,6 +1103,10 @@ packages: resolution: {integrity: sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==} engines: {node: '>=12'} + '@tanstack/history@1.99.13': + resolution: {integrity: sha512-JMd7USmnp8zV8BRGIjALqzPxazvKtQ7PGXQC7n39HpbqdsmfV2ePCzieO84IvN+mwsTrXErpbjI4BfKCa+ZNCg==} + engines: {node: '>=12'} + '@tanstack/react-router-devtools@1.120.2': resolution: {integrity: sha512-89qY5pKdIN6r5G0pHP92mLvorzd7rUlHjvCkjNYOyYduxGQ8a703Y7Fp/RqXibwhjvZcet6BR2IrEhIqg9Yjhw==} engines: {node: '>=12'} @@ -1108,6 +1115,13 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' + '@tanstack/react-router@1.109.2': + resolution: {integrity: sha512-cJZGIvYIrd5lcwbwoB2vxJe379TQZIM5/RAzlDSrvV5KAPlkmK3cqZHZ8zPrsZIBASEyERM8Od+jM9s3qDTXkA==} + engines: {node: '>=12'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + '@tanstack/react-router@1.120.2': resolution: {integrity: sha512-CNduh/O3miW6A/WDMd2cfca8D8x+kVJTYwG5fMaBfcEF/bfjneDnEWXsmKLMdB2iLc6miaRQu66ryPSMdIBUAw==} engines: {node: '>=12'} @@ -4712,6 +4726,8 @@ snapshots: '@tanstack/history@1.115.0': {} + '@tanstack/history@1.99.13': {} + '@tanstack/react-router-devtools@1.120.2(@tanstack/react-router@1.120.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.119.0)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tiny-invariant@1.3.3)': dependencies: '@tanstack/react-router': 1.120.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -4724,6 +4740,17 @@ snapshots: - csstype - tiny-invariant + '@tanstack/react-router@1.109.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/history': 1.99.13 + '@tanstack/react-store': 0.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/router-core': 1.119.0 + jsesc: 3.1.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + '@tanstack/react-router@1.120.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/history': 1.115.0