@sanity/sdk-react 0.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/_chunks-es/useLogOut.js +36 -0
  2. package/dist/_chunks-es/useLogOut.js.map +1 -0
  3. package/dist/components.d.ts +235 -0
  4. package/dist/components.js +250 -0
  5. package/dist/components.js.map +1 -0
  6. package/dist/hooks.d.ts +145 -0
  7. package/dist/hooks.js +27 -0
  8. package/dist/hooks.js.map +1 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.js +6 -0
  11. package/dist/index.js.map +1 -0
  12. package/package.json +113 -0
  13. package/src/_exports/components.ts +12 -0
  14. package/src/_exports/hooks.ts +7 -0
  15. package/src/_exports/index.ts +10 -0
  16. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +95 -0
  17. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +42 -0
  18. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +23 -0
  19. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +95 -0
  20. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +42 -0
  21. package/src/components/DocumentListLayout/DocumentListLayout.tsx +15 -0
  22. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +49 -0
  23. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +34 -0
  24. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +30 -0
  25. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +115 -0
  26. package/src/components/Login/LoginLinks.test.tsx +100 -0
  27. package/src/components/Login/LoginLinks.tsx +73 -0
  28. package/src/components/auth/AuthBoundary.test.tsx +103 -0
  29. package/src/components/auth/AuthBoundary.tsx +101 -0
  30. package/src/components/auth/AuthError.test.ts +36 -0
  31. package/src/components/auth/AuthError.ts +27 -0
  32. package/src/components/auth/Login.test.tsx +41 -0
  33. package/src/components/auth/Login.tsx +58 -0
  34. package/src/components/auth/LoginCallback.test.tsx +86 -0
  35. package/src/components/auth/LoginCallback.tsx +41 -0
  36. package/src/components/auth/LoginError.test.tsx +56 -0
  37. package/src/components/auth/LoginError.tsx +54 -0
  38. package/src/components/auth/LoginFooter.test.tsx +29 -0
  39. package/src/components/auth/LoginFooter.tsx +67 -0
  40. package/src/components/auth/LoginLayout.test.tsx +33 -0
  41. package/src/components/auth/LoginLayout.tsx +99 -0
  42. package/src/components/context/SanityProvider.test.tsx +25 -0
  43. package/src/components/context/SanityProvider.tsx +42 -0
  44. package/src/hooks/Documents/.keep +0 -0
  45. package/src/hooks/auth/useAuthState.test.tsx +106 -0
  46. package/src/hooks/auth/useAuthState.tsx +33 -0
  47. package/src/hooks/auth/useAuthToken.test.tsx +94 -0
  48. package/src/hooks/auth/useAuthToken.tsx +16 -0
  49. package/src/hooks/auth/useCurrentUser.test.tsx +50 -0
  50. package/src/hooks/auth/useCurrentUser.tsx +27 -0
  51. package/src/hooks/auth/useHandleCallback.test.tsx +25 -0
  52. package/src/hooks/auth/useHandleCallback.tsx +50 -0
  53. package/src/hooks/auth/useLogOut.test.tsx +67 -0
  54. package/src/hooks/auth/useLogOut.tsx +15 -0
  55. package/src/hooks/auth/useLoginUrls.test.tsx +61 -0
  56. package/src/hooks/auth/useLoginUrls.tsx +51 -0
  57. package/src/hooks/client/useClient.test.tsx +130 -0
  58. package/src/hooks/client/useClient.ts +56 -0
  59. package/src/hooks/context/useSanityInstance.test.tsx +31 -0
  60. package/src/hooks/context/useSanityInstance.ts +23 -0
@@ -0,0 +1,41 @@
1
+ import {createSanityInstance} from '@sanity/sdk'
2
+ import {ThemeProvider} from '@sanity/ui'
3
+ import {buildTheme} from '@sanity/ui/theme'
4
+ import {render, screen} from '@testing-library/react'
5
+ import React from 'react'
6
+ import {describe, expect, it, vi} from 'vitest'
7
+
8
+ import {SanityProvider} from '../context/SanityProvider'
9
+ import {Login} from './Login'
10
+
11
+ vi.mock('../../hooks/auth/useLoginUrls', () => ({
12
+ useLoginUrls: vi.fn(() => [
13
+ {title: 'Provider A', url: 'https://provider-a.com/auth'},
14
+ {title: 'Provider B', url: 'https://provider-b.com/auth'},
15
+ ]),
16
+ }))
17
+
18
+ const theme = buildTheme({})
19
+ const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
20
+ const renderWithWrappers = (ui: React.ReactElement) => {
21
+ return render(
22
+ <ThemeProvider theme={theme}>
23
+ <SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
24
+ </ThemeProvider>,
25
+ )
26
+ }
27
+
28
+ describe('Login', () => {
29
+ it('renders login providers', () => {
30
+ renderWithWrappers(<Login />)
31
+ expect(screen.getByText('Choose login provider')).toBeInTheDocument()
32
+ expect(screen.getByRole('link', {name: 'Provider A'})).toHaveAttribute(
33
+ 'href',
34
+ 'https://provider-a.com/auth',
35
+ )
36
+ expect(screen.getByRole('link', {name: 'Provider B'})).toHaveAttribute(
37
+ 'href',
38
+ 'https://provider-b.com/auth',
39
+ )
40
+ })
41
+ })
@@ -0,0 +1,58 @@
1
+ import {Button, Flex, Heading, Spinner} from '@sanity/ui'
2
+ import {Suspense} from 'react'
3
+ import styled from 'styled-components'
4
+
5
+ import {useLoginUrls} from '../../hooks/auth/useLoginUrls'
6
+ import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
7
+
8
+ /**
9
+ * @alpha
10
+ */
11
+ export interface LoginProps {
12
+ header?: React.ReactNode
13
+ footer?: React.ReactNode
14
+ }
15
+
16
+ const FallbackRoot = styled(Flex)`
17
+ height: 123px;
18
+ `
19
+
20
+ /**
21
+ * Login component that displays available authentication providers.
22
+ * Renders a list of login options with a loading fallback while providers load.
23
+ *
24
+ * @alpha
25
+ */
26
+ export function Login({header, footer}: LoginLayoutProps): JSX.Element {
27
+ return (
28
+ <LoginLayout header={header} footer={footer}>
29
+ <Flex direction="column" gap={4}>
30
+ <Heading as="h1" size={1} align="center">
31
+ Choose login provider
32
+ </Heading>
33
+
34
+ <Suspense
35
+ fallback={
36
+ <FallbackRoot align="center" justify="center">
37
+ <Spinner />
38
+ </FallbackRoot>
39
+ }
40
+ >
41
+ <Providers />
42
+ </Suspense>
43
+ </Flex>
44
+ </LoginLayout>
45
+ )
46
+ }
47
+
48
+ function Providers() {
49
+ const loginUrls = useLoginUrls()
50
+
51
+ return (
52
+ <Flex direction="column" gap={3}>
53
+ {loginUrls.map(({title, url}) => (
54
+ <Button key={url} text={title} as="a" href={url} mode="ghost" />
55
+ ))}
56
+ </Flex>
57
+ )
58
+ }
@@ -0,0 +1,86 @@
1
+ import {createSanityInstance} from '@sanity/sdk'
2
+ import {ThemeProvider} from '@sanity/ui'
3
+ import {buildTheme} from '@sanity/ui/theme'
4
+ import {render, screen, waitFor} from '@testing-library/react'
5
+ import React from 'react'
6
+ import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'
7
+
8
+ import {SanityProvider} from '../context/SanityProvider'
9
+
10
+ const theme = buildTheme({})
11
+ const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
12
+
13
+ const renderWithWrappers = (ui: React.ReactElement) => {
14
+ return render(
15
+ <ThemeProvider theme={theme}>
16
+ <SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
17
+ </ThemeProvider>,
18
+ )
19
+ }
20
+
21
+ // Mock `useHandleCallback`
22
+ vi.mock('../../hooks/auth/useHandleCallback', () => ({
23
+ useHandleCallback: vi.fn(() => async (url: string) => {
24
+ const parsedUrl = new URL(url)
25
+ const sid = new URLSearchParams(parsedUrl.hash.slice(1)).get('sid')
26
+ if (sid === 'valid') {
27
+ return 'https://example.com/new-location'
28
+ }
29
+ return false
30
+ }),
31
+ }))
32
+
33
+ describe('LoginCallback', () => {
34
+ beforeAll(() => {
35
+ // Stub `window.history` and `location`
36
+ vi.stubGlobal('history', {
37
+ replaceState: vi.fn(),
38
+ })
39
+ vi.stubGlobal('location', {
40
+ href: 'http://localhost',
41
+ })
42
+ })
43
+
44
+ afterAll(() => {
45
+ vi.unstubAllGlobals()
46
+ })
47
+
48
+ beforeEach(() => {
49
+ vi.clearAllMocks()
50
+ vi.resetModules()
51
+ })
52
+
53
+ it('renders a loading message', async () => {
54
+ const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
55
+ renderWithWrappers(<LoginCallback />)
56
+ expect(screen.getByText('Logging you in…')).toBeInTheDocument()
57
+ })
58
+
59
+ it('handles a successful callback and calls history.replaceState', async () => {
60
+ // Simulate a valid `sid` in the location hash
61
+ vi.stubGlobal('location', {href: 'http://localhost#sid=valid'})
62
+ const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
63
+
64
+ renderWithWrappers(<LoginCallback />)
65
+
66
+ await waitFor(() => {
67
+ expect(history.replaceState).toHaveBeenCalledWith(
68
+ null,
69
+ '',
70
+ 'https://example.com/new-location',
71
+ )
72
+ })
73
+ })
74
+
75
+ it('does not call history.replaceState on an unsuccessful callback', async () => {
76
+ // Simulate an invalid `sid` in the location hash
77
+ vi.stubGlobal('location', {href: 'http://localhost#sid=invalid'})
78
+ const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
79
+
80
+ renderWithWrappers(<LoginCallback />)
81
+
82
+ await waitFor(() => {
83
+ expect(history.replaceState).not.toHaveBeenCalled()
84
+ })
85
+ })
86
+ })
@@ -0,0 +1,41 @@
1
+ import {Flex, Spinner, Text} from '@sanity/ui'
2
+ import {useEffect} from 'react'
3
+ import styled from 'styled-components'
4
+
5
+ import {useHandleCallback} from '../../hooks/auth/useHandleCallback'
6
+ import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
7
+
8
+ const StyledFlex = styled(Flex)`
9
+ margin: auto;
10
+ `
11
+
12
+ /**
13
+ * Component shown during auth callback processing that handles login completion.
14
+ * Automatically processes the auth callback when mounted and updates the URL
15
+ * to remove callback parameters without triggering a page reload.
16
+ *
17
+ * @alpha
18
+ */
19
+ export function LoginCallback({header, footer}: LoginLayoutProps): React.ReactNode {
20
+ const handleCallback = useHandleCallback()
21
+
22
+ useEffect(() => {
23
+ const url = new URL(location.href)
24
+ handleCallback(url.toString()).then((replacementLocation) => {
25
+ if (replacementLocation) {
26
+ // history API with `replaceState` is used to prevent a reload but still
27
+ // remove the short-lived token from the URL
28
+ history.replaceState(null, '', replacementLocation)
29
+ }
30
+ })
31
+ }, [handleCallback])
32
+
33
+ return (
34
+ <LoginLayout header={header} footer={footer}>
35
+ <StyledFlex direction="column" justify="center" align="center" gap={4}>
36
+ <Text size={1}>Logging you in…</Text>
37
+ <Spinner size={4} />
38
+ </StyledFlex>
39
+ </LoginLayout>
40
+ )
41
+ }
@@ -0,0 +1,56 @@
1
+ import {createSanityInstance} from '@sanity/sdk'
2
+ import {ThemeProvider} from '@sanity/ui'
3
+ import {buildTheme} from '@sanity/ui/theme'
4
+ import {fireEvent, render, screen, waitFor} from '@testing-library/react'
5
+ import React from 'react'
6
+ import {describe, expect, it, vi} from 'vitest'
7
+
8
+ import {SanityProvider} from '../context/SanityProvider'
9
+ import {AuthError} from './AuthError'
10
+ import {LoginError} from './LoginError'
11
+
12
+ vi.mock('../../hooks/auth/useLogOut', () => ({
13
+ useLogOut: vi.fn(() => async () => {}),
14
+ }))
15
+
16
+ const theme = buildTheme({})
17
+ const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
18
+
19
+ const renderWithWrappers = (ui: React.ReactElement) => {
20
+ return render(
21
+ <ThemeProvider theme={theme}>
22
+ <SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
23
+ </ThemeProvider>,
24
+ )
25
+ }
26
+
27
+ describe('LoginError', () => {
28
+ it('shows authentication error and retry button', async () => {
29
+ const mockReset = vi.fn()
30
+ const error = new AuthError(new Error('Test error'))
31
+
32
+ renderWithWrappers(<LoginError error={error} resetErrorBoundary={mockReset} />)
33
+
34
+ expect(screen.getByText('Authentication Error')).toBeInTheDocument()
35
+ const retryButton = screen.getByRole('button', {name: 'Retry'})
36
+ fireEvent.click(retryButton)
37
+
38
+ await waitFor(() => {
39
+ expect(mockReset).toHaveBeenCalled()
40
+ })
41
+ })
42
+
43
+ it('throws an error if the error is not an instance of AuthError', () => {
44
+ const mockReset = vi.fn()
45
+ const nonAuthError = new Error('Non-auth error')
46
+
47
+ // Suppress console.error during this test
48
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
49
+
50
+ expect(() => {
51
+ renderWithWrappers(<LoginError error={nonAuthError} resetErrorBoundary={mockReset} />)
52
+ }).toThrow('Non-auth error')
53
+
54
+ consoleErrorSpy.mockRestore() // Restore original console.error behavior
55
+ })
56
+ })
@@ -0,0 +1,54 @@
1
+ import {Button, Flex, Text} from '@sanity/ui'
2
+ import {useCallback} from 'react'
3
+ import {type FallbackProps} from 'react-error-boundary'
4
+ import styled from 'styled-components'
5
+
6
+ import {useLogOut} from '../../hooks/auth/useLogOut'
7
+ import {AuthError} from './AuthError'
8
+ import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
9
+
10
+ /**
11
+ * @alpha
12
+ */
13
+ export type LoginErrorProps = FallbackProps & LoginLayoutProps
14
+
15
+ const StyledFlex = styled(Flex)`
16
+ margin: auto;
17
+ `
18
+
19
+ /**
20
+ * Displays authentication error details and provides retry functionality.
21
+ * Only handles {@link AuthError} instances - rethrows other error types.
22
+ *
23
+ * @alpha
24
+ */
25
+ export function LoginError({
26
+ error,
27
+ resetErrorBoundary,
28
+ header,
29
+ footer,
30
+ }: LoginErrorProps): React.ReactNode {
31
+ if (!(error instanceof AuthError)) throw error
32
+ const logout = useLogOut()
33
+
34
+ const handleRetry = useCallback(async () => {
35
+ await logout()
36
+ resetErrorBoundary()
37
+ }, [logout, resetErrorBoundary])
38
+
39
+ return (
40
+ <LoginLayout header={header} footer={footer}>
41
+ <StyledFlex direction="column" gap={4}>
42
+ <Flex direction="column" gap={3}>
43
+ <Text as="h2" align="center" weight="bold" size={3}>
44
+ Authentication Error
45
+ </Text>
46
+ <Text size={1} align="center">
47
+ Please try again or contact support if the problem persists.
48
+ </Text>
49
+ </Flex>
50
+ <Button text="Retry" tone="primary" onClick={handleRetry} />
51
+ </StyledFlex>
52
+ </LoginLayout>
53
+ )
54
+ }
@@ -0,0 +1,29 @@
1
+ import {createSanityInstance} from '@sanity/sdk'
2
+ import {ThemeProvider} from '@sanity/ui'
3
+ import {buildTheme} from '@sanity/ui/theme'
4
+ import {render, screen} from '@testing-library/react'
5
+ import React from 'react'
6
+ import {describe, expect, it} from 'vitest'
7
+
8
+ import {SanityProvider} from '../context/SanityProvider'
9
+ import {LoginFooter} from './LoginFooter'
10
+
11
+ const theme = buildTheme({})
12
+ const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
13
+ const renderWithWrappers = (ui: React.ReactElement) => {
14
+ return render(
15
+ <ThemeProvider theme={theme}>
16
+ <SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
17
+ </ThemeProvider>,
18
+ )
19
+ }
20
+
21
+ describe('LoginFooter', () => {
22
+ it('renders footer links', () => {
23
+ renderWithWrappers(<LoginFooter />)
24
+ expect(screen.getByText('Community')).toBeInTheDocument()
25
+ expect(screen.getByText('Docs')).toBeInTheDocument()
26
+ expect(screen.getByText('Privacy')).toBeInTheDocument()
27
+ expect(screen.getByText('sanity.io')).toBeInTheDocument()
28
+ })
29
+ })
@@ -0,0 +1,67 @@
1
+ import {SanityLogo} from '@sanity/logos'
2
+ import {Flex, Text} from '@sanity/ui'
3
+ import {Fragment} from 'react'
4
+ import styled from 'styled-components'
5
+
6
+ const LINKS = [
7
+ {
8
+ url: 'https://slack.sanity.io/',
9
+ i18nKey: 'workspaces.community-title',
10
+ title: 'Community',
11
+ },
12
+ {
13
+ url: 'https://www.sanity.io/docs',
14
+ i18nKey: 'workspaces.docs-title',
15
+ title: 'Docs',
16
+ },
17
+ {
18
+ url: 'https://www.sanity.io/legal/privacy',
19
+ i18nKey: 'workspaces.privacy-title',
20
+ title: 'Privacy',
21
+ },
22
+ {
23
+ url: 'https://www.sanity.io',
24
+ i18nKey: 'workspaces.sanity-io-title',
25
+ title: 'sanity.io',
26
+ },
27
+ ]
28
+
29
+ const StyledText = styled(Text)`
30
+ a {
31
+ color: inherit;
32
+ }
33
+ `
34
+
35
+ /**
36
+ * Default footer component for login screens showing Sanity branding and legal
37
+ * links.
38
+ *
39
+ * @alpha
40
+ */
41
+ export function LoginFooter(): React.ReactNode {
42
+ return (
43
+ <Flex direction="column" gap={4} justify="center" align="center" paddingTop={2}>
44
+ <Text size={3}>
45
+ <SanityLogo />
46
+ </Text>
47
+
48
+ <Flex align="center" gap={2}>
49
+ {LINKS.map((link, index) => (
50
+ <Fragment key={link.title}>
51
+ <StyledText muted size={1}>
52
+ <a href={link.url} target="_blank" rel="noopener noreferrer">
53
+ {link.title}
54
+ </a>
55
+ </StyledText>
56
+
57
+ {index < LINKS.length - 1 && (
58
+ <Text size={1} muted>
59
+
60
+ </Text>
61
+ )}
62
+ </Fragment>
63
+ ))}
64
+ </Flex>
65
+ </Flex>
66
+ )
67
+ }
@@ -0,0 +1,33 @@
1
+ import {createSanityInstance} from '@sanity/sdk'
2
+ import {ThemeProvider} from '@sanity/ui'
3
+ import {buildTheme} from '@sanity/ui/theme'
4
+ import {render, screen} from '@testing-library/react'
5
+ import React from 'react'
6
+ import {describe, expect, it} from 'vitest'
7
+
8
+ import {SanityProvider} from '../context/SanityProvider'
9
+ import {LoginLayout} from './LoginLayout'
10
+
11
+ const theme = buildTheme({})
12
+ const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
13
+ const renderWithWrappers = (ui: React.ReactElement) => {
14
+ return render(
15
+ <ThemeProvider theme={theme}>
16
+ <SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
17
+ </ThemeProvider>,
18
+ )
19
+ }
20
+
21
+ describe('LoginLayout', () => {
22
+ it('renders header, children, and footer', () => {
23
+ renderWithWrappers(
24
+ <LoginLayout header={<div>Header Content</div>} footer={<div>Footer Content</div>}>
25
+ <div>Main Content</div>
26
+ </LoginLayout>,
27
+ )
28
+
29
+ expect(screen.getByText('Header Content')).toBeInTheDocument()
30
+ expect(screen.getByText('Main Content')).toBeInTheDocument()
31
+ expect(screen.getByText('Footer Content')).toBeInTheDocument()
32
+ })
33
+ })
@@ -0,0 +1,99 @@
1
+ import {Card, Flex} from '@sanity/ui'
2
+ import styled from 'styled-components'
3
+
4
+ import {LoginFooter} from './LoginFooter'
5
+
6
+ /**
7
+ * @alpha
8
+ */
9
+ export interface LoginLayoutProps {
10
+ /** Optional header content rendered at top of card */
11
+ header?: React.ReactNode
12
+
13
+ /** Optional footer content rendered below card. Defaults to an internal login footer */
14
+ footer?: React.ReactNode
15
+
16
+ /** Main content rendered in card body */
17
+ children?: React.ReactNode
18
+ }
19
+
20
+ const Root = styled.div`
21
+ width: 100%;
22
+ display: flex;
23
+ `
24
+
25
+ const Container = styled(Flex)`
26
+ width: 320px;
27
+ margin: auto;
28
+ display: flex;
29
+ `
30
+
31
+ const StyledCard = styled(Card)``
32
+
33
+ const ChildrenFlex = styled(Flex)`
34
+ min-height: 154px;
35
+ `
36
+
37
+ /**
38
+ * Layout component for login-related screens providing consistent styling and structure.
39
+ * Renders content in a centered card with optional header and footer sections.
40
+ *
41
+ * Can be used to build custom login screens for the AuthBoundary component, including:
42
+ * - Login provider selection (LoginComponent)
43
+ * - OAuth callback handling (CallbackComponent)
44
+ * - Error states (LoginErrorComponent)
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * // Custom login screen using the layout
49
+ * function CustomLogin({header, footer}: LoginLayoutProps) {
50
+ * return (
51
+ * <LoginLayout
52
+ * header={header}
53
+ * footer={footer}
54
+ * >
55
+ * <CustomLoginContent />
56
+ * </LoginLayout>
57
+ * )
58
+ * }
59
+ *
60
+ * // Use with AuthBoundary
61
+ * <AuthBoundary
62
+ * LoginComponent={CustomLogin}
63
+ * header={<Logo />}
64
+ * >
65
+ * <ProtectedContent />
66
+ * </AuthBoundary>
67
+ * ```
68
+ *
69
+ * @alpha
70
+ */
71
+ export function LoginLayout({
72
+ children,
73
+ footer = <LoginFooter />,
74
+ header,
75
+ }: LoginLayoutProps): React.ReactNode {
76
+ return (
77
+ <Root>
78
+ <Container direction="column" gap={4}>
79
+ <StyledCard border radius={2} paddingY={4}>
80
+ <Flex direction="column" gap={4}>
81
+ {header && (
82
+ <Card borderBottom paddingX={4} paddingBottom={3}>
83
+ {header}
84
+ </Card>
85
+ )}
86
+
87
+ {children && (
88
+ <ChildrenFlex paddingX={4} direction="column">
89
+ {children}
90
+ </ChildrenFlex>
91
+ )}
92
+ </Flex>
93
+ </StyledCard>
94
+
95
+ {footer}
96
+ </Container>
97
+ </Root>
98
+ )
99
+ }
@@ -0,0 +1,25 @@
1
+ import {createSanityInstance} from '@sanity/sdk'
2
+ import {render} from '@testing-library/react'
3
+ import {describe, expect, it} from 'vitest'
4
+
5
+ import {useSanityInstance} from '../../hooks/context/useSanityInstance'
6
+ import {SanityProvider} from './SanityProvider'
7
+
8
+ describe('SanityProvider', () => {
9
+ const sanityInstance = createSanityInstance({projectId: 'test-project', dataset: 'production'})
10
+
11
+ it('provides instance to nested components', () => {
12
+ const TestComponent = () => {
13
+ const instance = useSanityInstance()
14
+ return <div data-testid="test">{instance.identity.projectId}</div>
15
+ }
16
+
17
+ const {getByTestId} = render(
18
+ <SanityProvider sanityInstance={sanityInstance}>
19
+ <TestComponent />
20
+ </SanityProvider>,
21
+ )
22
+
23
+ expect(getByTestId('test')).toHaveTextContent('test-project')
24
+ })
25
+ })
@@ -0,0 +1,42 @@
1
+ import {type SanityInstance} from '@sanity/sdk'
2
+ import {createContext, type ReactElement} from 'react'
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export interface SanityProviderProps {
8
+ children: React.ReactNode
9
+ sanityInstance: SanityInstance
10
+ }
11
+
12
+ export const SanityInstanceContext = createContext<SanityInstance | null>(null)
13
+
14
+ /**
15
+ * Top-level context provider that provides a Sanity configuration instance.
16
+ * This must wrap any Sanity SDK React component.
17
+ * @public
18
+ * @param props - Sanity project and dataset configuration
19
+ * @returns Rendered component
20
+ * @example
21
+ * ```tsx
22
+ * import {createSanityInstance} from '@sanity/sdk'
23
+ * import {ExampleComponent, SanityProvider} from '@sanity/sdk-react'
24
+ *
25
+ * const sanityInstance = createSanityInstance({projectId: 'your-project-id', dataset: 'production'})
26
+ *
27
+ * export default function MyApp() {
28
+ * return (
29
+ * <SanityProvider sanityInstance={sanityInstance}>
30
+ * <ExampleComponent />
31
+ * </SanityProvider>
32
+ * )
33
+ * }
34
+ * ```
35
+ */
36
+ export const SanityProvider = ({children, sanityInstance}: SanityProviderProps): ReactElement => {
37
+ return (
38
+ <SanityInstanceContext.Provider value={sanityInstance}>
39
+ {children}
40
+ </SanityInstanceContext.Provider>
41
+ )
42
+ }
File without changes