@sanity/assist 1.0.0

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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/index.d.ts +52 -0
  4. package/dist/index.esm.js +2341 -0
  5. package/dist/index.esm.js.map +1 -0
  6. package/dist/index.js +2341 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +98 -0
  9. package/sanity.json +8 -0
  10. package/src/_lib/connector/ConnectFromRegion.tsx +24 -0
  11. package/src/_lib/connector/ConnectToRegion.tsx +22 -0
  12. package/src/_lib/connector/ConnectorRegion.tsx +23 -0
  13. package/src/_lib/connector/ConnectorsProvider.tsx +19 -0
  14. package/src/_lib/connector/ConnectorsStore.ts +122 -0
  15. package/src/_lib/connector/ConnectorsStoreContext.ts +4 -0
  16. package/src/_lib/connector/helpers.ts +5 -0
  17. package/src/_lib/connector/index.ts +9 -0
  18. package/src/_lib/connector/mapConnectorToLine.ts +83 -0
  19. package/src/_lib/connector/types.ts +56 -0
  20. package/src/_lib/connector/useConnectorsStore.ts +13 -0
  21. package/src/_lib/connector/useRegionRects.ts +141 -0
  22. package/src/_lib/fixedListenQuery.ts +101 -0
  23. package/src/_lib/form/DocumentForm.tsx +197 -0
  24. package/src/_lib/form/helpers.ts +31 -0
  25. package/src/_lib/form/index.ts +1 -0
  26. package/src/_lib/randomKey.ts +29 -0
  27. package/src/_lib/useListeningQuery.ts +61 -0
  28. package/src/_lib/usePrevious.ts +9 -0
  29. package/src/assistConnectors/AssistConnectorsOverlay.tsx +132 -0
  30. package/src/assistConnectors/ConnectorPath.tsx +62 -0
  31. package/src/assistConnectors/draw/arrowPath.ts +9 -0
  32. package/src/assistConnectors/draw/connectorPath.ts +142 -0
  33. package/src/assistConnectors/index.ts +1 -0
  34. package/src/assistDocument/AssistDocumentContext.tsx +31 -0
  35. package/src/assistDocument/AssistDocumentContextProvider.tsx +17 -0
  36. package/src/assistDocument/AssistDocumentInput.tsx +46 -0
  37. package/src/assistDocument/RequestRunInstructionProvider.tsx +50 -0
  38. package/src/assistDocument/components/AssistDocumentForm.tsx +188 -0
  39. package/src/assistDocument/components/FieldRefPreview.tsx +27 -0
  40. package/src/assistDocument/components/InstructionsArrayField.tsx +8 -0
  41. package/src/assistDocument/components/InstructionsArrayInput.tsx +26 -0
  42. package/src/assistDocument/components/SelectedFieldContext.tsx +10 -0
  43. package/src/assistDocument/components/generic/HiddenFieldTitle.tsx +5 -0
  44. package/src/assistDocument/components/helpers.ts +21 -0
  45. package/src/assistDocument/components/instruction/BackToInstructionsLink.tsx +31 -0
  46. package/src/assistDocument/components/instruction/FieldRefInput.tsx +33 -0
  47. package/src/assistDocument/components/instruction/InstructionInput.tsx +87 -0
  48. package/src/assistDocument/components/instruction/PromptInput.tsx +52 -0
  49. package/src/assistDocument/components/instruction/appearance/IconInput.tsx +46 -0
  50. package/src/assistDocument/components/instruction/appearance/InstructionVisibility.tsx +37 -0
  51. package/src/assistDocument/hooks/useAssistDocumentContextValue.tsx +68 -0
  52. package/src/assistDocument/hooks/useDocumentState.ts +6 -0
  53. package/src/assistDocument/hooks/useInstructionToaster.tsx +74 -0
  54. package/src/assistDocument/hooks/useStudioAssistDocument.ts +119 -0
  55. package/src/assistDocument/index.ts +1 -0
  56. package/src/assistFormComponents/AssistField.tsx +51 -0
  57. package/src/assistFormComponents/AssistFormBlock.tsx +31 -0
  58. package/src/assistFormComponents/AssistInlineFormBlock.tsx +14 -0
  59. package/src/assistFormComponents/AssistItem.tsx +20 -0
  60. package/src/assistFormComponents/validation/listItem.tsx +63 -0
  61. package/src/assistFormComponents/validation/validationList.tsx +89 -0
  62. package/src/assistInspector/AssistInspector.tsx +379 -0
  63. package/src/assistInspector/FieldAutocomplete.tsx +119 -0
  64. package/src/assistInspector/InstructionTaskHistoryButton.tsx +261 -0
  65. package/src/assistInspector/constants.ts +1 -0
  66. package/src/assistInspector/helpers.ts +125 -0
  67. package/src/assistInspector/index.ts +26 -0
  68. package/src/assistLayout/AiAssistanceConfigContext.tsx +81 -0
  69. package/src/assistLayout/AlphaMigration.tsx +311 -0
  70. package/src/assistLayout/AssistLayout.tsx +38 -0
  71. package/src/assistLayout/RunInstructionProvider.tsx +222 -0
  72. package/src/components/AssistFeatureBadge.tsx +9 -0
  73. package/src/components/Delay.tsx +25 -0
  74. package/src/components/HideReferenceChangedBannerInput.tsx +25 -0
  75. package/src/components/SafeValueInput.tsx +73 -0
  76. package/src/components/TimeAgo.tsx +18 -0
  77. package/src/constants.ts +20 -0
  78. package/src/fieldActions/PrivateIcon.tsx +20 -0
  79. package/src/fieldActions/assistFieldActions.tsx +230 -0
  80. package/src/globals.d.ts +4 -0
  81. package/src/helpers/assistSupported.ts +44 -0
  82. package/src/helpers/ids.ts +19 -0
  83. package/src/helpers/misc.ts +16 -0
  84. package/src/helpers/typeUtils.ts +15 -0
  85. package/src/helpers/useAssistSupported.ts +10 -0
  86. package/src/index.ts +6 -0
  87. package/src/legacy-types.ts +72 -0
  88. package/src/onboarding/FieldActionsOnboarding.tsx +90 -0
  89. package/src/onboarding/FirstAssistedPathProvider.tsx +29 -0
  90. package/src/onboarding/InspectorOnboarding.tsx +46 -0
  91. package/src/onboarding/onboardingStore.ts +33 -0
  92. package/src/plugin.tsx +80 -0
  93. package/src/presence/AiFieldPresence.tsx +28 -0
  94. package/src/presence/AssistAvatar.tsx +96 -0
  95. package/src/presence/AssistDocumentPresence.tsx +58 -0
  96. package/src/presence/useAssistPresence.ts +61 -0
  97. package/src/schemas/assistDocumentSchema.tsx +450 -0
  98. package/src/schemas/contextDocumentSchema.tsx +56 -0
  99. package/src/schemas/index.ts +25 -0
  100. package/src/schemas/serialize/SchemTypeTool.tsx +102 -0
  101. package/src/schemas/serialize/schemaUtils.ts +37 -0
  102. package/src/schemas/serialize/serializeSchema.test.ts +382 -0
  103. package/src/schemas/serialize/serializeSchema.ts +162 -0
  104. package/src/schemas/serializedSchemaTypeSchema.ts +59 -0
  105. package/src/schemas/typeDefExtensions.ts +30 -0
  106. package/src/types.ts +167 -0
  107. package/src/useApiClient.ts +140 -0
  108. package/src/vite.config.ts +9 -0
  109. package/v2-incompatible.js +11 -0
@@ -0,0 +1,101 @@
1
+ import {SanityClient} from '@sanity/client'
2
+ import {defer, delay, merge, Observable, of, partition, switchMap, throwError} from 'rxjs'
3
+ import {filter, mergeMap, share, take} from 'rxjs/operators'
4
+ import {exhaustMapToWithTrailing} from 'rxjs-exhaustmap-with-trailing'
5
+ import {MutationEvent, ReconnectEvent, WelcomeEvent} from 'sanity'
6
+
7
+ /** @internal */
8
+ export type ListenQueryParams = Record<string, string | number | boolean | string[]>
9
+
10
+ /** @beta */
11
+ export interface ListenQueryOptions {
12
+ tag?: string
13
+ apiVersion?: string
14
+ throttleTime?: number
15
+ transitions?: ('update' | 'appear' | 'disappear')[]
16
+ }
17
+
18
+ const fetch = (
19
+ client: SanityClient,
20
+ query: string,
21
+ params: ListenQueryParams,
22
+ options: ListenQueryOptions
23
+ ) =>
24
+ defer(() =>
25
+ // getVersionedClient(options.apiVersion)
26
+ client.observable.fetch(query, params, {
27
+ tag: options.tag,
28
+ filterResponse: true,
29
+ })
30
+ )
31
+
32
+ const listen = (
33
+ client: SanityClient,
34
+ query: string,
35
+ params: ListenQueryParams,
36
+ options: ListenQueryOptions
37
+ ) =>
38
+ defer(() =>
39
+ // getVersionedClient(options.apiVersion)
40
+ client.listen(query, params, {
41
+ events: ['welcome', 'mutation', 'reconnect'],
42
+ includeResult: false,
43
+ visibility: 'query',
44
+ tag: options.tag,
45
+ })
46
+ ) as Observable<ReconnectEvent | WelcomeEvent | MutationEvent>
47
+
48
+ function isWelcomeEvent(
49
+ event: MutationEvent | ReconnectEvent | WelcomeEvent
50
+ ): event is WelcomeEvent {
51
+ return event.type === 'welcome'
52
+ }
53
+
54
+ /** @internal */
55
+ export const listenQuery = (
56
+ client: SanityClient,
57
+ query: string | {fetch: string; listen: string},
58
+ params: ListenQueryParams = {},
59
+ options: ListenQueryOptions = {}
60
+ ) => {
61
+ const fetchQuery = typeof query === 'string' ? query : query.fetch
62
+ const listenerQuery = typeof query === 'string' ? query : query.listen
63
+
64
+ const fetchOnce$ = fetch(client, fetchQuery, params, options)
65
+
66
+ const events$ = listen(client, listenerQuery, params, options).pipe(
67
+ mergeMap((ev, i) => {
68
+ const isFirst = i === 0
69
+ if (isFirst && !isWelcomeEvent(ev)) {
70
+ // if the first event is not welcome, it is most likely a reconnect and
71
+ // if it's not a reconnect something is very wrong
72
+ return throwError(
73
+ new Error(
74
+ ev.type === 'reconnect'
75
+ ? 'Could not establish EventSource connection'
76
+ : `Received unexpected type of first event "${ev.type}"`
77
+ )
78
+ )
79
+ }
80
+ return of(ev)
81
+ }),
82
+ share()
83
+ )
84
+
85
+ const [welcome$, mutationAndReconnect$] = partition(events$, isWelcomeEvent)
86
+ const isRelevantEvent = (event: MutationEvent | ReconnectEvent | WelcomeEvent): boolean => {
87
+ if (!options.transitions || event.type !== 'mutation') {
88
+ return true
89
+ }
90
+
91
+ return options.transitions.includes(event.transition)
92
+ }
93
+
94
+ return merge(
95
+ welcome$.pipe(take(1)),
96
+ mutationAndReconnect$.pipe(
97
+ filter(isRelevantEvent),
98
+ switchMap((event) => merge(of(event), of(event).pipe(delay(options.throttleTime || 1000))))
99
+ )
100
+ ).pipe(exhaustMapToWithTrailing(fetchOnce$))
101
+ }
@@ -0,0 +1,197 @@
1
+ import {Box, BoxProps, Flex, focusFirstDescendant, Spinner, Text} from '@sanity/ui'
2
+ import React, {HTMLProps, useEffect, useMemo, useRef} from 'react'
3
+ import {tap} from 'rxjs/operators'
4
+ import {
5
+ createPatchChannel,
6
+ DocumentMutationEvent,
7
+ DocumentRebaseEvent,
8
+ FormBuilder,
9
+ fromMutationPatches,
10
+ PatchMsg,
11
+ useDocumentPresence,
12
+ useDocumentStore,
13
+ } from 'sanity'
14
+ import {useDocumentPane} from 'sanity/desk'
15
+
16
+ const preventDefault = (ev: React.FormEvent) => ev.preventDefault()
17
+
18
+ export function DocumentForm(
19
+ props: Omit<BoxProps, 'as'> & Omit<HTMLProps<HTMLDivElement>, 'as' | 'onSubmit' | 'ref'>
20
+ ) {
21
+ const {
22
+ collapsedFieldSets,
23
+ collapsedPaths,
24
+ displayed: value,
25
+ documentId,
26
+ documentType,
27
+ editState,
28
+ formState,
29
+ onBlur,
30
+ onChange,
31
+ onFocus,
32
+ onPathOpen,
33
+ onSetActiveFieldGroup,
34
+ onSetCollapsedFieldSet,
35
+ onSetCollapsedPath,
36
+ ready,
37
+ validation,
38
+ } = useDocumentPane()
39
+
40
+ const documentStore = useDocumentStore()
41
+ const presence = useDocumentPresence(documentId)
42
+
43
+ // The `patchChannel` is an INTERNAL publish/subscribe channel that we use to notify form-builder
44
+ // nodes about both remote and local patches.
45
+ // - Used by the Portable Text input to modify selections.
46
+ // - Used by `withDocument` to reset value.
47
+ const patchChannel = useMemo(() => createPatchChannel(), [])
48
+
49
+ const isLocked = editState?.transactionSyncLock?.enabled
50
+
51
+ useEffect(() => {
52
+ const sub = documentStore.pair
53
+ .documentEvents(documentId, documentType)
54
+ .pipe(
55
+ tap((event) => {
56
+ if (event.type === 'mutation') {
57
+ patchChannel.publish(prepareMutationEvent(event))
58
+ }
59
+
60
+ if (event.type === 'rebase') {
61
+ patchChannel.publish(prepareRebaseEvent(event))
62
+ }
63
+ })
64
+ )
65
+ .subscribe()
66
+
67
+ return () => {
68
+ sub.unsubscribe()
69
+ }
70
+ }, [documentId, documentStore, documentType, patchChannel])
71
+
72
+ const hasRev = Boolean(value?._rev)
73
+ useEffect(() => {
74
+ if (hasRev) {
75
+ // this is a workaround for an issue that caused the document pushed to withDocument to get
76
+ // stuck at the first initial value.
77
+ // This effect is triggered only when the document goes from not having a revision, to getting one
78
+ // so it will kick in as soon as the document is received from the backend
79
+ patchChannel.publish({
80
+ type: 'mutation',
81
+ patches: [],
82
+ snapshot: value,
83
+ })
84
+ }
85
+ // React to changes in hasRev only
86
+ // eslint-disable-next-line react-hooks/exhaustive-deps
87
+ }, [hasRev])
88
+
89
+ const formRef = useRef<null | HTMLDivElement>(null)
90
+
91
+ useEffect(() => {
92
+ focusFirstDescendant(formRef.current!)
93
+ }, [])
94
+
95
+ if (isLocked) {
96
+ return (
97
+ <Box as="form" {...props} ref={formRef}>
98
+ <Flex
99
+ align="center"
100
+ direction="column"
101
+ height="fill"
102
+ justify="center"
103
+ padding={3}
104
+ sizing="border"
105
+ >
106
+ <Text size={1}>
107
+ Please hold tight while the document is synced. This usually happens right after the
108
+ document has been published, and it shouldn’t take more than a few seconds
109
+ </Text>
110
+ </Flex>
111
+ </Box>
112
+ )
113
+ }
114
+
115
+ return (
116
+ <Box as="form" {...props} onSubmit={preventDefault} ref={formRef}>
117
+ {ready ? (
118
+ formState === null ? (
119
+ <Flex
120
+ align="center"
121
+ direction="column"
122
+ height="fill"
123
+ justify="center"
124
+ padding={3}
125
+ sizing="border"
126
+ >
127
+ <Text size={1}>This form is hidden</Text>
128
+ </Flex>
129
+ ) : (
130
+ <FormBuilder
131
+ __internal_patchChannel={patchChannel}
132
+ collapsedFieldSets={collapsedFieldSets}
133
+ collapsedPaths={collapsedPaths}
134
+ focusPath={formState.focusPath}
135
+ changed={formState.changed}
136
+ focused={formState.focused}
137
+ groups={formState.groups}
138
+ id="root"
139
+ members={formState.members}
140
+ onChange={onChange}
141
+ onFieldGroupSelect={onSetActiveFieldGroup}
142
+ onPathBlur={onBlur}
143
+ onPathFocus={onFocus}
144
+ onPathOpen={onPathOpen}
145
+ onSetFieldSetCollapsed={onSetCollapsedFieldSet}
146
+ onSetPathCollapsed={onSetCollapsedPath}
147
+ presence={presence}
148
+ readOnly={formState.readOnly}
149
+ schemaType={formState.schemaType}
150
+ validation={validation}
151
+ value={formState.value}
152
+ />
153
+ )
154
+ ) : (
155
+ <Flex
156
+ align="center"
157
+ direction="column"
158
+ height="fill"
159
+ justify="center"
160
+ padding={3}
161
+ sizing="border"
162
+ >
163
+ <Spinner muted />
164
+
165
+ <Box marginTop={3}>
166
+ <Text align="center" muted size={1}>
167
+ Loading document
168
+ </Text>
169
+ </Box>
170
+ </Flex>
171
+ )}
172
+ </Box>
173
+ )
174
+ }
175
+
176
+ function prepareMutationEvent(event: DocumentMutationEvent): PatchMsg {
177
+ const patches = event.mutations.map((mut) => mut.patch).filter(Boolean)
178
+
179
+ return {
180
+ type: 'mutation',
181
+ snapshot: event.document,
182
+ patches: fromMutationPatches(event.origin, patches),
183
+ }
184
+ }
185
+
186
+ function prepareRebaseEvent(event: DocumentRebaseEvent): PatchMsg {
187
+ const remotePatches = event.remoteMutations.map((mut) => mut.patch).filter(Boolean)
188
+ const localPatches = event.localMutations.map((mut) => mut.patch).filter(Boolean)
189
+
190
+ return {
191
+ type: 'rebase',
192
+ snapshot: event.document,
193
+ patches: fromMutationPatches('remote', remotePatches).concat(
194
+ fromMutationPatches('local', localPatches)
195
+ ),
196
+ }
197
+ }
@@ -0,0 +1,31 @@
1
+ import {isArraySchemaType, isObjectSchemaType, ObjectItem, SchemaType} from 'sanity'
2
+ import {randomKey} from '../randomKey'
3
+
4
+ export function createProtoValue(type: SchemaType): any {
5
+ if (isObjectSchemaType(type)) {
6
+ return type.name === 'object' ? {} : {_type: type.name}
7
+ }
8
+ if (isArraySchemaType(type)) {
9
+ return []
10
+ }
11
+ if (type.jsonType === 'string') {
12
+ return ''
13
+ }
14
+ if (type.jsonType === 'number') {
15
+ return 0
16
+ }
17
+ if (type.jsonType === 'boolean') {
18
+ return false
19
+ }
20
+ return undefined
21
+ }
22
+
23
+ export function createProtoArrayValue<Item extends ObjectItem>(type: SchemaType): Item {
24
+ if (!isObjectSchemaType(type)) {
25
+ throw new Error(
26
+ `Invalid item type: "${type.type}". Default array input can only contain objects (for now)`
27
+ )
28
+ }
29
+
30
+ return {...createProtoValue(type), _key: randomKey(12)} as Item
31
+ }
@@ -0,0 +1 @@
1
+ export * from './DocumentForm'
@@ -0,0 +1,29 @@
1
+ import getRandomValues from 'get-random-values-esm'
2
+
3
+ // WHATWG crypto RNG - https://w3c.github.io/webcrypto/Overview.html
4
+ function whatwgRNG(length = 16) {
5
+ const rnds8 = new Uint8Array(length)
6
+ getRandomValues(rnds8)
7
+ return rnds8
8
+ }
9
+
10
+ const getByteHexTable = (() => {
11
+ let table: string[]
12
+ return () => {
13
+ if (table) {
14
+ return table
15
+ }
16
+ table = []
17
+ for (let i = 0; i < 256; ++i) {
18
+ table[i] = (i + 0x100).toString(16).substring(1)
19
+ }
20
+ return table
21
+ }
22
+ })()
23
+
24
+ export function randomKey(length?: number) {
25
+ const table = getByteHexTable()
26
+ return whatwgRNG(length)
27
+ .reduce((str, n) => str + table[n], '')
28
+ .slice(0, length)
29
+ }
@@ -0,0 +1,61 @@
1
+ import {useEffect, useRef, useState} from 'react'
2
+ import {catchError, distinctUntilChanged} from 'rxjs/operators'
3
+ import isEqual from 'react-fast-compare'
4
+ import {ListenQueryOptions, useClient} from 'sanity'
5
+ import {listenQuery} from './fixedListenQuery'
6
+
7
+ type Params = Record<string, string | number | boolean | string[]>
8
+
9
+ type ReturnShape<T> = {
10
+ loading: boolean
11
+ error: boolean
12
+ data: T | null
13
+ }
14
+
15
+ type Observable = {
16
+ unsubscribe: () => void
17
+ }
18
+
19
+ const DEFAULT_PARAMS = {}
20
+ const DEFAULT_OPTIONS: ListenQueryOptions = {apiVersion: `v2022-05-09`}
21
+
22
+ export function useListeningQuery<T>(
23
+ query: string,
24
+ params: Params = DEFAULT_PARAMS,
25
+ options: ListenQueryOptions = DEFAULT_OPTIONS
26
+ ): ReturnShape<T> {
27
+ const [loading, setLoading] = useState(true)
28
+ const [error, setError] = useState(false)
29
+ const [data, setData] = useState<T | null>(null)
30
+ const subscription = useRef<null | Observable>(null)
31
+
32
+ const client = useClient({apiVersion: `v2022-05-09`})
33
+
34
+ useEffect(() => {
35
+ if (query) {
36
+ subscription.current = listenQuery(client, query, params, options)
37
+ .pipe(
38
+ distinctUntilChanged(isEqual),
39
+ catchError((err) => {
40
+ console.error(err)
41
+ setError(err)
42
+ setLoading(false)
43
+ setData(null)
44
+
45
+ return err
46
+ })
47
+ )
48
+ .subscribe((documents) => {
49
+ setData((current) => (isEqual(current, documents) ? current : documents))
50
+ setLoading(false)
51
+ setError(false)
52
+ })
53
+ }
54
+
55
+ return () => {
56
+ return subscription.current ? subscription.current.unsubscribe() : undefined
57
+ }
58
+ }, [query, params, options, client])
59
+
60
+ return {loading, error, data}
61
+ }
@@ -0,0 +1,9 @@
1
+ import {useEffect, useRef} from 'react'
2
+
3
+ export function usePrevious<T>(value: T) {
4
+ const ref = useRef<T>()
5
+ useEffect(() => {
6
+ ref.current = value
7
+ }, [value])
8
+ return ref.current
9
+ }
@@ -0,0 +1,132 @@
1
+ import {Fragment, useEffect, useState} from 'react'
2
+ import {Connector, ConnectorOptions} from '../_lib/connector'
3
+ import {ConnectorPath} from './ConnectorPath'
4
+
5
+ const DEBUG = false
6
+
7
+ const options: ConnectorOptions = {
8
+ arrow: {
9
+ marginX: 10.5,
10
+ marginY: 5,
11
+ size: 4,
12
+ threshold: 16.5,
13
+ },
14
+ divider: {
15
+ offsetX: -10.5,
16
+ },
17
+ path: {
18
+ cornerRadius: 3,
19
+ marginY: 10.5,
20
+ strokeWidth: 1,
21
+ },
22
+ }
23
+
24
+ export function AssistConnectorsOverlay(props: {connectors: Connector[]}) {
25
+ const {connectors} = props
26
+ // const zIndexes = connectors.map((connector) => {
27
+ // const zIndex = connector.from.payload?.zIndex
28
+
29
+ // if (typeof zIndex === 'number') {
30
+ // return zIndex
31
+ // }
32
+
33
+ // return 1
34
+ // })
35
+ const [, setRedraw] = useState(false)
36
+ useEffect(() => {
37
+ // hacky workaround to force redraw for connectors on initial render
38
+ // this seem to improve initial measurements of elements
39
+ setRedraw(true)
40
+ }, [])
41
+
42
+ // const zIndex = zIndexes.length ? Math.max(...zIndexes) : 1
43
+
44
+ return (
45
+ <>
46
+ <svg
47
+ fill="none"
48
+ width={window.innerWidth}
49
+ height={window.innerHeight}
50
+ style={{
51
+ position: 'absolute',
52
+ top: 0,
53
+ left: 0,
54
+ width: '100%',
55
+ height: '100%',
56
+ pointerEvents: 'none',
57
+ zIndex: 150,
58
+ // zIndex,
59
+ }}
60
+ >
61
+ {connectors.map((connector) => (
62
+ <ConnectorPath
63
+ from={connector.from}
64
+ key={connector.key}
65
+ options={options}
66
+ to={connector.to}
67
+ />
68
+ ))}
69
+ </svg>
70
+ {DEBUG &&
71
+ connectors.map(({key, from, to}) => {
72
+ return (
73
+ <Fragment key={key}>
74
+ <div
75
+ style={{
76
+ position: 'fixed',
77
+ top: from.bounds.y,
78
+ left: from.bounds.x,
79
+ width: from.bounds.w,
80
+ height: from.bounds.h,
81
+ pointerEvents: 'none',
82
+ overflow: 'hidden',
83
+ outline: '1px dotted red',
84
+ outlineOffset: -1,
85
+ zIndex: 10000000 - 1,
86
+ }}
87
+ >
88
+ <div
89
+ style={{
90
+ position: 'absolute',
91
+ top: from.element.y - from.bounds.y,
92
+ left: from.element.x - from.bounds.x,
93
+ width: from.element.w,
94
+ height: from.element.h,
95
+ border: '1px solid red',
96
+ boxSizing: 'border-box',
97
+ }}
98
+ />
99
+ </div>
100
+
101
+ <div
102
+ style={{
103
+ position: 'fixed',
104
+ top: to.bounds.y,
105
+ left: to.bounds.x,
106
+ width: to.bounds.w,
107
+ height: to.bounds.h,
108
+ pointerEvents: 'none',
109
+ overflow: 'hidden',
110
+ outline: '1px dotted teal',
111
+ outlineOffset: -1,
112
+ zIndex: 10000000 - 1,
113
+ }}
114
+ >
115
+ <div
116
+ style={{
117
+ position: 'absolute',
118
+ top: to.element.y - to.bounds.y,
119
+ left: to.element.x - to.bounds.x,
120
+ width: to.element.w,
121
+ height: to.element.h,
122
+ border: '1px solid teal',
123
+ boxSizing: 'border-box',
124
+ }}
125
+ />
126
+ </div>
127
+ </Fragment>
128
+ )
129
+ })}
130
+ </>
131
+ )
132
+ }
@@ -0,0 +1,62 @@
1
+ import {rgba, useTheme} from '@sanity/ui'
2
+ import {useMemo} from 'react'
3
+ import {ConnectorOptions, mapConnectorToLine, Rect} from '../_lib/connector'
4
+ import {arrowPath} from './draw/arrowPath'
5
+ import {drawConnectorPath} from './draw/connectorPath'
6
+
7
+ export function ConnectorPath(props: {
8
+ from: {bounds: Rect; element: Rect}
9
+ to: {bounds: Rect; element: Rect}
10
+ options: ConnectorOptions
11
+ }) {
12
+ const {from, options, to} = props
13
+ const {strokeWidth} = options.path
14
+ const theme = useTheme()
15
+
16
+ const line = useMemo(() => mapConnectorToLine(options, {from, to}), [from, options, to])
17
+
18
+ return (
19
+ <>
20
+ <path
21
+ d={drawConnectorPath(options, line)}
22
+ stroke={theme.sanity.color.base.bg}
23
+ strokeWidth={strokeWidth + 4}
24
+ />
25
+
26
+ <path
27
+ d={drawConnectorPath(options, line)}
28
+ stroke={rgba(theme.sanity.color.base.border, 0.5)}
29
+ strokeWidth={strokeWidth}
30
+ />
31
+
32
+ {line.from.isAbove && (
33
+ <path
34
+ d={arrowPath(
35
+ options,
36
+ line.from.x + options.arrow.marginX,
37
+ line.from.bounds.y - options.arrow.threshold + options.arrow.marginY,
38
+ -1
39
+ )}
40
+ stroke={theme.sanity.color.base.border}
41
+ strokeWidth={strokeWidth}
42
+ />
43
+ )}
44
+
45
+ {line.from.isBelow && (
46
+ <path
47
+ d={arrowPath(
48
+ options,
49
+ line.from.x + options.arrow.marginX,
50
+ line.from.bounds.y +
51
+ line.from.bounds.h +
52
+ options.arrow.threshold -
53
+ options.arrow.marginY,
54
+ 1
55
+ )}
56
+ stroke={theme.sanity.color.base.border}
57
+ strokeWidth={strokeWidth}
58
+ />
59
+ )}
60
+ </>
61
+ )
62
+ }
@@ -0,0 +1,9 @@
1
+ import {ConnectorOptions} from '../../_lib/connector'
2
+
3
+ export function arrowPath(options: ConnectorOptions, x: number, y: number, dir: 1 | -1): string {
4
+ return [
5
+ `M ${x - options.arrow.size} ${y - options.arrow.size * dir} `,
6
+ `L ${x} ${y}`,
7
+ `L ${x + options.arrow.size} ${y - options.arrow.size * dir}`,
8
+ ].join('')
9
+ }