diff --git a/package.json b/package.json index 8f0f2c86..37048339 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "unplugin-auto-import": "^21.0.0", "unplugin-icons": "^23.0.1", "viem": "^2.48.8", - "vocs": "2.0.0-rc.0", + "vocs": "2.0.0-rc.2", "wagmi": "3.6.14", "waku": "1.0.0-beta.0", "webauthx": "~0.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db1dcaed..09472b8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,8 +106,8 @@ importers: specifier: ^2.48.8 version: 2.48.8(typescript@5.9.3)(zod@4.3.6) vocs: - specifier: 2.0.0-rc.0 - version: 2.0.0-rc.0(@cfworker/json-schema@4.1.1)(@types/react@19.2.14)(mermaid@11.14.0)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.14(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(waku@1.0.0-beta.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) + specifier: 2.0.0-rc.2 + version: 2.0.0-rc.2(@cfworker/json-schema@4.1.1)(@types/react@19.2.14)(mermaid@11.14.0)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.14(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(waku@1.0.0-beta.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) wagmi: specifier: 3.6.14 version: 3.6.14(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.6))(@types/react@19.2.14)(accounts@0.10.7)(react@19.2.6)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)(zod@4.3.6)) @@ -4010,8 +4010,8 @@ packages: vite: optional: true - vocs@2.0.0-rc.0: - resolution: {integrity: sha512-GJdTZADwN6IKsWHITsffMJP15d50kVbHk/g1SDiW4Jh9OVszjiEhT5quQ5krR0wVZCErq49ORr4x5QSJGQNnEQ==} + vocs@2.0.0-rc.2: + resolution: {integrity: sha512-oP1pfLJnlnXPzApgu5JxI7M53A5z0RlkkTy1aHJg/bYFdnsM00ZLjcFCkL/UV/CYmqiahn9T4yP1gLoztnW9FA==} hasBin: true peerDependencies: '@vocs/twoslash-rust': ^0.1.0-rc.0 @@ -8338,10 +8338,11 @@ snapshots: optionalDependencies: vite: 8.0.14(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) - vocs@2.0.0-rc.0(@cfworker/json-schema@4.1.1)(@types/react@19.2.14)(mermaid@11.14.0)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.14(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(waku@1.0.0-beta.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)): + vocs@2.0.0-rc.2(@cfworker/json-schema@4.1.1)(@types/react@19.2.14)(mermaid@11.14.0)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.14(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(waku@1.0.0-beta.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(react-dom@19.2.6(react@19.2.6))(react-server-dom-webpack@19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)))(react@19.2.6)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)): dependencies: '@base-ui/react': 1.3.0(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@codesandbox/sandpack-react': 2.20.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@hono/node-server': 2.0.3(hono@4.12.18) '@iconify-json/lucide': 1.2.102 '@iconify-json/simple-icons': 1.2.77 '@iconify-json/vscode-icons': 1.2.45 diff --git a/src/components/PostHogSetup.tsx b/src/components/PostHogSetup.tsx index 89cb48b0..c0afb351 100644 --- a/src/components/PostHogSetup.tsx +++ b/src/components/PostHogSetup.tsx @@ -1,6 +1,5 @@ 'use client' -import posthog from 'posthog-js' import { useEffect } from 'react' function PostHogInitializer() { @@ -10,7 +9,9 @@ function PostHogInitializer() { if (!posthogKey || !posthogHost) return - const init = () => { + const init = async () => { + const { default: posthog } = await import('posthog-js') + posthog.init(posthogKey, { api_host: '/ingest', ui_host: posthogHost, @@ -28,10 +29,12 @@ function PostHogInitializer() { } if ('requestIdleCallback' in window) { - window.requestIdleCallback(init) - } else { - setTimeout(init, 1) + const idleId = window.requestIdleCallback(init, { timeout: 2_000 }) + return () => window.cancelIdleCallback(idleId) } + + const timeoutId = globalThis.setTimeout(init, 1) + return () => globalThis.clearTimeout(timeoutId) }, []) return null diff --git a/src/lib/pageSettled.ts b/src/lib/pageSettled.ts new file mode 100644 index 00000000..aab11ce9 --- /dev/null +++ b/src/lib/pageSettled.ts @@ -0,0 +1,44 @@ +'use client' + +import { useEffect, useState } from 'react' + +const pageSettledDelayMs = 4_000 + +export function onPageSettled(callback: () => void) { + if (typeof window === 'undefined') return () => {} + + let cancelled = false + let timeoutId: number | undefined + let idleId: number | undefined + + const run = () => { + if (cancelled) return + if ('requestIdleCallback' in window) { + idleId = window.requestIdleCallback(callback, { timeout: 2_000 }) + return + } + callback() + } + + const schedule = () => { + timeoutId = window.setTimeout(run, pageSettledDelayMs) + } + + if (document.readyState === 'complete') schedule() + else window.addEventListener('load', schedule, { once: true }) + + return () => { + cancelled = true + window.removeEventListener('load', schedule) + if (timeoutId) window.clearTimeout(timeoutId) + if (idleId && 'cancelIdleCallback' in window) window.cancelIdleCallback(idleId) + } +} + +export function usePageSettled() { + const [settled, setSettled] = useState(false) + + useEffect(() => onPageSettled(() => setSettled(true)), []) + + return settled +} diff --git a/src/lib/useRootWebAuthnAccount.ts b/src/lib/useRootWebAuthnAccount.ts index 9da27db2..968aab73 100644 --- a/src/lib/useRootWebAuthnAccount.ts +++ b/src/lib/useRootWebAuthnAccount.ts @@ -3,8 +3,8 @@ import { useQuery } from '@tanstack/react-query' import type { WebAuthnP256 } from 'viem/tempo' import { Account } from 'viem/tempo' -import { useConnection } from 'wagmi' -import { config, webAuthnRpId } from '../wagmi.config.ts' +import { useConfig, useConnection } from 'wagmi' +import { webAuthnRpId } from '../wagmi.config.ts' type RootWebAuthnAccount = ReturnType type RootWebAuthnCredential = WebAuthnP256.P256Credential @@ -20,6 +20,7 @@ type RootWebAuthnAccountProvider = { const rootWebAuthnAccountTimeoutMs = 30_000 export function useRootWebAuthnAccount() { + const config = useConfig() const { address, connector } = useConnection() return useQuery({ @@ -45,6 +46,7 @@ export function useRootWebAuthnAccount() { } const credential = await waitForStoredCredential( + config, address as `0x${string}`, rootWebAuthnAccountTimeoutMs, ) @@ -92,6 +94,7 @@ async function waitForProviderAccount( } async function waitForStoredCredential( + config: ReturnType, address: `0x${string}`, timeoutMs = 5_000, ): Promise { diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index a44a84f4..76e56fdf 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -1,11 +1,17 @@ 'use client' -import { Analytics } from '@vercel/analytics/react' -import { SpeedInsights } from '@vercel/speed-insights/react' -import type React from 'react' -import { Toaster } from 'sonner' -import GoogleAnalytics from '../components/GoogleAnalytics' -import PostHogSetup from '../components/PostHogSetup' +import { lazy, type PropsWithChildren, Suspense } from 'react' +import { usePageSettled } from '../lib/pageSettled' + +const Analytics = lazy(() => + import('@vercel/analytics/react').then((module) => ({ default: module.Analytics })), +) +const SpeedInsights = lazy(() => + import('@vercel/speed-insights/react').then((module) => ({ default: module.SpeedInsights })), +) +const Toaster = lazy(() => import('sonner').then((module) => ({ default: module.Toaster }))) +const GoogleAnalytics = lazy(() => import('../components/GoogleAnalytics')) +const PostHogSetup = lazy(() => import('../components/PostHogSetup')) if (typeof window !== 'undefined') { window.addEventListener('vite:preloadError', (event) => { @@ -18,30 +24,41 @@ if (typeof window !== 'undefined') { } export default function Layout( - props: React.PropsWithChildren<{ + props: PropsWithChildren<{ path: string - frontmatter?: { mipd?: boolean } + frontmatter?: { interactive?: boolean; mipd?: boolean } }>, ) { + const pageSettled = usePageSettled() + const needsToaster = Boolean(props.frontmatter?.interactive || props.frontmatter?.mipd) + return ( <> {props.children} - - - - - + + {needsToaster && ( + + )} + {pageSettled && ( + <> + + + + + + )} + ) } diff --git a/src/pages/_mdx-wrapper.tsx b/src/pages/_mdx-wrapper.tsx index 4a28aa90..63f9a4dd 100644 --- a/src/pages/_mdx-wrapper.tsx +++ b/src/pages/_mdx-wrapper.tsx @@ -1,7 +1,7 @@ 'use client' /** - * MDX page wrapper — wraps every MDX page rendered by Vocs. + * MDX page wrapper: wraps every MDX page rendered by Vocs. * * ## Conditional Providers * @@ -20,18 +20,19 @@ * * ## Frontmatter flags * - * - `interactive` — loads the Wagmi/QueryClient provider tree. Required for + * - `interactive` loads the Wagmi/QueryClient provider tree. Required for * any page that uses wallet hooks, Demo components, or guide steps. - * - `mipd` — enables Multi Injected Provider Discovery (auto-detects browser + * - `mipd` enables Multi Injected Provider Discovery (auto-detects browser * extension wallets like MetaMask). Implies `interactive`. Only needed on * pages where users connect external wallets. */ -import type React from 'react' +import { lazy, type ReactNode, Suspense } from 'react' import { Layout, MdxPageContext } from 'vocs' -import Providers from '../components/Providers' -export default function MDXWrapper({ children }: { children: React.ReactNode }) { +const Providers = lazy(() => import('../components/Providers')) + +export default function MDXWrapper({ children }: { children: ReactNode }) { const context = MdxPageContext.use() const frontmatter = context.frontmatter as Record | undefined const needsProviders = Boolean(frontmatter?.interactive || frontmatter?.mipd) @@ -39,7 +40,9 @@ export default function MDXWrapper({ children }: { children: React.ReactNode }) return ( {needsProviders ? ( - {children} + + {children} + ) : ( children )} diff --git a/src/pages/index.mdx b/src/pages/index.mdx index 4b8658bc..9d4b56cf 100644 --- a/src/pages/index.mdx +++ b/src/pages/index.mdx @@ -35,7 +35,7 @@ These docs cover everything from creating a wallet to building payment systems o /> diff --git a/src/wagmi.config.ts b/src/wagmi.config.ts index db3f9e7b..e50afe72 100644 --- a/src/wagmi.config.ts +++ b/src/wagmi.config.ts @@ -109,8 +109,6 @@ export namespace getConfig { export type Config = ReturnType -export const config = getConfig() - export const queryClient = new QueryClient() export function useTempoWalletConnector() {