@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,145 @@
1
+ import {AuthProvider} from '@sanity/sdk'
2
+ import {AuthState} from '@sanity/sdk'
3
+ import {AuthStore} from '@sanity/sdk'
4
+ import {CurrentUser} from '@sanity/sdk'
5
+ import type {SanityInstance} from '@sanity/sdk'
6
+
7
+ /**
8
+ * A React hook that subscribes to authentication state changes.
9
+ *
10
+ * This hook provides access to the current authentication state type from the Sanity auth store.
11
+ * It automatically re-renders the component when the authentication state changes.
12
+ *
13
+ * @remarks
14
+ * The hook uses `useSyncExternalStore` to safely subscribe to auth state changes
15
+ * and ensure consistency between server and client rendering.
16
+ *
17
+ * @returns The current authentication state type
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * function AuthStatus() {
22
+ * const authState = useAuthState()
23
+ * return <div>Current auth state: {authState}</div>
24
+ * }
25
+ * ```
26
+ *
27
+ * @public
28
+ */
29
+ export declare function useAuthState(): AuthState
30
+
31
+ /**
32
+ * Hook to get the currently logged in user
33
+ * @public
34
+ * @returns The current user or null if not authenticated
35
+ */
36
+ export declare const useAuthToken: () => string | null
37
+
38
+ /**
39
+ * Hook to get the currently logged in user
40
+ * @public
41
+ * @returns The current user or null if not authenticated
42
+ */
43
+ export declare const useCurrentUser: () => CurrentUser | null
44
+
45
+ /**
46
+ * A React hook that returns a function for handling authentication callbacks.
47
+ *
48
+ * @remarks
49
+ * This hook provides access to the authentication store's callback handler,
50
+ * which processes auth redirects by extracting the session ID and fetching the
51
+ * authentication token. If fetching the long-lived token is successful,
52
+ * `handleCallback` will return a Promise that resolves a new location that
53
+ * removes the short-lived token from the URL. Use this in combination with
54
+ * `history.replaceState` or your own router's `replace` function to update the
55
+ * current location without triggering a reload.
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * function AuthCallback() {
60
+ * const handleCallback = useHandleCallback()
61
+ * const router = useRouter() // Example router
62
+ *
63
+ * useEffect(() => {
64
+ * async function processCallback() {
65
+ * // Handle the callback and get the cleaned URL
66
+ * const newUrl = await handleCallback(window.location.href)
67
+ *
68
+ * if (newUrl) {
69
+ * // Replace URL without triggering navigation
70
+ * router.replace(newUrl, {shallow: true})
71
+ * }
72
+ * }
73
+ *
74
+ * processCallback().catch(console.error)
75
+ * }, [handleCallback, router])
76
+ *
77
+ * return <div>Completing login...</div>
78
+ * }
79
+ * ```
80
+ *
81
+ * @returns A callback handler function that processes OAuth redirects
82
+ * @public
83
+ */
84
+ export declare function useHandleCallback(): AuthStore['handleCallback']
85
+
86
+ /**
87
+ * A React hook that retrieves the available authentication provider URLs for login.
88
+ *
89
+ * @remarks
90
+ * This hook fetches the login URLs from the Sanity auth store when the component mounts.
91
+ * Each provider object contains information about an authentication method, including its URL.
92
+ * The hook will suspend if the login URLs have not yet loaded.
93
+ *
94
+ * @example
95
+ * ```tsx
96
+ * // LoginProviders component that uses the hook
97
+ * function LoginProviders() {
98
+ * const providers = useLoginUrls()
99
+ *
100
+ * return (
101
+ * <div>
102
+ * {providers.map((provider) => (
103
+ * <a key={provider.name} href={provider.url}>
104
+ * Login with {provider.title}
105
+ * </a>
106
+ * ))}
107
+ * </div>
108
+ * )
109
+ * }
110
+ *
111
+ * // Parent component with Suspense boundary
112
+ * function LoginPage() {
113
+ * return (
114
+ * <Suspense fallback={<div>Loading authentication providers...</div>}>
115
+ * <LoginProviders />
116
+ * </Suspense>
117
+ * )
118
+ * }
119
+ * ```
120
+ *
121
+ * @returns An array of {@link AuthProvider} objects containing login URLs and provider information
122
+ * @public
123
+ */
124
+ export declare function useLoginUrls(): AuthProvider[]
125
+
126
+ /**
127
+ * Hook to log out of the current session
128
+ * @public
129
+ * @returns A function to log out of the current session
130
+ */
131
+ export declare const useLogOut: () => AuthStore['logout']
132
+
133
+ /**
134
+ * Hook that provides the current Sanity instance from the context.
135
+ * This must be called from within a `SanityProvider` component.
136
+ * @public
137
+ * @returns the current Sanity instance
138
+ * @example
139
+ * ```tsx
140
+ * const instance = useSanityInstance()
141
+ * ```
142
+ */
143
+ export declare const useSanityInstance: () => SanityInstance
144
+
145
+ export {}
package/dist/hooks.js ADDED
@@ -0,0 +1,27 @@
1
+ import { useSanityInstance } from "./_chunks-es/useLogOut.js";
2
+ import { useAuthState, useHandleCallback, useLogOut, useLoginUrls } from "./_chunks-es/useLogOut.js";
3
+ import { getAuthStore } from "@sanity/sdk";
4
+ import { useStore } from "zustand";
5
+ const useAuthToken = () => {
6
+ const instance = useSanityInstance(), { tokenState } = getAuthStore(instance);
7
+ return useStore(tokenState);
8
+ }, useCurrentUser = () => {
9
+ const instance = useSanityInstance(), { currentUserState } = getAuthStore(instance);
10
+ if (!currentUserState.getState())
11
+ throw new Promise((resolve) => {
12
+ const unsubscribe = currentUserState.subscribe((currentUser) => {
13
+ currentUser && (unsubscribe(), resolve());
14
+ });
15
+ });
16
+ return useStore(currentUserState);
17
+ };
18
+ export {
19
+ useAuthState,
20
+ useAuthToken,
21
+ useCurrentUser,
22
+ useHandleCallback,
23
+ useLogOut,
24
+ useLoginUrls,
25
+ useSanityInstance
26
+ };
27
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sources":["../src/hooks/auth/useAuthToken.tsx","../src/hooks/auth/useCurrentUser.tsx"],"sourcesContent":["import {getAuthStore} from '@sanity/sdk'\nimport {useStore} from 'zustand'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\n/**\n * Hook to get the currently logged in user\n * @public\n * @returns The current user or null if not authenticated\n */\nexport const useAuthToken = (): string | null => {\n const instance = useSanityInstance()\n const {tokenState} = getAuthStore(instance)\n\n return useStore(tokenState)\n}\n","import {type CurrentUser, type CurrentUserSlice, getAuthStore} from '@sanity/sdk'\nimport {useStore} from 'zustand'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\n/**\n * Hook to get the currently logged in user\n * @public\n * @returns The current user or null if not authenticated\n */\nexport const useCurrentUser = (): CurrentUser | null => {\n const instance = useSanityInstance()\n const {currentUserState} = getAuthStore(instance)\n\n // TODO: update this hook so it can never return null\n if (!currentUserState.getState())\n throw new Promise<void>((resolve) => {\n const unsubscribe = currentUserState.subscribe((currentUser) => {\n if (currentUser) {\n unsubscribe()\n resolve()\n }\n })\n })\n\n return useStore<CurrentUserSlice>(currentUserState)\n}\n"],"names":[],"mappings":";;;;AAUO,MAAM,eAAe,MAAqB;AAC/C,QAAM,WAAW,kBAAkB,GAC7B,EAAC,WAAU,IAAI,aAAa,QAAQ;AAE1C,SAAO,SAAS,UAAU;AAC5B,GCLa,iBAAiB,MAA0B;AACtD,QAAM,WAAW,kBAAkB,GAC7B,EAAC,iBAAgB,IAAI,aAAa,QAAQ;AAG5C,MAAA,CAAC,iBAAiB,SAAS;AACvB,UAAA,IAAI,QAAc,CAAC,YAAY;AACnC,YAAM,cAAc,iBAAiB,UAAU,CAAC,gBAAgB;AAC1D,wBACF,eACA;MAAQ,CAEX;AAAA,IAAA,CACF;AAEH,SAAO,SAA2B,gBAAgB;AACpD;"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * An example function that will be removed.
3
+ * @public
4
+ */
5
+ export declare function main(): void
6
+
7
+ export {}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ function main() {
2
+ }
3
+ export {
4
+ main
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/_exports/index.ts"],"sourcesContent":["/**\n * An example function that will be removed.\n * @public\n */\nexport function main(): void {\n //\n}\n\n// export * from './components'\n// export * from './hooks'\n"],"names":[],"mappings":"AAIO,SAAS,OAAa;AAE7B;"}
package/package.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "name": "@sanity/sdk-react",
3
+ "version": "0.0.0-alpha.1",
4
+ "private": false,
5
+ "description": "Sanity SDK React toolkit for Content OS",
6
+ "keywords": [],
7
+ "homepage": "https://github.com/sanity-io/sdk#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/sanity-io/sdk/issues"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+ssh://git@github.com/sanity-io/sdk.git"
14
+ },
15
+ "license": "MIT",
16
+ "author": "Sanity <developers@sanity.io>",
17
+ "sideEffects": false,
18
+ "type": "module",
19
+ "exports": {
20
+ ".": {
21
+ "source": "./src/_exports/index.ts",
22
+ "import": "./dist/index.js",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./components": {
26
+ "source": "./src/_exports/components.ts",
27
+ "import": "./dist/components.js",
28
+ "default": "./dist/components.js"
29
+ },
30
+ "./hooks": {
31
+ "source": "./src/_exports/hooks.ts",
32
+ "import": "./dist/hooks.js",
33
+ "default": "./dist/hooks.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "main": "./dist/index.js",
38
+ "module": "./dist/index.js",
39
+ "types": "./dist/index.d.ts",
40
+ "typesVersions": {
41
+ "*": {
42
+ "components": [
43
+ "./dist/components.d.ts"
44
+ ],
45
+ "hooks": [
46
+ "./dist/hooks.d.ts"
47
+ ]
48
+ }
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "src"
53
+ ],
54
+ "lint-staged": {
55
+ "*": [
56
+ "prettier --write --cache --ignore-unknown"
57
+ ]
58
+ },
59
+ "browserslist": "extends @sanity/browserslist-config",
60
+ "prettier": "@sanity/prettier-config",
61
+ "dependencies": {
62
+ "@sanity/logos": "^2.1.13",
63
+ "@sanity/ui": "^2.8.19",
64
+ "react-error-boundary": "^4.1.2",
65
+ "rxjs": "^7.8.1",
66
+ "styled-components": "^6.1.13",
67
+ "zustand": "^5.0.1",
68
+ "@sanity/sdk": "0.0.0-alpha.1"
69
+ },
70
+ "devDependencies": {
71
+ "@sanity/client": "^6.24.1",
72
+ "@sanity/pkg-utils": "^6.11.14",
73
+ "@sanity/prettier-config": "^1.0.3",
74
+ "@storybook/react": "^8.4.6",
75
+ "@testing-library/jest-dom": "^6.6.3",
76
+ "@testing-library/react": "^16.0.1",
77
+ "@types/react": "^18.3.13",
78
+ "@types/react-dom": "^18.3.1",
79
+ "@vitejs/plugin-react": "^4.3.4",
80
+ "@vitest/coverage-v8": "2.1.8",
81
+ "eslint": "^9.16.0",
82
+ "jsdom": "^25.0.1",
83
+ "lint-staged": "^15.2.10",
84
+ "prettier": "^3.4.2",
85
+ "react": "^18.3.1",
86
+ "react-dom": "^18.3.1",
87
+ "typescript": "^5.7.2",
88
+ "vitest": "^2.1.8",
89
+ "@repo/config-eslint": "0.0.0",
90
+ "@repo/package.config": "0.0.1"
91
+ },
92
+ "peerDependencies": {
93
+ "react": "^18.0.0",
94
+ "react-dom": "^18.0.0"
95
+ },
96
+ "engines": {
97
+ "node": ">=20.0.0"
98
+ },
99
+ "publishConfig": {
100
+ "access": "restricted"
101
+ },
102
+ "scripts": {
103
+ "build": "pkg build --strict --clean --check",
104
+ "clean": "rimraf dist",
105
+ "dev": "pkg watch",
106
+ "format": "prettier --write --cache --ignore-unknown .",
107
+ "lint": "eslint .",
108
+ "test": "vitest run",
109
+ "test:coverage": "vitest run --coverage",
110
+ "test:watch": "vitest",
111
+ "ts:check": "tsc --noEmit"
112
+ }
113
+ }
@@ -0,0 +1,12 @@
1
+ export {AuthBoundary, type AuthBoundaryProps} from '../components/auth/AuthBoundary'
2
+ export {Login} from '../components/auth/Login'
3
+ export {LoginCallback} from '../components/auth/LoginCallback'
4
+ export {LoginError, type LoginErrorProps} from '../components/auth/LoginError'
5
+ export {LoginLayout, type LoginLayoutProps} from '../components/auth/LoginLayout'
6
+ export type {SanityProviderProps} from '../components/context/SanityProvider'
7
+ export {SanityProvider} from '../components/context/SanityProvider'
8
+ export {DocumentGridLayout} from '../components/DocumentGridLayout/DocumentGridLayout'
9
+ export {DocumentListLayout} from '../components/DocumentListLayout/DocumentListLayout'
10
+ export {DocumentPreviewLayout} from '../components/DocumentPreviewLayout/DocumentPreviewLayout'
11
+ export {type DocumentPreviewLayoutProps} from '../components/DocumentPreviewLayout/DocumentPreviewLayout'
12
+ export {LoginLinks} from '../components/Login/LoginLinks'
@@ -0,0 +1,7 @@
1
+ export {useAuthState} from '../hooks/auth/useAuthState'
2
+ export {useAuthToken} from '../hooks/auth/useAuthToken'
3
+ export {useCurrentUser} from '../hooks/auth/useCurrentUser'
4
+ export {useHandleCallback} from '../hooks/auth/useHandleCallback'
5
+ export {useLoginUrls} from '../hooks/auth/useLoginUrls'
6
+ export {useLogOut} from '../hooks/auth/useLogOut'
7
+ export {useSanityInstance} from '../hooks/context/useSanityInstance'
@@ -0,0 +1,10 @@
1
+ /**
2
+ * An example function that will be removed.
3
+ * @public
4
+ */
5
+ export function main(): void {
6
+ //
7
+ }
8
+
9
+ // export * from './components'
10
+ // export * from './hooks'
@@ -0,0 +1,95 @@
1
+ import {type Meta, type StoryObj} from '@storybook/react'
2
+
3
+ import {DocumentPreviewLayout} from '../DocumentPreviewLayout/DocumentPreviewLayout.tsx'
4
+ import {DocumentGridLayout} from './DocumentGridLayout.tsx'
5
+
6
+ const meta: Meta<typeof DocumentGridLayout> = {
7
+ title: 'DocumentGridLayout',
8
+ component: DocumentGridLayout,
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ const mockDocs = [
15
+ {id: '1', title: 'Just a title', url: '#', docType: 'article', status: 'published'},
16
+ {
17
+ id: '2',
18
+ title: 'A title, but also',
19
+ subtitle: 'A subtitle',
20
+ url: '#',
21
+ docType: 'article',
22
+ status: 'draft',
23
+ },
24
+ {
25
+ id: '3',
26
+ title: 'Hello World',
27
+ subtitle: 'What a nice list I get to live in',
28
+ url: '#',
29
+ docType: 'image',
30
+ status: 'published',
31
+ },
32
+ {
33
+ id: '4',
34
+ title: 'I’ve been selected',
35
+ subtitle: 'I feel special',
36
+ selected: true,
37
+ url: '#',
38
+ docType: 'video',
39
+ status: 'draft',
40
+ },
41
+ {
42
+ id: '5',
43
+ title: 'A very long title that at some point might get truncated if it goes for long enough',
44
+ subtitle:
45
+ 'Along with a subtitle that is quite long as well, in order to demonstrate the truncation of its text',
46
+ url: '#',
47
+ docType: 'audio',
48
+ status: 'published',
49
+ },
50
+ {
51
+ id: '6',
52
+ title: 'Hello World',
53
+ subtitle: 'What a nice list I get to live in',
54
+ url: '#',
55
+ docType: 'pdf',
56
+ status: 'published',
57
+ },
58
+ {id: '7', title: 'Just a title', url: '#', docType: 'note', status: 'published,'},
59
+ {
60
+ id: '8',
61
+ title: 'A title, but also',
62
+ subtitle: 'A subtitle',
63
+ url: '#',
64
+ docType: 'document',
65
+ status: 'draft',
66
+ },
67
+ {
68
+ id: '9',
69
+ title: 'Hello World',
70
+ subtitle: 'What a nice list I get to live in',
71
+ url: '#',
72
+ docType: 'biography',
73
+ status: 'published',
74
+ },
75
+ ]
76
+
77
+ export const Default: Story = {
78
+ render: () => {
79
+ return (
80
+ <DocumentGridLayout>
81
+ {mockDocs.map((doc) => (
82
+ <li key={doc.id}>
83
+ <DocumentPreviewLayout
84
+ title={doc.title}
85
+ subtitle={doc.subtitle}
86
+ docType={doc.docType}
87
+ status={doc.status}
88
+ url={doc.url}
89
+ />
90
+ </li>
91
+ ))}
92
+ </DocumentGridLayout>
93
+ )
94
+ },
95
+ }
@@ -0,0 +1,42 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {render, screen} from '../../../test/test-utils.tsx'
4
+ import {DocumentGridLayout} from './DocumentGridLayout.tsx'
5
+
6
+ describe('DocumentGridLayout', () => {
7
+ const mockDocuments = [
8
+ {
9
+ id: '1',
10
+ title: 'Test Document 1',
11
+ subtitle: 'Subtitle 1',
12
+ docType: 'post',
13
+ status: 'published',
14
+ url: '/doc/1',
15
+ },
16
+ {
17
+ id: '2',
18
+ title: 'Test Document 2',
19
+ subtitle: 'Subtitle 2',
20
+ docType: 'page',
21
+ status: 'draft',
22
+ url: '/doc/2',
23
+ },
24
+ ]
25
+
26
+ it('renders the expected content', () => {
27
+ render(
28
+ <DocumentGridLayout>
29
+ {mockDocuments.map((doc) => (
30
+ <li key={doc.id}>{doc.title}</li>
31
+ ))}
32
+ </DocumentGridLayout>,
33
+ )
34
+ const list = screen.getByRole('list')
35
+ expect(list.tagName).toBe('OL')
36
+ expect(list.dataset['ui']).toBe('DocumentGridLayout')
37
+
38
+ const items = screen.getAllByRole('listitem')
39
+ expect(items[0]).toContainHTML('Test Document 1')
40
+ expect(items[1]).toContainHTML('Test Document 2')
41
+ })
42
+ })
@@ -0,0 +1,23 @@
1
+ import type {PropsWithChildren, ReactElement} from 'react'
2
+ import styled from 'styled-components'
3
+
4
+ const DocumentGrid = styled.div`
5
+ display: grid;
6
+ list-style: none;
7
+ margin: unset;
8
+ padding: unset;
9
+ grid-template-columns: repeat(auto-fit, minmax(38ch, 1fr));
10
+ `
11
+
12
+ /**
13
+ * @public
14
+ */
15
+ export const DocumentGridLayout = (props: PropsWithChildren): ReactElement => {
16
+ return (
17
+ <DocumentGrid as="ol" data-ui="DocumentGridLayout">
18
+ {props.children}
19
+ </DocumentGrid>
20
+ )
21
+ }
22
+
23
+ DocumentGridLayout.displayName = 'DocumentGridLayout'
@@ -0,0 +1,95 @@
1
+ import {type Meta, type StoryObj} from '@storybook/react'
2
+
3
+ import {DocumentPreviewLayout} from '../DocumentPreviewLayout/DocumentPreviewLayout.tsx'
4
+ import {DocumentListLayout} from './DocumentListLayout.tsx'
5
+
6
+ const meta: Meta<typeof DocumentListLayout> = {
7
+ title: 'DocumentListLayout',
8
+ component: DocumentListLayout,
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ const mockDocs = [
15
+ {id: '1', title: 'Just a title', url: '#', docType: 'article', status: 'published'},
16
+ {
17
+ id: '2',
18
+ title: 'A title, but also',
19
+ subtitle: 'A subtitle',
20
+ url: '#',
21
+ docType: 'article',
22
+ status: 'draft',
23
+ },
24
+ {
25
+ id: '3',
26
+ title: 'Hello World',
27
+ subtitle: 'What a nice list I get to live in',
28
+ url: '#',
29
+ docType: 'image',
30
+ status: 'published',
31
+ },
32
+ {
33
+ id: '4',
34
+ title: 'I’ve been selected',
35
+ subtitle: 'I feel special',
36
+ selected: true,
37
+ url: '#',
38
+ docType: 'video',
39
+ status: 'draft',
40
+ },
41
+ {
42
+ id: '5',
43
+ title: 'A very long title that at some point might get truncated if it goes for long enough',
44
+ subtitle:
45
+ 'Along with a subtitle that is quite long as well, in order to demonstrate the truncation of its text',
46
+ url: '#',
47
+ docType: 'audio',
48
+ status: 'published',
49
+ },
50
+ {
51
+ id: '6',
52
+ title: 'Hello World',
53
+ subtitle: 'What a nice list I get to live in',
54
+ url: '#',
55
+ docType: 'pdf',
56
+ status: 'published',
57
+ },
58
+ {id: '7', title: 'Just a title', url: '#', docType: 'note', status: 'published,'},
59
+ {
60
+ id: '8',
61
+ title: 'A title, but also',
62
+ subtitle: 'A subtitle',
63
+ url: '#',
64
+ docType: 'document',
65
+ status: 'draft',
66
+ },
67
+ {
68
+ id: '9',
69
+ title: 'Hello World',
70
+ subtitle: 'What a nice list I get to live in',
71
+ url: '#',
72
+ docType: 'biography',
73
+ status: 'published',
74
+ },
75
+ ]
76
+
77
+ export const Default: Story = {
78
+ render: () => {
79
+ return (
80
+ <DocumentListLayout>
81
+ {mockDocs.map((doc) => (
82
+ <li key={doc.id}>
83
+ <DocumentPreviewLayout
84
+ title={doc.title}
85
+ subtitle={doc.subtitle}
86
+ docType={doc.docType}
87
+ status={doc.status}
88
+ url={doc.url}
89
+ />
90
+ </li>
91
+ ))}
92
+ </DocumentListLayout>
93
+ )
94
+ },
95
+ }
@@ -0,0 +1,42 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {render, screen} from '../../../test/test-utils.tsx'
4
+ import {DocumentListLayout} from './DocumentListLayout.tsx'
5
+
6
+ describe('DocumentListLayout', () => {
7
+ const mockDocuments = [
8
+ {
9
+ id: '1',
10
+ title: 'Test Document 1',
11
+ subtitle: 'Subtitle 1',
12
+ docType: 'post',
13
+ status: 'published',
14
+ url: '/doc/1',
15
+ },
16
+ {
17
+ id: '2',
18
+ title: 'Test Document 2',
19
+ subtitle: 'Subtitle 2',
20
+ docType: 'page',
21
+ status: 'draft',
22
+ url: '/doc/2',
23
+ },
24
+ ]
25
+
26
+ it('renders the expected content', () => {
27
+ render(
28
+ <DocumentListLayout>
29
+ {mockDocuments.map((doc) => (
30
+ <li key={doc.id}>{doc.title}</li>
31
+ ))}
32
+ </DocumentListLayout>,
33
+ )
34
+ const list = screen.getByRole('list')
35
+ expect(list.tagName).toBe('OL')
36
+ expect(list.dataset['ui']).toBe('DocumentListLayout')
37
+
38
+ const items = screen.getAllByRole('listitem')
39
+ expect(items[0]).toContainHTML('Test Document 1')
40
+ expect(items[1]).toContainHTML('Test Document 2')
41
+ })
42
+ })
@@ -0,0 +1,15 @@
1
+ import {Stack} from '@sanity/ui'
2
+ import type {PropsWithChildren, ReactElement} from 'react'
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export const DocumentListLayout = (props: PropsWithChildren): ReactElement => {
8
+ return (
9
+ <Stack as="ol" data-ui="DocumentListLayout">
10
+ {props.children}
11
+ </Stack>
12
+ )
13
+ }
14
+
15
+ DocumentListLayout.displayName = 'DocumentListLayout'