diff --git a/packages/swingset/src/components/DocsViewer.tsx b/packages/swingset/src/components/DocsViewer.tsx index 95c83d0de05..fe05e51d56c 100644 --- a/packages/swingset/src/components/DocsViewer.tsx +++ b/packages/swingset/src/components/DocsViewer.tsx @@ -1,45 +1,66 @@ 'use client'; -import dynamic from 'next/dynamic'; - import { getModule } from '@/lib/registry'; +import AccordionDoc from '../stories/accordion.mdx'; +import AutocompleteDoc from '../stories/autocomplete.mdx'; +import ButtonDoc from '../stories/button.mdx'; +import CollapsibleDoc from '../stories/collapsible.mdx'; +import DeleteOrganizationDoc from '../stories/delete-organization.mdx'; +import DestructiveDoc from '../stories/destructive.mdx'; +import DialogComponentDoc from '../stories/dialog.component.mdx'; +import DialogDoc from '../stories/dialog.mdx'; +import InputDoc from '../stories/input.mdx'; +import LeaveOrganizationDoc from '../stories/leave-organization.mdx'; +import MenuDoc from '../stories/menu.mdx'; +import OrganizationProfileDoc from '../stories/organization-profile.mdx'; +import OrganizationProfileGeneralDoc from '../stories/organization-profile-general.mdx'; +import PopoverDoc from '../stories/popover.mdx'; +import SelectDoc from '../stories/select.mdx'; +import TabsComponentDoc from '../stories/tabs.component.mdx'; +import TabsDoc from '../stories/tabs.mdx'; +import TooltipDoc from '../stories/tooltip.mdx'; import { PlaygroundProvider } from './PlaygroundContext'; import { ViewSource } from './ViewSource'; -// MDX docs keyed by `group` slug → `component` slug. Group-aware so identically-named -// entries (the headless `Dialog` primitive vs. the styled `Dialog` component) stay distinct. +// MDX docs are imported statically (not via `next/dynamic`). A `next/dynamic` lazy boundary +// resolves differently on the server vs. the client, shifting React's `useId` tree-path and +// hydration-mismatching any `useId` component inside (Tabs, Dialog, …). Static imports render an +// identical tree on both sides, so `useId` stays stable and SSR is preserved. +// +// Keyed by `group` slug → `component` slug. Group-aware so identically-named entries (the headless +// `Dialog` primitive vs. the styled `Dialog` component) stay distinct. const docModules: Record> = { aio: { - 'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')), + 'organization-profile': OrganizationProfileDoc, }, panels: { - 'organization-profile-general': dynamic(() => import('../stories/organization-profile-general.mdx')), + 'organization-profile-general': OrganizationProfileGeneralDoc, }, sections: { - 'leave-organization': dynamic(() => import('../stories/leave-organization.mdx')), - 'delete-organization': dynamic(() => import('../stories/delete-organization.mdx')), + 'leave-organization': LeaveOrganizationDoc, + 'delete-organization': DeleteOrganizationDoc, }, blocks: { - destructive: dynamic(() => import('../stories/destructive.mdx')), + destructive: DestructiveDoc, }, components: { - button: dynamic(() => import('../stories/button.mdx')), - input: dynamic(() => import('../stories/input.mdx')), - dialog: dynamic(() => import('../stories/dialog.component.mdx')), - tabs: dynamic(() => import('../stories/tabs.component.mdx')), + button: ButtonDoc, + input: InputDoc, + dialog: DialogComponentDoc, + tabs: TabsComponentDoc, }, primitives: { // Headless primitives — alphabetical. - accordion: dynamic(() => import('../stories/accordion.mdx')), - autocomplete: dynamic(() => import('../stories/autocomplete.mdx')), - collapsible: dynamic(() => import('../stories/collapsible.mdx')), - dialog: dynamic(() => import('../stories/dialog.mdx')), - menu: dynamic(() => import('../stories/menu.mdx')), - popover: dynamic(() => import('../stories/popover.mdx')), - select: dynamic(() => import('../stories/select.mdx')), - tabs: dynamic(() => import('../stories/tabs.mdx')), - tooltip: dynamic(() => import('../stories/tooltip.mdx')), + accordion: AccordionDoc, + autocomplete: AutocompleteDoc, + collapsible: CollapsibleDoc, + dialog: DialogDoc, + menu: MenuDoc, + popover: PopoverDoc, + select: SelectDoc, + tabs: TabsDoc, + tooltip: TooltipDoc, }, }; diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 28f5b8b602a..4888815d82e 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -22,14 +22,14 @@ import { meta as leaveOrganizationMeta, } from '../stories/leave-organization.stories'; import { meta as menuMeta } from '../stories/menu.stories'; -import { - Default as OrganizationProfileGeneralDefault, - meta as organizationProfileGeneralMeta, -} from '../stories/organization-profile-general.stories'; import { Default as OrganizationProfileDefault, meta as organizationProfileMeta, } from '../stories/organization-profile.stories'; +import { + Default as OrganizationProfileGeneralDefault, + meta as organizationProfileGeneralMeta, +} from '../stories/organization-profile-general.stories'; import { meta as popoverMeta } from '../stories/popover.stories'; import { meta as selectMeta } from '../stories/select.stories'; import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories'; diff --git a/packages/ui/package.json b/packages/ui/package.json index 125a050ed0c..450962ce64c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -104,6 +104,7 @@ "@solana/wallet-adapter-react": "catalog:module-manager", "@solana/wallet-standard": "catalog:module-manager", "@swc/helpers": "catalog:repo", + "@tanstack/react-query": "^5.100.6", "alien-signals": "2.0.6", "copy-to-clipboard": "3.3.3", "core-js": "catalog:repo", diff --git a/packages/ui/src/mosaic/aio/organization-profile.tsx b/packages/ui/src/mosaic/aio/organization-profile.tsx index 743da8386b5..efe936dfdf9 100644 --- a/packages/ui/src/mosaic/aio/organization-profile.tsx +++ b/packages/ui/src/mosaic/aio/organization-profile.tsx @@ -1,6 +1,9 @@ +import { ClientSuspense } from '../components/client-suspense'; import { Box } from '../components/box'; +import { SectionSkeleton } from '../components/section-skeleton'; import { Tabs } from '../components/tabs'; import { OrganizationProfileGeneral } from '../panels/organization-profile-general'; +import { OrganizationMembers } from '../sections/organization-members'; export function OrganizationProfile() { return ( @@ -28,16 +31,9 @@ export function OrganizationProfile() { -

} - sx={t => ({ - ...t.text('base'), - fontWeight: t.font.medium, - textAlign: 'center', - })} - > - Members content - + }> + + diff --git a/packages/ui/src/mosaic/components/client-suspense.tsx b/packages/ui/src/mosaic/components/client-suspense.tsx new file mode 100644 index 00000000000..93b5b3b1e4a --- /dev/null +++ b/packages/ui/src/mosaic/components/client-suspense.tsx @@ -0,0 +1,26 @@ +import { type ReactNode, Suspense, useSyncExternalStore } from 'react'; + +const subscribe = () => () => {}; + +/** + * A Suspense boundary that only activates on the client. + * + * On the server (and the first client render, for hydration parity) it renders `fallback` + * directly — it does **not** mount `children`, so nothing suspends server-side. That matters for + * client-only data: a real `` whose promise can only resolve in the browser would keep + * the SSR stream open forever (perpetual tab loading) or re-suspend in a loop. Once mounted, it + * upgrades to a normal `` and the children load + suspend as usual. + */ +export function ClientSuspense({ fallback, children }: { fallback: ReactNode; children: ReactNode }) { + const mounted = useSyncExternalStore( + subscribe, + () => true, // client + () => false, // server + first hydration pass + ); + + if (!mounted) { + return <>{fallback}; + } + + return {children}; +} diff --git a/packages/ui/src/mosaic/data/members-query.ts b/packages/ui/src/mosaic/data/members-query.ts new file mode 100644 index 00000000000..c6df0a7faed --- /dev/null +++ b/packages/ui/src/mosaic/data/members-query.ts @@ -0,0 +1,44 @@ +import { infiniteQueryOptions } from '@tanstack/react-query'; + +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +/** + * Server collection: the org's members, paginated. This is the half that genuinely wants + * TanStack — caching, staleness, infinite pagination, invalidation — none of which a signal + * gives you. A colocated query-options factory is the unit of reuse: the same `membersQuery(id)` + * feeds suspense reads, prefetch, and tests. + */ + +export const membersKey = (orgId: string) => ['org', orgId, 'members'] as const; + +export interface MemberRecord { + id: string; + name: string; + role: string; +} + +interface MembersPage { + members: MemberRecord[]; + nextPage: number | null; +} + +const PAGE_SIZE = 3; +const TOTAL_PAGES = 3; + +export const membersQuery = (orgId: string) => + infiniteQueryOptions({ + queryKey: membersKey(orgId), + initialPageParam: 1, + queryFn: async ({ pageParam }): Promise => { + await delay(500); + const base = (pageParam - 1) * PAGE_SIZE; + const members = Array.from({ length: PAGE_SIZE }, (_, i) => ({ + id: `mem_${base + i}`, + name: `Member ${base + i + 1}`, + role: base + i === 0 ? 'org:admin' : 'org:member', + })); + return { members, nextPage: pageParam < TOTAL_PAGES ? pageParam + 1 : null }; + }, + getNextPageParam: last => last.nextPage, + staleTime: 60_000, + }); diff --git a/packages/ui/src/mosaic/data/organization-store.ts b/packages/ui/src/mosaic/data/organization-store.ts new file mode 100644 index 00000000000..604446efe2b --- /dev/null +++ b/packages/ui/src/mosaic/data/organization-store.ts @@ -0,0 +1,77 @@ +import { signal } from 'alien-signals'; + +/** + * Self-contained store for the org-profile spike. No external data dependencies — it models the + * two distinct kinds of state the real components consume, so component code is written exactly as + * it would be against production and only this file would be swapped: + * + * - the live, client-owned org **identity** (the active organization + the current user's + * membership) — modelled as a signal, the synchronous source of truth; + * - server **collections** (members, invitations) — fetched + cached by TanStack Query, see + * `members-query.ts`. + * + * Resources carry their own mutation methods (`destroy`), mirroring the real resource API so the + * mutation hooks call `organization.destroy()` / `membership.destroy()` rather than reaching into + * the store. + */ + +/** Time until the simulated org identity hydrates (analogue of waiting on the SDK to load). */ +const LOAD_DELAY_MS = 600; +/** Artificial latency for `destroy()` mutations. */ +const MUTATION_DELAY_MS = 2000; + +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +export interface OrganizationResource { + id: string; + name: string; + slug: string | null; + membersCount: number; + /** Permanently delete the organization (admin-only in the real API). */ + destroy: () => Promise; +} + +export interface MembershipResource { + id: string; + /** e.g. 'org:admin' | 'org:member' */ + role: string; + /** Leave the organization (removes the current member). */ + destroy: () => Promise; +} + +export interface ActiveOrganization { + organization: OrganizationResource; + membership: MembershipResource; +} + +// read: organizationSignal() · write: organizationSignal(next) +export const organizationSignal = signal(null); + +let hydration: Promise | null = null; + +/** + * Resolves once the active organization is available. Idempotent — every `useOrganization()` + * instance awaits the same promise, so independently-mounted sections suspend and resume on the + * same tick instead of racing their own timers. + */ +export function ensureOrganization(): Promise { + if (!hydration) { + hydration = delay(LOAD_DELAY_MS).then(() => { + organizationSignal({ + organization: { + id: 'org_mock', + name: "Alex's Organization", + slug: 'alex-org', + membersCount: 4, + destroy: () => delay(MUTATION_DELAY_MS), + }, + membership: { + id: 'mem_mock', + role: 'org:admin', + destroy: () => delay(MUTATION_DELAY_MS), + }, + }); + }); + } + return hydration; +} diff --git a/packages/ui/src/mosaic/data/query-client.ts b/packages/ui/src/mosaic/data/query-client.ts new file mode 100644 index 00000000000..3b21e00b6a2 --- /dev/null +++ b/packages/ui/src/mosaic/data/query-client.ts @@ -0,0 +1,39 @@ +import { QueryClient } from '@tanstack/react-query'; + +/** Builds a QueryClient with Mosaic's cache defaults — the one place construction lives. */ +export function makeQueryClient(): QueryClient { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60_000, + retry: false, + refetchOnWindowFocus: false, + }, + }, + }); +} + +let browserClient: QueryClient | undefined; + +/** + * The QueryClient to read/mutate against, passed explicitly to each `useQuery`/`useMutation` + * (react-query's optional `queryClient` arg) so any section renders standalone — no + * `QueryClientProvider` required in the tree (e.g. a per-component story). + * + * - Browser: one memoized client per tab → shared cache (dedup, invalidation, staleness). + * - Server: a fresh client per call, so one request's cache never leaks into another's. + * + * That split is what keeps us SSR-**safe**. Going further to SSR-**with-data** (server prefetch → + * `dehydrate()` → ``) means moving this behind a per-request provider — kept a + * one-file swap by routing all construction through here. + * + * Caveat: on the server each call returns a distinct client, so cross-component cache sharing + * within a single SSR render only holds once that provider exists. Fine for the current sections — + * they suspend on the org signal before any server-side query runs. + */ +export function getMosaicQueryClient(): QueryClient { + if (typeof window === 'undefined') { + return makeQueryClient(); + } + return (browserClient ??= makeQueryClient()); +} diff --git a/packages/ui/src/mosaic/data/use-org-mutations.ts b/packages/ui/src/mosaic/data/use-org-mutations.ts new file mode 100644 index 00000000000..9da3047779b --- /dev/null +++ b/packages/ui/src/mosaic/data/use-org-mutations.ts @@ -0,0 +1,40 @@ +import { useMutation } from '@tanstack/react-query'; + +import { membersKey } from './members-query'; +import type { MembershipResource, OrganizationResource } from './organization-store'; +import { getMosaicQueryClient } from './query-client'; + +/** + * Mutations via `useMutation`, calling the resource's own method — `isPending` / `error` / + * `onSuccess` come for free, replacing the hand-rolled `isDeleting` `useState` + `await` dance and + * giving cache invalidation a first-class home. + * + * The client is passed explicitly (react-query's optional `queryClient` arg) so a section renders + * standalone — no `QueryClientProvider` needed in the tree (e.g. a per-component story). + */ + +/** `organization.destroy()` — an admin deletes the whole org. */ +export function useDeleteOrganization(organization: OrganizationResource) { + return useMutation( + { + mutationFn: () => organization.destroy(), + }, + getMosaicQueryClient(), + ); +} + +/** `membership.destroy()` — the current user leaves the org. */ +export function useLeaveOrganization(organization: OrganizationResource, membership: MembershipResource) { + const queryClient = getMosaicQueryClient(); + + return useMutation( + { + mutationFn: () => membership.destroy(), + onSuccess: () => { + // Membership changed — drop the cached members collection so it refetches. + void queryClient.invalidateQueries({ queryKey: membersKey(organization.id) }); + }, + }, + queryClient, + ); +} diff --git a/packages/ui/src/mosaic/data/use-organization.ts b/packages/ui/src/mosaic/data/use-organization.ts new file mode 100644 index 00000000000..911734f5642 --- /dev/null +++ b/packages/ui/src/mosaic/data/use-organization.ts @@ -0,0 +1,30 @@ +import { effect } from 'alien-signals'; +import { useSyncExternalStore } from 'react'; + +import { type ActiveOrganization, ensureOrganization, organizationSignal } from './organization-store'; + +/** + * The active organization (and the current user's membership) via a signal — the synchronous + * source of truth. + * + * Suspends (throws the hydration promise) until hydrated, then returns a plain non-null value. + * That single move deletes the `{ isLoaded } | ...` union and the `if (!isLoaded) return ` + * branch from every consumer: a Suspense boundary above owns the loading state instead. + */ +export function useOrganization(): ActiveOrganization { + const active = useSyncExternalStore( + callback => + effect(() => { + organizationSignal(); + callback(); + }), + () => organizationSignal(), + () => null, // server snapshot: unhydrated → boundary renders fallback, matching first client render. + ); + + if (!active) { + throw ensureOrganization(); + } + + return active; +} diff --git a/packages/ui/src/mosaic/panels/organization-profile-general.tsx b/packages/ui/src/mosaic/panels/organization-profile-general.tsx index fc346ff9f75..4deaf76e539 100644 --- a/packages/ui/src/mosaic/panels/organization-profile-general.tsx +++ b/packages/ui/src/mosaic/panels/organization-profile-general.tsx @@ -1,8 +1,22 @@ +import { ClientSuspense } from '../components/client-suspense'; import { Box } from '../components/box'; +import { SectionSkeleton } from '../components/section-skeleton'; import { DeleteOrganization } from '../sections/delete-organization'; import { LeaveOrganization } from '../sections/leave-organization'; import { alpha } from '../utils'; +function Divider() { + return ( + ({ + height: '1px', + background: `light-dark(${alpha('#000', 10)},${alpha('#fff', 10)})`, + marginBlock: t.spacing(4), + })} + /> + ); +} + export function OrganizationProfileGeneral() { return ( - - ({ - height: '1px', - background: `light-dark(${alpha('#000', 10)},${alpha('#fff', 10)})`, - marginBlock: t.spacing(4), - })} - /> - + {/* One boundary owns loading for the whole panel — both sections suspend on the shared + org-hydration promise and resume on the same tick, no per-section `isLoaded` branch. */} + + + + + + } + > + + + + ); } diff --git a/packages/ui/src/mosaic/sections/delete-organization.tsx b/packages/ui/src/mosaic/sections/delete-organization.tsx index 527f1ab3aa0..c1949486da1 100644 --- a/packages/ui/src/mosaic/sections/delete-organization.tsx +++ b/packages/ui/src/mosaic/sections/delete-organization.tsx @@ -2,25 +2,14 @@ import { useState } from 'react'; import { Box } from '../components/box'; import { Button } from '../components/button'; -import { SectionSkeleton } from '../components/section-skeleton'; import { Destructive } from '../block/destructive'; -import { useOrganization } from '../mock/use-organization'; +import { useOrganization } from '../data/use-organization'; +import { useDeleteOrganization } from '../data/use-org-mutations'; export function DeleteOrganization() { - const { isLoaded, organization } = useOrganization(); + const { organization } = useOrganization(); // signal — non-null inside the Suspense boundary + const del = useDeleteOrganization(organization); // mutation — owns its own pending/error state const [open, setOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - - if (!isLoaded || !organization) { - return ; - } - - const handleDelete = async () => { - setIsDeleting(true); - await organization.destroy(); - setIsDeleting(false); - setOpen(false); - }; return ( del.mutate(undefined, { onSuccess: () => setOpen(false) })} + isDeleting={del.isPending} /> diff --git a/packages/ui/src/mosaic/sections/leave-organization.tsx b/packages/ui/src/mosaic/sections/leave-organization.tsx index a32d66a1919..19b159f3ea7 100644 --- a/packages/ui/src/mosaic/sections/leave-organization.tsx +++ b/packages/ui/src/mosaic/sections/leave-organization.tsx @@ -2,91 +2,78 @@ import { useState } from 'react'; import { Box } from '../components/box'; import { Button } from '../components/button'; -import { SectionSkeleton } from '../components/section-skeleton'; import { Destructive } from '../block/destructive'; -import { useOrganization } from '../mock/use-organization'; +import { useOrganization } from '../data/use-organization'; +import { useLeaveOrganization } from '../data/use-org-mutations'; export function LeaveOrganization() { - const { isLoaded, organization, membership } = useOrganization(); + const { organization, membership } = useOrganization(); // signal — non-null inside the Suspense boundary + const leave = useLeaveOrganization(organization, membership); // mutation — owns its own pending/error state const [open, setOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - - if (!isLoaded || !organization || !membership) { - return ; - } - - const handleLeave = async () => { - setIsDeleting(true); - await membership.destroy(); - setIsDeleting(false); - setOpen(false); - }; return ( - <> + ({ + width: '100%', + containerType: 'inline-size', + })} + > ({ - width: '100%', - containerType: 'inline-size', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + columnGap: t.spacing(10), + rowGap: t.spacing(4), + '@container (min-width: 600px)': { + flexDirection: 'row', + }, })} > - ({ - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - columnGap: t.spacing(10), - rowGap: t.spacing(4), - '@container (min-width: 600px)': { - flexDirection: 'row', - }, - })} - > - -

} - sx={t => ({ - ...t.text('base'), - fontWeight: t.font.semibold, - })} - > - Leave organization - -

} - sx={t => ({ - ...t.text('sm'), - textWrap: 'balance', - marginBlockStart: t.spacing(1), - color: t.color.mutedForeground, - })} - > - You will be removed from the organization and need to be invited back - + +

} + sx={t => ({ + ...t.text('base'), + fontWeight: t.font.semibold, + })} + > + Leave organization + +

} + sx={t => ({ + ...t.text('sm'), + textWrap: 'balance', + marginBlockStart: t.spacing(1), + color: t.color.mutedForeground, + })} + > + You will be removed from the organization and need to be invited back - ( - - )} - open={open} - onOpenChange={setOpen} - title='Leave organization' - description='Are you sure you want to leave this organization? You will lose access to this organization and its applications.' - primaryActionLabel='Leave organization' - resourceName={organization.name} - onDelete={handleLeave} - isDeleting={isDeleting} - /> + ( + + )} + open={open} + onOpenChange={setOpen} + title='Leave organization' + description='Are you sure you want to leave this organization? You will lose access to this organization and its applications.' + resourceName={organization.name} + primaryActionLabel='Leave organization' + onDelete={() => leave.mutate(undefined, { onSuccess: () => setOpen(false) })} + isDeleting={leave.isPending} + /> - + ); } diff --git a/packages/ui/src/mosaic/sections/organization-members.tsx b/packages/ui/src/mosaic/sections/organization-members.tsx new file mode 100644 index 00000000000..575a443560e --- /dev/null +++ b/packages/ui/src/mosaic/sections/organization-members.tsx @@ -0,0 +1,61 @@ +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; + +import { Box } from '../components/box'; +import { Button } from '../components/button'; +import { membersQuery } from '../data/members-query'; +import { getMosaicQueryClient } from '../data/query-client'; +import { useOrganization } from '../data/use-organization'; + +export function OrganizationMembers() { + const { organization } = useOrganization(); // signal: which org's members to load + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useSuspenseInfiniteQuery( + membersQuery(organization.id), + getMosaicQueryClient(), + ); + const members = data.pages.flatMap(page => page.members); + + return ( + ({ + display: 'flex', + flexDirection: 'column', + gap: t.spacing(2), + })} + > + {members.map(member => ( + ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + paddingBlock: t.spacing(2), + })} + > + } + sx={t => t.text('sm')} + > + {member.name} + + } + sx={t => ({ ...t.text('xs'), color: t.color.mutedForeground })} + > + {member.role} + + + ))} + {hasNextPage ? ( + + ) : null} + + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7a67eef5f8..28c793a7fdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1086,6 +1086,9 @@ importers: '@swc/helpers': specifier: catalog:repo version: 0.5.21 + '@tanstack/react-query': + specifier: ^5.100.6 + version: 5.101.0(react@18.3.1) alien-signals: specifier: 2.0.6 version: 2.0.6 @@ -5576,6 +5579,14 @@ packages: '@tanstack/query-core@5.100.6': resolution: {integrity: sha512-Os2CPUr98to98RYm+D4qGqGkiffn7MGSyl2547a4MljVkHE30AMJRqTiyCqBfMwzAx/I91vCkAxp5tHSla6Twg==} + '@tanstack/query-core@5.101.0': + resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} + + '@tanstack/react-query@5.101.0': + resolution: {integrity: sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==} + peerDependencies: + react: 18.3.1 + '@tanstack/react-router@1.157.16': resolution: {integrity: sha512-xwFQa7S7dhBhm3aJYwU79cITEYgAKSrcL6wokaROIvl2JyIeazn8jueWqUPJzFjv+QF6Q8euKRlKUEyb5q2ymg==} engines: {node: '>=12'} @@ -21195,6 +21206,13 @@ snapshots: '@tanstack/query-core@5.100.6': {} + '@tanstack/query-core@5.101.0': {} + + '@tanstack/react-query@5.101.0(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.101.0 + react: 18.3.1 + '@tanstack/react-router@1.157.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/history': 1.154.14