diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 5a9651e..3f5ebd8 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -22,9 +22,9 @@ }, "dependencies": { "@cloudflare/vite-plugin": "^1.26.0", - "@pierre/diffs": "^1.1.12", "@diffkit/icons": "workspace:*", "@diffkit/ui": "workspace:*", + "@pierre/diffs": "^1.1.12", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-devtools": "latest", "@tanstack/react-query": "latest", @@ -37,6 +37,7 @@ "better-auth": "^1.6.0", "drizzle-orm": "^0.45.2", "next-themes": "^0.4.6", + "nuqs": "^2.8.9", "octokit": "^5.0.5", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/apps/dashboard/src/components/layouts/dashboard-bottombar.tsx b/apps/dashboard/src/components/layouts/dashboard-bottombar.tsx index e6000c4..c7ff3de 100644 --- a/apps/dashboard/src/components/layouts/dashboard-bottombar.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-bottombar.tsx @@ -1,9 +1,12 @@ import { AlertCircleIcon, XIcon } from "@diffkit/icons"; import { cn } from "@diffkit/ui/lib/utils"; +import { useShowOrgSetupQueryState } from "#/lib/github-access-dialog-query"; +import { openGitHubAccessPrompt } from "#/lib/github-access-modal-store"; import { removeWarning, useWarnings } from "#/lib/warning-store"; export function DashboardBottomBar() { const warnings = useWarnings(); + const [, setShowOrgSetup] = useShowOrgSetupQueryState(); if (warnings.length === 0) return null; @@ -18,16 +21,38 @@ export function DashboardBottomBar() { > {warning.message} - {warning.action && ( - - {warning.action.label} - - )} + {warning.action + ? (() => { + const action = warning.action; + + return action.kind === "link" ? ( + + {action.label} + + ) : ( + + ); + })() + : null} {warning.dismissible && ( + {primaryHref ? ( + + ) : null} + + + + ); +} + +type AccessTarget = { + login: string; + type: "personal" | "org"; + installed: boolean; + scope: "all" | "selected" | null; + href: string | null; + isHighlighted: boolean; +}; + +function buildTargets( + state: GitHubAppAccessState, + highlightedOwner: string | null, +): AccessTarget[] { + const targets: AccessTarget[] = []; + + targets.push({ + login: state.viewerLogin, + type: "personal", + installed: !!state.personalInstallation, + scope: state.personalInstallation + ? state.personalInstallation.repositorySelection === "selected" + ? "selected" + : "all" + : null, + href: getAccessHrefForOwner(state, state.viewerLogin), + isHighlighted: + highlightedOwner?.toLowerCase() === state.viewerLogin.toLowerCase(), + }); + + for (const org of state.organizations) { + const installation = findInstallationForOwner(state, org.login); + targets.push({ + login: org.login, + type: "org", + installed: !!installation, + scope: installation + ? installation.repositorySelection === "selected" + ? "selected" + : "all" + : null, + href: getAccessHrefForOwner(state, org.login), + isHighlighted: + highlightedOwner?.toLowerCase() === org.login.toLowerCase(), + }); + } + + return targets; +} + +function AccessList({ + state, + highlightedOwner, +}: { + state: GitHubAppAccessState; + highlightedOwner: string | null; +}) { + const targets = buildTargets(state, highlightedOwner); + + return ( + + ); +} + +function StatusDot({ installed }: { installed: boolean }) { + return ( +
+ ); +} diff --git a/apps/dashboard/src/lib/github-access-dialog-query.ts b/apps/dashboard/src/lib/github-access-dialog-query.ts new file mode 100644 index 0000000..4786b34 --- /dev/null +++ b/apps/dashboard/src/lib/github-access-dialog-query.ts @@ -0,0 +1,9 @@ +import { parseAsBoolean, useQueryState } from "nuqs"; + +export const showOrgSetupParser = parseAsBoolean + .withDefault(false) + .withOptions({ history: "replace" }); + +export function useShowOrgSetupQueryState() { + return useQueryState("show-org-setup", showOrgSetupParser); +} diff --git a/apps/dashboard/src/lib/github-access-modal-store.ts b/apps/dashboard/src/lib/github-access-modal-store.ts new file mode 100644 index 0000000..250bc4b --- /dev/null +++ b/apps/dashboard/src/lib/github-access-modal-store.ts @@ -0,0 +1,44 @@ +import { useSyncExternalStore } from "react"; + +export type GitHubAccessPrompt = { + source: "onboarding" | "warning"; + owner?: string; + repo?: string; + fallbackHref?: string; +}; + +let prompt: GitHubAccessPrompt | null = null; +const listeners = new Set<() => void>(); + +function emitChange() { + for (const listener of listeners) { + listener(); + } +} + +function subscribe(listener: () => void) { + listeners.add(listener); + return () => listeners.delete(listener); +} + +function getSnapshot() { + return prompt; +} + +export function openGitHubAccessPrompt(nextPrompt: GitHubAccessPrompt) { + prompt = nextPrompt; + emitChange(); +} + +export function closeGitHubAccessPrompt() { + if (!prompt) { + return; + } + + prompt = null; + emitChange(); +} + +export function useGitHubAccessPrompt() { + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +} diff --git a/apps/dashboard/src/lib/github-access.test.ts b/apps/dashboard/src/lib/github-access.test.ts new file mode 100644 index 0000000..c6ba3f8 --- /dev/null +++ b/apps/dashboard/src/lib/github-access.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; +import { + buildGitHubAppInstallUrl, + buildGitHubOrganizationInstallationsUrl, + findInstallationForOwner, + type GitHubAppAccessState, + getAccessHrefForOwner, +} from "./github-access"; + +const state: GitHubAppAccessState = { + viewerLogin: "adn", + appSlug: "diff-kit", + publicInstallUrl: "https://github.com/apps/diff-kit/installations/new", + personalInstallation: { + id: 1, + account: { + login: "adn", + name: null, + avatarUrl: null, + type: "User", + }, + targetType: "User", + repositorySelection: "selected", + manageUrl: "https://github.com/settings/installations/1", + suspendedAt: null, + }, + orgInstallations: [ + { + id: 2, + account: { + login: "supabase", + name: null, + avatarUrl: null, + type: "Organization", + }, + targetType: "Organization", + repositorySelection: "all", + manageUrl: + "https://github.com/organizations/supabase/settings/installations/2", + suspendedAt: null, + }, + ], + organizations: [ + { id: 10, login: "supabase", avatarUrl: null }, + { id: 11, login: "vercel", avatarUrl: null }, + ], + missingOrganizations: [{ id: 11, login: "vercel", avatarUrl: null }], +}; + +describe("buildGitHubAppInstallUrl", () => { + it("builds the public GitHub app install URL", () => { + expect(buildGitHubAppInstallUrl("diff-kit")).toBe( + "https://github.com/apps/diff-kit/installations/new", + ); + }); +}); + +describe("findInstallationForOwner", () => { + it("returns the personal installation for the viewer account", () => { + expect(findInstallationForOwner(state, "adn")?.id).toBe(1); + }); + + it("returns the matching organization installation", () => { + expect(findInstallationForOwner(state, "supabase")?.id).toBe(2); + }); +}); + +describe("getAccessHrefForOwner", () => { + it("prefers the existing installation management URL", () => { + expect(getAccessHrefForOwner(state, "supabase")).toBe( + "https://github.com/organizations/supabase/settings/installations/2", + ); + }); + + it("falls back to the public install URL when the org is missing", () => { + expect(getAccessHrefForOwner(state, "vercel")).toBe( + "https://github.com/organizations/vercel/settings/installations", + ); + }); + + it("uses the provided fallback URL without state", () => { + expect( + getAccessHrefForOwner(null, "vercel", "https://fallback.example"), + ).toBe("https://fallback.example"); + }); +}); + +describe("buildGitHubOrganizationInstallationsUrl", () => { + it("builds the organization installations settings URL", () => { + expect(buildGitHubOrganizationInstallationsUrl("supabase")).toBe( + "https://github.com/organizations/supabase/settings/installations", + ); + }); +}); diff --git a/apps/dashboard/src/lib/github-access.ts b/apps/dashboard/src/lib/github-access.ts new file mode 100644 index 0000000..65d0888 --- /dev/null +++ b/apps/dashboard/src/lib/github-access.ts @@ -0,0 +1,88 @@ +export type GitHubInstallationTargetType = "Organization" | "User" | "Unknown"; + +export type GitHubAppInstallation = { + id: number; + account: { + login: string; + name: string | null; + avatarUrl: string | null; + type: GitHubInstallationTargetType; + }; + targetType: GitHubInstallationTargetType; + repositorySelection: "all" | "selected" | "unknown"; + manageUrl: string | null; + suspendedAt: string | null; +}; + +export type GitHubOrganization = { + id: number; + login: string; + avatarUrl: string | null; +}; + +export type GitHubAppAccessState = { + viewerLogin: string; + appSlug: string | null; + publicInstallUrl: string | null; + personalInstallation: GitHubAppInstallation | null; + orgInstallations: GitHubAppInstallation[]; + organizations: GitHubOrganization[]; + missingOrganizations: GitHubOrganization[]; +}; + +export function buildGitHubAppInstallUrl(slug: string | null | undefined) { + return slug ? `https://github.com/apps/${slug}/installations/new` : null; +} + +export function buildGitHubOrganizationInstallationsUrl(login: string) { + return `https://github.com/organizations/${login}/settings/installations`; +} + +function normalizeLogin(login: string) { + return login.trim().toLowerCase(); +} + +export function findInstallationForOwner( + state: GitHubAppAccessState, + owner: string, +) { + const normalizedOwner = normalizeLogin(owner); + + if (normalizeLogin(state.viewerLogin) === normalizedOwner) { + return state.personalInstallation; + } + + return ( + state.orgInstallations.find( + (installation) => + normalizeLogin(installation.account.login) === normalizedOwner, + ) ?? null + ); +} + +export function getAccessHrefForOwner( + state: GitHubAppAccessState | null | undefined, + owner: string | null | undefined, + fallbackHref?: string, +) { + if (!state || !owner) { + return fallbackHref ?? null; + } + + const normalizedOwner = normalizeLogin(owner); + const installation = findInstallationForOwner(state, owner); + if (installation?.manageUrl) { + return installation.manageUrl; + } + + if ( + normalizeLogin(state.viewerLogin) !== normalizedOwner && + state.organizations.some( + (organization) => normalizeLogin(organization.login) === normalizedOwner, + ) + ) { + return buildGitHubOrganizationInstallationsUrl(owner); + } + + return state.publicInstallUrl ?? fallbackHref ?? null; +} diff --git a/apps/dashboard/src/lib/github.functions.ts b/apps/dashboard/src/lib/github.functions.ts index 376204f..4d4120a 100644 --- a/apps/dashboard/src/lib/github.functions.ts +++ b/apps/dashboard/src/lib/github.functions.ts @@ -27,6 +27,13 @@ import type { SubmitReviewInput, UserRepoSummary, } from "./github.types"; +import { + buildGitHubAppInstallUrl, + type GitHubAppAccessState, + type GitHubAppInstallation, + type GitHubInstallationTargetType, + type GitHubOrganization, +} from "./github-access"; import { getGitHubAppSlug } from "./github-app.server"; import { bustGitHubCache, @@ -128,6 +135,31 @@ type GitHubApiLabel = { description?: string | null; }; +type GitHubInstallationAccountPayload = { + login?: string; + avatar_url?: string | null; + type?: string; +}; + +type GitHubUserInstallationPayload = { + id?: number; + account?: GitHubInstallationAccountPayload | null; + html_url?: string | null; + target_type?: string; + repository_selection?: string; + suspended_at?: string | null; +}; + +type GitHubUserInstallationsPayload = { + installations?: GitHubUserInstallationPayload[]; +}; + +type GitHubAuthenticatedOrgPayload = { + id?: number; + login?: string; + avatar_url?: string | null; +}; + type PullSearchRole = | "all" | "assigned" @@ -176,13 +208,10 @@ function toMutationError(action: string, error: unknown): MutationResult { console.error(`[${action}]`, error); if (error instanceof RequestError) { if (error.status === 403) { - const slug = getGitHubAppSlug(); return { ok: false, error: `Failed to ${action}: Insufficient permissions`, - installUrl: slug - ? `https://github.com/apps/${slug}/installations/new` - : undefined, + installUrl: buildGitHubAppInstallUrl(getGitHubAppSlug()) ?? undefined, }; } if (error.status === 404) { @@ -1187,6 +1216,116 @@ export const getGitHubViewer = createServerFn({ method: "GET" }).handler( }, ); +export const getGitHubAppAccessState = createServerFn({ + method: "GET", +}).handler(async (): Promise => { + const context = await getGitHubContext(); + if (!context) { + return null; + } + + const viewer = await getViewer(context); + const appSlug = getGitHubAppSlug(); + const publicInstallUrl = buildGitHubAppInstallUrl(appSlug); + + let installations: GitHubAppInstallation[] = []; + try { + const installationsResponse = await context.octokit.request( + "GET /user/installations", + { + per_page: 100, + }, + ); + const payload = + installationsResponse.data as GitHubUserInstallationsPayload; + installations = (payload.installations ?? []).flatMap((installation) => { + if (!installation.id || !installation.account?.login) { + return []; + } + + const targetType = toInstallationTargetType(installation.target_type); + + return [ + { + id: installation.id, + account: { + login: installation.account.login, + name: null, + avatarUrl: installation.account.avatar_url ?? null, + type: toInstallationTargetType(installation.account.type), + }, + targetType, + repositorySelection: + installation.repository_selection === "all" || + installation.repository_selection === "selected" + ? installation.repository_selection + : "unknown", + manageUrl: installation.html_url ?? null, + suspendedAt: installation.suspended_at ?? null, + }, + ]; + }); + } catch (error) { + console.error("[github-access] failed to load installations", error); + } + + let organizations: GitHubOrganization[] = []; + try { + const organizationsResponse = await context.octokit.request( + "GET /user/orgs", + { + per_page: 100, + }, + ); + const payload = + organizationsResponse.data as GitHubAuthenticatedOrgPayload[]; + organizations = payload.flatMap((organization) => { + if (!organization.id || !organization.login) { + return []; + } + + return [ + { + id: organization.id, + login: organization.login, + avatarUrl: organization.avatar_url ?? null, + }, + ]; + }); + } catch (error) { + console.error("[github-access] failed to load organizations", error); + } + + const viewerLogin = viewer.login; + const personalInstallation = + installations.find( + (installation) => + installation.targetType === "User" || + installation.account.login.toLowerCase() === viewerLogin.toLowerCase(), + ) ?? null; + const orgInstallations = installations.filter( + (installation) => installation.targetType === "Organization", + ); + const installedOrganizationLogins = new Set( + orgInstallations.map((installation) => + installation.account.login.toLowerCase(), + ), + ); + + return { + viewerLogin, + appSlug, + publicInstallUrl, + personalInstallation, + orgInstallations, + organizations, + missingOrganizations: organizations.filter( + (organization) => + !installedOrganizationLogins.has(organization.login.toLowerCase()), + ), + }; +}); + export const getUserRepos = createServerFn({ method: "GET" }).handler( async (): Promise => { const context = await getGitHubContext(); @@ -1224,6 +1363,16 @@ export const getUserRepos = createServerFn({ method: "GET" }).handler( }, ); +function toInstallationTargetType( + value: string | undefined, +): GitHubInstallationTargetType { + if (value === "Organization" || value === "User") { + return value; + } + + return "Unknown"; +} + export const getMyPulls = createServerFn({ method: "GET" }).handler( async (): Promise => { const context = await getGitHubContext(); diff --git a/apps/dashboard/src/lib/warning-store.ts b/apps/dashboard/src/lib/warning-store.ts index c8f1779..613a317 100644 --- a/apps/dashboard/src/lib/warning-store.ts +++ b/apps/dashboard/src/lib/warning-store.ts @@ -1,9 +1,18 @@ import { useSyncExternalStore } from "react"; -export interface WarningAction { - label: string; - href: string; -} +export type WarningAction = + | { + kind: "link"; + label: string; + href: string; + } + | { + kind: "github-access"; + label: string; + href?: string; + owner?: string; + repo?: string; + }; export interface Warning { id: string; @@ -58,16 +67,19 @@ export function checkPermissionWarning( result.error && result.error.includes("Insufficient permissions") ) { + const [owner = repo] = repo.split("/"); + addWarning({ id: `permissions:${repo}`, message: `Your GitHub App may not have sufficient permissions for ${repo}.`, dismissible: true, - action: result.installUrl - ? { - label: "Configure access", - href: result.installUrl, - } - : undefined, + action: { + kind: "github-access", + label: "Configure access", + href: result.installUrl, + owner, + repo, + }, }); } } diff --git a/apps/dashboard/src/routes/__root.tsx b/apps/dashboard/src/routes/__root.tsx index 7a666a1..af8b131 100644 --- a/apps/dashboard/src/routes/__root.tsx +++ b/apps/dashboard/src/routes/__root.tsx @@ -10,6 +10,7 @@ import { import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { Agentation } from "agentation"; import { ThemeProvider } from "next-themes"; +import { NuqsAdapter } from "nuqs/adapters/tanstack-router"; import { ErrorScreen } from "#/components/layouts/error-screen"; import { buildSeo, buildWebSiteSchema } from "#/lib/seo"; import { siteConfig } from "#/lib/site-config"; @@ -88,8 +89,10 @@ function RootDocument({ children }: { children: React.ReactNode }) { function RootComponent() { return ( - - + + + + ); } diff --git a/packages/ui/src/components/alert-dialog.tsx b/packages/ui/src/components/alert-dialog.tsx index 325679d..0d215b8 100644 --- a/packages/ui/src/components/alert-dialog.tsx +++ b/packages/ui/src/components/alert-dialog.tsx @@ -36,7 +36,7 @@ function AlertDialogOverlay({ {children} - + Close @@ -76,7 +76,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return (
); @@ -102,7 +102,7 @@ function DialogTitle({ return ( ); @@ -115,7 +115,7 @@ function DialogDescription({ return ( ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a0084d..7486f08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,7 +49,7 @@ importers: version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.12) '@tanstack/react-query': specifier: latest - version: 5.96.2(react@19.2.4) + version: 5.97.0(react@19.2.4) '@tanstack/react-router': specifier: latest version: 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -58,7 +58,7 @@ importers: version: 1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-router-ssr-query': specifier: latest - version: 1.166.10(@tanstack/query-core@5.96.2)(@tanstack/react-query@5.96.2(react@19.2.4))(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.166.10(@tanstack/query-core@5.97.0)(@tanstack/react-query@5.97.0(react@19.2.4))(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-start': specifier: latest version: 1.167.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) @@ -77,6 +77,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + nuqs: + specifier: ^2.8.9 + version: 2.8.9(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) octokit: specifier: ^5.0.5 version: 5.0.5 @@ -2524,6 +2527,9 @@ packages: '@speed-highlight/core@1.2.15': resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2654,8 +2660,8 @@ packages: resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} engines: {node: '>=20.19'} - '@tanstack/query-core@5.96.2': - resolution: {integrity: sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==} + '@tanstack/query-core@5.97.0': + resolution: {integrity: sha512-QdpLP5VzVMgo4VtaPppRA2W04UFjIqX+bxke/ZJhE5cfd5UPkRzqIAJQt9uXkQJjqE8LBOMbKv7f8HCsZltXlg==} '@tanstack/react-devtools@0.10.2': resolution: {integrity: sha512-1BmZyxOrI5SqmRJ5MgkYZNNdnlLsJxQRI2YgorrAvcF2MxK6x5RcuStvD8+YlXoMw3JtNukPxoITirKAnKYDQA==} @@ -2666,8 +2672,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-query@5.96.2': - resolution: {integrity: sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==} + '@tanstack/react-query@5.97.0': + resolution: {integrity: sha512-y4So4eGcQoK2WVMAcDNZE9ofB/p5v1OlKvtc1F3uqHwrtifobT7q+ZnXk2mRkc8E84HKYSlAE9z6HXl2V0+ySQ==} peerDependencies: react: ^18 || ^19 @@ -4032,6 +4038,27 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nuqs@2.8.9: + resolution: {integrity: sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==} + peerDependencies: + '@remix-run/react': '>=2' + '@tanstack/react-router': ^1 + next: '>=14.2.0' + react: '>=18.2.0 || ^19.0.0-0' + react-router: ^5 || ^6 || ^7 + react-router-dom: ^5 || ^6 || ^7 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@tanstack/react-router': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + nypm@0.6.5: resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} engines: {node: '>=18'} @@ -6663,6 +6690,8 @@ snapshots: '@speed-highlight/core@1.2.15': {} + '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} '@tailwindcss/node@4.2.2': @@ -6791,7 +6820,7 @@ snapshots: '@tanstack/history@1.161.6': {} - '@tanstack/query-core@5.96.2': {} + '@tanstack/query-core@5.97.0': {} '@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.12)': dependencies: @@ -6806,9 +6835,9 @@ snapshots: - solid-js - utf-8-validate - '@tanstack/react-query@5.96.2(react@19.2.4)': + '@tanstack/react-query@5.97.0(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.96.2 + '@tanstack/query-core': 5.97.0 react: 19.2.4 '@tanstack/react-router-devtools@1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -6822,12 +6851,12 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/react-router-ssr-query@1.166.10(@tanstack/query-core@5.96.2)(@tanstack/react-query@5.96.2(react@19.2.4))(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router-ssr-query@1.166.10(@tanstack/query-core@5.97.0)(@tanstack/react-query@5.97.0(react@19.2.4))(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.96.2 - '@tanstack/react-query': 5.96.2(react@19.2.4) + '@tanstack/query-core': 5.97.0 + '@tanstack/react-query': 5.97.0(react@19.2.4) '@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-ssr-query-core': 1.167.0(@tanstack/query-core@5.96.2)(@tanstack/router-core@1.168.9) + '@tanstack/router-ssr-query-core': 1.167.0(@tanstack/query-core@5.97.0)(@tanstack/router-core@1.168.9) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) transitivePeerDependencies: @@ -6938,9 +6967,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-ssr-query-core@1.167.0(@tanstack/query-core@5.96.2)(@tanstack/router-core@1.168.9)': + '@tanstack/router-ssr-query-core@1.167.0(@tanstack/query-core@5.97.0)(@tanstack/router-core@1.168.9)': dependencies: - '@tanstack/query-core': 5.96.2 + '@tanstack/query-core': 5.97.0 '@tanstack/router-core': 1.168.9 '@tanstack/router-utils@1.161.6': @@ -8430,6 +8459,13 @@ snapshots: dependencies: boolbase: 1.0.0 + nuqs@2.8.9(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + dependencies: + '@standard-schema/spec': 1.0.0 + react: 19.2.4 + optionalDependencies: + '@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + nypm@0.6.5: dependencies: citty: 0.2.2