sanity-plugin-workflow 1.0.0-beta.1 → 1.0.0-beta.10

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 (56) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +81 -13
  3. package/lib/{src/index.d.ts → index.d.ts} +4 -3
  4. package/lib/index.esm.js +2106 -1
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +2119 -1
  7. package/lib/index.js.map +1 -1
  8. package/package.json +51 -40
  9. package/src/actions/AssignWorkflow.tsx +49 -0
  10. package/src/actions/BeginWorkflow.tsx +63 -0
  11. package/src/actions/CompleteWorkflow.tsx +41 -0
  12. package/src/actions/UpdateWorkflow.tsx +126 -0
  13. package/src/badges/AssigneesBadge.tsx +53 -0
  14. package/src/badges/StateBadge.tsx +28 -0
  15. package/src/components/DocumentCard/AvatarGroup.tsx +12 -8
  16. package/src/components/DocumentCard/CompleteButton.tsx +68 -0
  17. package/src/components/DocumentCard/EditButton.tsx +3 -2
  18. package/src/components/DocumentCard/Field.tsx +38 -0
  19. package/src/components/DocumentCard/Validate.tsx +21 -0
  20. package/src/components/DocumentCard/ValidationStatus.tsx +37 -0
  21. package/src/components/DocumentCard/core/DraftStatus.tsx +32 -0
  22. package/src/components/DocumentCard/core/PublishedStatus.tsx +39 -0
  23. package/src/components/DocumentCard/core/TimeAgo.tsx +11 -0
  24. package/src/components/DocumentCard/index.tsx +177 -68
  25. package/src/components/DocumentList.tsx +169 -0
  26. package/src/components/Filters.tsx +168 -0
  27. package/src/components/FloatingCard.tsx +29 -0
  28. package/src/components/StateTitle/Status.tsx +27 -0
  29. package/src/components/StateTitle/index.tsx +78 -0
  30. package/src/components/UserAssignment.tsx +57 -75
  31. package/src/components/UserAssignmentInput.tsx +27 -0
  32. package/src/components/UserDisplay.tsx +57 -0
  33. package/src/components/Verify.tsx +297 -0
  34. package/src/components/WorkflowContext.tsx +71 -0
  35. package/src/components/WorkflowSignal.tsx +30 -0
  36. package/src/components/WorkflowTool.tsx +373 -162
  37. package/src/constants/index.ts +31 -0
  38. package/src/helpers/arraysContainMatchingString.ts +6 -0
  39. package/src/helpers/filterItemsAndSort.ts +41 -0
  40. package/src/helpers/generateMultipleOrderRanks.ts +80 -0
  41. package/src/helpers/initialRank.ts +13 -0
  42. package/src/hooks/useWorkflowDocuments.tsx +76 -78
  43. package/src/hooks/useWorkflowMetadata.tsx +31 -26
  44. package/src/index.ts +60 -57
  45. package/src/schema/workflow/workflow.metadata.ts +68 -0
  46. package/src/tools/index.ts +15 -0
  47. package/src/types/index.ts +27 -6
  48. package/src/actions/DemoteAction.tsx +0 -62
  49. package/src/actions/PromoteAction.tsx +0 -62
  50. package/src/actions/RequestReviewAction.js +0 -61
  51. package/src/actions/index.js +0 -21
  52. package/src/badges/index.tsx +0 -31
  53. package/src/components/Mutate.tsx +0 -54
  54. package/src/components/StateTimeline.tsx +0 -98
  55. package/src/components/UserSelectInput.tsx +0 -43
  56. package/src/schema/workflow/metadata.ts +0 -38
@@ -1,92 +1,201 @@
1
1
  /* eslint-disable react/prop-types */
2
+ import {DragHandleIcon} from '@sanity/icons'
3
+ import {Box, Card, CardTone, Flex, Stack, useTheme} from '@sanity/ui'
4
+ import {useCallback, useEffect, useMemo, useState} from 'react'
2
5
  import {
3
- Box,
4
- Button,
5
- Card,
6
- Flex,
7
- Popover,
8
- Stack,
9
- useClickOutside,
10
- useTheme,
11
- } from '@sanity/ui'
12
- import {AddIcon, DragHandleIcon} from '@sanity/icons'
13
- import React, {useState} from 'react'
14
- import {useSchema, SchemaType} from 'sanity'
15
- import {UserSelectMenu} from 'sanity-plugin-utils'
6
+ SchemaType,
7
+ useSchema,
8
+ ValidationStatus as ValidationStatusType,
9
+ } from 'sanity'
16
10
  import {Preview} from 'sanity'
17
11
 
12
+ import {SanityDocumentWithMetadata, State, User} from '../../types'
13
+ import UserDisplay from '../UserDisplay'
14
+ import CompleteButton from './CompleteButton'
15
+ import {DraftStatus} from './core/DraftStatus'
16
+ import {PublishedStatus} from './core/PublishedStatus'
18
17
  import EditButton from './EditButton'
19
- import {SanityDocumentWithMetadata, User} from '../../types'
20
- import AvatarGroup from './AvatarGroup'
21
- import UserAssignment from '../UserAssignment'
18
+ import Validate from './Validate'
19
+ import {ValidationStatus} from './ValidationStatus'
22
20
 
23
21
  type DocumentCardProps = {
24
- userList: User[]
22
+ isDragDisabled: boolean
23
+ isPatching: boolean
24
+ userRoleCanDrop: boolean
25
25
  isDragging: boolean
26
26
  item: SanityDocumentWithMetadata
27
+ states: State[]
28
+ toggleInvalidDocumentId: (
29
+ documentId: string,
30
+ action: 'ADD' | 'REMOVE'
31
+ ) => void
32
+ userList: User[]
27
33
  }
28
34
 
29
35
  export function DocumentCard(props: DocumentCardProps) {
30
- const {userList, isDragging, item} = props
36
+ const {
37
+ isDragDisabled,
38
+ isPatching,
39
+ userRoleCanDrop,
40
+ isDragging,
41
+ item,
42
+ states,
43
+ toggleInvalidDocumentId,
44
+ userList,
45
+ } = props
31
46
  const {assignees = [], documentId} = item._metadata ?? {}
32
47
  const schema = useSchema()
48
+ const state = states.find((s) => s.id === item._metadata?.state)
33
49
 
34
50
  const isDarkMode = useTheme().sanity.color.dark
35
- const defaultCardTone = isDarkMode ? 'transparent' : 'default'
51
+ const defaultCardTone = isDarkMode ? `transparent` : `default`
52
+
53
+ // Validation only runs if the state requests it
54
+ // Because it's not performant to run it on many documents simultaneously
55
+ // So we fake it here, and maybe set it inside <Validate />
56
+ const [optimisticValidation, setOptimisticValidation] =
57
+ useState<ValidationStatusType>({
58
+ isValidating: state?.requireValidation ?? false,
59
+ validation: [],
60
+ })
61
+
62
+ const {isValidating, validation} = optimisticValidation
63
+
64
+ const handleValidation = useCallback((updates: ValidationStatusType) => {
65
+ setOptimisticValidation(updates)
66
+ }, [])
67
+
68
+ const cardTone = useMemo(() => {
69
+ let tone: CardTone = defaultCardTone
36
70
 
37
- // Open/close handler
38
- // const [popoverRef, setPopoverRef] = useState(null)
39
- // const [openId, setOpenId] = useState<string | undefined>(``)
71
+ if (!userRoleCanDrop) return isDarkMode ? `default` : `transparent`
72
+ if (!documentId) return tone
73
+ if (isPatching) tone = isDarkMode ? `default` : `transparent`
74
+ if (isDragging) tone = `positive`
40
75
 
41
- // useClickOutside(() => setOpenId(``), [popoverRef])
76
+ if (state?.requireValidation && !isValidating && validation.length > 0) {
77
+ if (validation.some((v) => v.level === 'error')) {
78
+ tone = `critical`
79
+ } else {
80
+ tone = `caution`
81
+ }
82
+ }
42
83
 
43
- // const handleKeyDown = React.useCallback((e) => {
44
- // if (e.key === 'Escape') {
45
- // setOpenId(``)
46
- // }
47
- // }, [])
84
+ return tone
85
+ }, [
86
+ defaultCardTone,
87
+ userRoleCanDrop,
88
+ isPatching,
89
+ isDarkMode,
90
+ documentId,
91
+ isDragging,
92
+ isValidating,
93
+ validation,
94
+ state?.requireValidation,
95
+ ])
96
+
97
+ // Update validation status
98
+ // Cannot be done in the above memo because it would set state during render
99
+ useEffect(() => {
100
+ if (!isValidating && validation.length > 0) {
101
+ if (validation.some((v) => v.level === 'error')) {
102
+ toggleInvalidDocumentId(documentId, 'ADD')
103
+ } else {
104
+ toggleInvalidDocumentId(documentId, 'REMOVE')
105
+ }
106
+ } else {
107
+ toggleInvalidDocumentId(documentId, 'REMOVE')
108
+ }
109
+ }, [documentId, isValidating, toggleInvalidDocumentId, validation])
110
+
111
+ const hasError = useMemo(
112
+ () => (isValidating ? false : validation.some((v) => v.level === 'error')),
113
+ [isValidating, validation]
114
+ )
115
+
116
+ const isLastState = useMemo(
117
+ () => states[states.length - 1].id === item._metadata?.state,
118
+ [states, item._metadata.state]
119
+ )
48
120
 
49
121
  return (
50
- <Box paddingY={2} paddingX={3}>
51
- <Card
52
- radius={2}
53
- shadow={isDragging ? 3 : 1}
54
- tone={isDragging ? 'positive' : defaultCardTone}
55
- >
56
- <Stack>
57
- <Card
58
- borderBottom
59
- radius={2}
60
- padding={3}
61
- paddingLeft={2}
62
- tone="inherit"
63
- style={{pointerEvents: 'none'}}
64
- >
65
- <Flex align="center" justify="space-between" gap={1}>
66
- <Preview
67
- layout="default"
68
- value={item}
69
- schemaType={schema.get(item._type) as SchemaType}
70
- />
71
- <DragHandleIcon style={{flexShrink: 0}} />
72
- </Flex>
73
- </Card>
74
-
75
- <Card padding={2} radius={2} tone="inherit">
76
- <Flex align="center" justify="space-between" gap={1}>
77
- {documentId && (
78
- <UserAssignment
79
- userList={userList}
80
- assignees={assignees}
81
- documentId={documentId}
122
+ <>
123
+ {state?.requireValidation ? (
124
+ <Validate
125
+ documentId={documentId}
126
+ type={item._type}
127
+ onChange={handleValidation}
128
+ />
129
+ ) : null}
130
+ <Box paddingBottom={3} paddingX={3}>
131
+ <Card radius={2} shadow={isDragging ? 3 : 1} tone={cardTone}>
132
+ <Stack>
133
+ <Card
134
+ borderBottom
135
+ radius={2}
136
+ padding={3}
137
+ paddingLeft={2}
138
+ tone={cardTone}
139
+ style={{pointerEvents: 'none'}}
140
+ >
141
+ <Flex align="center" justify="space-between" gap={1}>
142
+ <Box flex={1}>
143
+ <Preview
144
+ layout="default"
145
+ skipVisibilityCheck
146
+ value={item}
147
+ schemaType={schema.get(item._type) as SchemaType}
148
+ />
149
+ </Box>
150
+ <Box style={{flexShrink: 0}}>
151
+ {hasError || isDragDisabled || isPatching ? null : (
152
+ <DragHandleIcon />
153
+ )}
154
+ </Box>
155
+ </Flex>
156
+ </Card>
157
+
158
+ <Card padding={2} radius={2} tone="inherit">
159
+ <Flex align="center" justify="space-between" gap={3}>
160
+ <Box flex={1}>
161
+ {documentId && (
162
+ <UserDisplay
163
+ userList={userList}
164
+ assignees={assignees}
165
+ documentId={documentId}
166
+ disabled={!userRoleCanDrop}
167
+ />
168
+ )}
169
+ </Box>
170
+ {validation.length > 0 ? (
171
+ <ValidationStatus validation={validation} />
172
+ ) : null}
173
+ <DraftStatus document={item} />
174
+ <PublishedStatus document={item} />
175
+ <EditButton
176
+ id={item._id}
177
+ type={item._type}
178
+ disabled={!userRoleCanDrop}
82
179
  />
83
- )}
84
-
85
- <EditButton id={item._id} type={item._type} />
86
- </Flex>
87
- </Card>
88
- </Stack>
89
- </Card>
90
- </Box>
180
+ {isLastState && states.length <= 3 ? (
181
+ <CompleteButton
182
+ documentId={documentId}
183
+ disabled={!userRoleCanDrop}
184
+ />
185
+ ) : null}
186
+ </Flex>
187
+ {isLastState && states.length > 3 ? (
188
+ <Stack paddingTop={2}>
189
+ <CompleteButton
190
+ documentId={documentId}
191
+ disabled={!userRoleCanDrop}
192
+ />
193
+ </Stack>
194
+ ) : null}
195
+ </Card>
196
+ </Stack>
197
+ </Card>
198
+ </Box>
199
+ </>
91
200
  )
92
201
  }
@@ -0,0 +1,169 @@
1
+ import {Draggable, DraggableStyle} from '@hello-pangea/dnd'
2
+ import {useVirtualizer, VirtualItem} from '@tanstack/react-virtual'
3
+ import {CSSProperties, useMemo, useRef} from 'react'
4
+ import {CurrentUser} from 'sanity'
5
+ import {UserExtended} from 'sanity-plugin-utils'
6
+
7
+ import {filterItemsAndSort} from '../helpers/filterItemsAndSort'
8
+ import {SanityDocumentWithMetadata, State} from '../types'
9
+ import {DocumentCard} from './DocumentCard'
10
+
11
+ type DocumentListProps = {
12
+ data: SanityDocumentWithMetadata[]
13
+ invalidDocumentIds: string[]
14
+ patchingIds: string[]
15
+ selectedSchemaTypes: string[]
16
+ selectedUserIds: string[]
17
+ state: State
18
+ states: State[]
19
+ toggleInvalidDocumentId: (
20
+ documentId: string,
21
+ action: 'ADD' | 'REMOVE'
22
+ ) => void
23
+ user: CurrentUser | null
24
+ userList: UserExtended[]
25
+ userRoleCanDrop: boolean
26
+ }
27
+
28
+ function getStyle(
29
+ draggableStyle: DraggableStyle | undefined,
30
+ virtualItem: VirtualItem
31
+ ): CSSProperties {
32
+ // Default transform required by tanstack virtual for positioning
33
+ let transform = `translateY(${virtualItem.start}px)`
34
+
35
+ // If a card is being dragged over, this card needs to move up or down
36
+ if (draggableStyle && draggableStyle.transform) {
37
+ // So get the transform value from beautiful-dnd
38
+ const draggableTransformY = parseInt(
39
+ draggableStyle.transform.split(',')[1].split('px')[0],
40
+ 10
41
+ )
42
+
43
+ // And apply it to the card
44
+ transform = `translateY(${virtualItem.start + draggableTransformY}px)`
45
+ }
46
+
47
+ return {
48
+ position: 'absolute',
49
+ top: 0,
50
+ left: 0,
51
+ width: '100%',
52
+ height: `${virtualItem.size}px`,
53
+ transform,
54
+ }
55
+ }
56
+
57
+ export default function DocumentList(props: DocumentListProps) {
58
+ const {
59
+ data = [],
60
+ invalidDocumentIds,
61
+ patchingIds,
62
+ selectedSchemaTypes,
63
+ selectedUserIds,
64
+ state,
65
+ states,
66
+ toggleInvalidDocumentId,
67
+ user,
68
+ userList,
69
+ userRoleCanDrop,
70
+ } = props
71
+
72
+ const dataFiltered = useMemo(() => {
73
+ return data.length
74
+ ? filterItemsAndSort(data, state.id, selectedUserIds, selectedSchemaTypes)
75
+ : []
76
+ }, [data, selectedSchemaTypes, selectedUserIds, state.id])
77
+
78
+ const parentRef = useRef(null)
79
+
80
+ const virtualizer = useVirtualizer({
81
+ count: dataFiltered.length,
82
+ getScrollElement: () => parentRef.current,
83
+ getItemKey: (index) => dataFiltered[index]?._metadata?.documentId ?? index,
84
+ estimateSize: () => 115,
85
+ overscan: 7,
86
+ measureElement: (element) => {
87
+ return element.getBoundingClientRect().height || 115
88
+ },
89
+ })
90
+
91
+ if (!data.length || !dataFiltered.length) {
92
+ return null
93
+ }
94
+
95
+ return (
96
+ <div
97
+ ref={parentRef}
98
+ style={{
99
+ height: `100%`,
100
+ overflow: 'auto',
101
+ // Smooths scrollbar behaviour
102
+ overflowAnchor: 'none',
103
+ scrollBehavior: 'auto',
104
+ paddingTop: 1,
105
+ }}
106
+ >
107
+ <div
108
+ style={{
109
+ height: `${virtualizer.getTotalSize()}px`,
110
+ width: '100%',
111
+ position: 'relative',
112
+ }}
113
+ >
114
+ {virtualizer.getVirtualItems().map((virtualItem) => {
115
+ const item = dataFiltered[virtualItem.index]
116
+
117
+ const {documentId, assignees} = item?._metadata ?? {}
118
+
119
+ const isInvalid = invalidDocumentIds.includes(documentId)
120
+ const meInAssignees = user?.id ? assignees?.includes(user.id) : false
121
+ const isDragDisabled =
122
+ patchingIds.includes(documentId) ||
123
+ !userRoleCanDrop ||
124
+ isInvalid ||
125
+ !(state.requireAssignment
126
+ ? state.requireAssignment && meInAssignees
127
+ : true)
128
+
129
+ return (
130
+ <Draggable
131
+ key={virtualItem.key}
132
+ draggableId={documentId}
133
+ index={virtualItem.index}
134
+ isDragDisabled={isDragDisabled}
135
+ >
136
+ {(draggableProvided, draggableSnapshot) => (
137
+ <div
138
+ ref={draggableProvided.innerRef}
139
+ {...draggableProvided.draggableProps}
140
+ {...draggableProvided.dragHandleProps}
141
+ style={getStyle(
142
+ draggableProvided.draggableProps.style,
143
+ virtualItem
144
+ )}
145
+ >
146
+ <div
147
+ ref={virtualizer.measureElement}
148
+ data-index={virtualItem.index}
149
+ >
150
+ <DocumentCard
151
+ userRoleCanDrop={userRoleCanDrop}
152
+ isDragDisabled={isDragDisabled}
153
+ isPatching={patchingIds.includes(documentId)}
154
+ isDragging={draggableSnapshot.isDragging}
155
+ item={item}
156
+ toggleInvalidDocumentId={toggleInvalidDocumentId}
157
+ userList={userList}
158
+ states={states}
159
+ />
160
+ </div>
161
+ </div>
162
+ )}
163
+ </Draggable>
164
+ )
165
+ })}
166
+ </div>
167
+ </div>
168
+ )
169
+ }
@@ -0,0 +1,168 @@
1
+ import {ResetIcon, UserIcon} from '@sanity/icons'
2
+ import {Button, Card, Flex, Menu, MenuButton} from '@sanity/ui'
3
+ import {useCallback} from 'react'
4
+ import {useCurrentUser, UserAvatar, useSchema} from 'sanity'
5
+ import {UserExtended, UserSelectMenu} from 'sanity-plugin-utils'
6
+
7
+ type FiltersProps = {
8
+ uniqueAssignedUsers: UserExtended[]
9
+ selectedUserIds: string[]
10
+ schemaTypes: string[]
11
+ selectedSchemaTypes: string[]
12
+ toggleSelectedUser: (userId: string) => void
13
+ resetSelectedUsers: () => void
14
+ toggleSelectedSchemaType: (schemaType: string) => void
15
+ }
16
+
17
+ export default function Filters(props: FiltersProps) {
18
+ const {
19
+ uniqueAssignedUsers = [],
20
+ selectedUserIds,
21
+ schemaTypes,
22
+ selectedSchemaTypes,
23
+ toggleSelectedUser,
24
+ resetSelectedUsers,
25
+ toggleSelectedSchemaType,
26
+ } = props
27
+
28
+ const currentUser = useCurrentUser()
29
+ const schema = useSchema()
30
+
31
+ const onAdd = useCallback(
32
+ (id: string) => {
33
+ if (!selectedUserIds.includes(id)) {
34
+ toggleSelectedUser(id)
35
+ }
36
+ },
37
+ [selectedUserIds, toggleSelectedUser]
38
+ )
39
+
40
+ const onRemove = useCallback(
41
+ (id: string) => {
42
+ if (selectedUserIds.includes(id)) {
43
+ toggleSelectedUser(id)
44
+ }
45
+ },
46
+ [selectedUserIds, toggleSelectedUser]
47
+ )
48
+
49
+ const onClear = useCallback(() => {
50
+ resetSelectedUsers()
51
+ }, [resetSelectedUsers])
52
+
53
+ if (uniqueAssignedUsers.length === 0 && schemaTypes.length < 2) {
54
+ return null
55
+ }
56
+
57
+ const meInUniqueAssignees =
58
+ currentUser?.id && uniqueAssignedUsers.find((u) => u.id === currentUser.id)
59
+ const uniqueAssigneesNotMe = uniqueAssignedUsers.filter(
60
+ (u) => u.id !== currentUser?.id
61
+ )
62
+
63
+ return (
64
+ <Card tone="primary" padding={2} borderBottom style={{overflowX: 'hidden'}}>
65
+ <Flex align="center">
66
+ <Flex align="center" gap={1} flex={1}>
67
+ {uniqueAssignedUsers.length > 5 ? (
68
+ <Card tone="default">
69
+ <MenuButton
70
+ button={
71
+ <Button
72
+ text="Filter Assignees"
73
+ tone="primary"
74
+ icon={UserIcon}
75
+ />
76
+ }
77
+ id="user-filters"
78
+ menu={
79
+ <Menu>
80
+ <UserSelectMenu
81
+ value={selectedUserIds}
82
+ userList={uniqueAssignedUsers}
83
+ onAdd={onAdd}
84
+ onRemove={onRemove}
85
+ onClear={onClear}
86
+ labels={{
87
+ addMe: 'Filter mine',
88
+ removeMe: 'Clear mine',
89
+ clear: 'Clear filters',
90
+ }}
91
+ />
92
+ </Menu>
93
+ }
94
+ popover={{portal: true}}
95
+ />
96
+ </Card>
97
+ ) : (
98
+ <>
99
+ {meInUniqueAssignees ? (
100
+ <>
101
+ <Button
102
+ padding={0}
103
+ mode={
104
+ selectedUserIds.includes(currentUser.id)
105
+ ? `default`
106
+ : `bleed`
107
+ }
108
+ onClick={() => toggleSelectedUser(currentUser.id)}
109
+ >
110
+ <Flex padding={1} align="center" justify="center">
111
+ <UserAvatar user={currentUser.id} size={1} withTooltip />
112
+ </Flex>
113
+ </Button>
114
+ <Card borderRight style={{height: 30}} tone="inherit" />
115
+ </>
116
+ ) : null}
117
+ {uniqueAssigneesNotMe.map((user) => (
118
+ <Button
119
+ key={user.id}
120
+ padding={0}
121
+ mode={selectedUserIds.includes(user.id) ? `default` : `bleed`}
122
+ onClick={() => toggleSelectedUser(user.id)}
123
+ >
124
+ <Flex padding={1} align="center" justify="center">
125
+ <UserAvatar user={user} size={1} withTooltip />
126
+ </Flex>
127
+ </Button>
128
+ ))}
129
+
130
+ {selectedUserIds.length > 0 ? (
131
+ <Button
132
+ text="Clear"
133
+ onClick={resetSelectedUsers}
134
+ mode="ghost"
135
+ icon={ResetIcon}
136
+ />
137
+ ) : null}
138
+ </>
139
+ )}
140
+ </Flex>
141
+
142
+ {schemaTypes.length > 1 ? (
143
+ <Flex align="center" gap={1}>
144
+ {schemaTypes.map((typeName) => {
145
+ const schemaType = schema.get(typeName)
146
+
147
+ if (!schemaType) {
148
+ return null
149
+ }
150
+
151
+ return (
152
+ <Button
153
+ key={typeName}
154
+ text={schemaType?.title ?? typeName}
155
+ icon={schemaType?.icon ?? undefined}
156
+ mode={
157
+ selectedSchemaTypes.includes(typeName) ? `default` : `ghost`
158
+ }
159
+ onClick={() => toggleSelectedSchemaType(typeName)}
160
+ />
161
+ )
162
+ })}
163
+ </Flex>
164
+ ) : null}
165
+ </Flex>
166
+ </Card>
167
+ )
168
+ }
@@ -0,0 +1,29 @@
1
+ import {PropsWithChildren} from 'react'
2
+ import styled, {css} from 'styled-components'
3
+ import {Card, Grid} from '@sanity/ui'
4
+ import {motion, AnimatePresence} from 'framer-motion'
5
+
6
+ const StyledFloatingCard = styled(Card)(
7
+ () => css`
8
+ position: fixed;
9
+ bottom: 0;
10
+ left: 0;
11
+ z-index: 1000;
12
+ `
13
+ )
14
+
15
+ export default function FloatingCard({children}: PropsWithChildren) {
16
+ const childrenHaveValues = Array.isArray(children) ? children.some(Boolean) : Boolean(children)
17
+
18
+ return (
19
+ <AnimatePresence>
20
+ {childrenHaveValues ? (
21
+ <motion.div key="floater" initial={{opacity: 0}} animate={{opacity: 1}} exit={{opacity: 0}}>
22
+ <StyledFloatingCard shadow={3} padding={3} margin={3} radius={3}>
23
+ <Grid gap={2}>{children}</Grid>
24
+ </StyledFloatingCard>
25
+ </motion.div>
26
+ ) : null}
27
+ </AnimatePresence>
28
+ )
29
+ }
@@ -0,0 +1,27 @@
1
+ import React from 'react'
2
+ import {Box, Text, Tooltip} from '@sanity/ui'
3
+
4
+ type StatusProps = {
5
+ text: string
6
+ icon: React.ComponentType
7
+ }
8
+
9
+ export function Status(props: StatusProps) {
10
+ const {text, icon} = props
11
+ const Icon = icon
12
+
13
+ return (
14
+ <Tooltip
15
+ portal
16
+ content={
17
+ <Box padding={2}>
18
+ <Text size={1}>{text}</Text>
19
+ </Box>
20
+ }
21
+ >
22
+ <Text size={1}>
23
+ <Icon />
24
+ </Text>
25
+ </Tooltip>
26
+ )
27
+ }