@sanity/ui 2.14.4 → 2.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.14.4",
3
+ "version": "2.15.0",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -121,7 +121,7 @@
121
121
  "@sanity/icons": "^3.7.0",
122
122
  "csstype": "^3.1.3",
123
123
  "framer-motion": "^12.4.7",
124
- "react-compiler-runtime": "19.0.0-beta-e1e972c-20250221",
124
+ "react-compiler-runtime": "19.0.0-beta-40c6c23-20250301",
125
125
  "react-refractor": "^2.2.0",
126
126
  "use-effect-event": "^1.0.2"
127
127
  },
@@ -135,21 +135,21 @@
135
135
  "@sanity/pkg-utils": "^6.13.4",
136
136
  "@sanity/prettier-config": "^1.0.3",
137
137
  "@sanity/semantic-release-preset": "^5.0.0",
138
- "@sanity/ui-workshop": "^2.0.30",
139
- "@storybook/addon-a11y": "^8.6.2",
140
- "@storybook/addon-docs": "^8.6.2",
141
- "@storybook/addon-essentials": "^8.6.2",
142
- "@storybook/addon-interactions": "^8.6.2",
143
- "@storybook/addon-links": "^8.6.2",
144
- "@storybook/addon-mdx-gfm": "^8.6.2",
145
- "@storybook/addon-storysource": "^8.6.2",
146
- "@storybook/addon-themes": "^8.6.2",
147
- "@storybook/blocks": "^8.6.2",
148
- "@storybook/manager-api": "^8.6.2",
149
- "@storybook/react": "^8.6.2",
150
- "@storybook/react-vite": "^8.6.2",
151
- "@storybook/test": "^8.6.2",
152
- "@storybook/theming": "^8.6.2",
138
+ "@sanity/ui-workshop": "^2.0.31",
139
+ "@storybook/addon-a11y": "^8.6.3",
140
+ "@storybook/addon-docs": "^8.6.3",
141
+ "@storybook/addon-essentials": "^8.6.3",
142
+ "@storybook/addon-interactions": "^8.6.3",
143
+ "@storybook/addon-links": "^8.6.3",
144
+ "@storybook/addon-mdx-gfm": "^8.6.3",
145
+ "@storybook/addon-storysource": "^8.6.3",
146
+ "@storybook/addon-themes": "^8.6.3",
147
+ "@storybook/blocks": "^8.6.3",
148
+ "@storybook/manager-api": "^8.6.3",
149
+ "@storybook/react": "^8.6.3",
150
+ "@storybook/react-vite": "^8.6.3",
151
+ "@storybook/test": "^8.6.3",
152
+ "@storybook/theming": "^8.6.3",
153
153
  "@testing-library/dom": "^10.4.0",
154
154
  "@testing-library/jest-dom": "^6.6.3",
155
155
  "@testing-library/react": "^16.2.0",
@@ -164,7 +164,7 @@
164
164
  "@typescript-eslint/eslint-plugin": "^7.18.0",
165
165
  "@typescript-eslint/parser": "^7.18.0",
166
166
  "@vitejs/plugin-react": "^4.3.4",
167
- "babel-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
167
+ "babel-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
168
168
  "babel-plugin-styled-components": "^2.1.4",
169
169
  "commitizen": "^4.3.1",
170
170
  "cypress": "^13.17.0",
@@ -176,7 +176,7 @@
176
176
  "eslint-plugin-import": "^2.31.0",
177
177
  "eslint-plugin-jsx-a11y": "^6.10.2",
178
178
  "eslint-plugin-react": "^7.37.4",
179
- "eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
179
+ "eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
180
180
  "eslint-plugin-react-hooks": "experimental",
181
181
  "eslint-plugin-storybook": "^0.11.3",
182
182
  "http-server": "^14.1.1",
@@ -187,7 +187,7 @@
187
187
  "lint-staged": "^14.0.1",
188
188
  "module-alias": "^2.2.3",
189
189
  "npm-run-all2": "^5.0.2",
190
- "prettier": "^3.5.2",
190
+ "prettier": "^3.5.3",
191
191
  "react": "^19.0.0",
192
192
  "react-dom": "^19.0.0",
193
193
  "react-is": "^19.0.0",
@@ -195,7 +195,7 @@
195
195
  "rimraf": "^5.0.5",
196
196
  "semantic-release": "^24.2.3",
197
197
  "start-server-and-test": "^2.0.10",
198
- "storybook": "^8.6.2",
198
+ "storybook": "^8.6.3",
199
199
  "styled-components": "^6.1.15",
200
200
  "tsconfig-paths": "^4.2.0",
201
201
  "typescript": "5.7.3",
@@ -1,64 +1,76 @@
1
- import {styled, keyframes, css} from 'styled-components'
1
+ import {styled} from 'styled-components'
2
2
  import {ThemeColorStateToneKey, getTheme_v2} from '../../../theme'
3
- import {POPOVER_MOTION_CONTENT_OPACITY_PROPERTY} from '../../constants'
4
- import {Flex} from '../../primitives'
5
- import {ThemeProps} from '../../styles'
3
+ import {Card, Flex} from '../../primitives'
4
+ import type {ButtonTone} from '../../types'
5
+
6
+ const LOADING_BAR_HEIGHT = 2
7
+
8
+ export const STATUS_CARD_TONE = {
9
+ error: 'critical',
10
+ warning: 'caution',
11
+ success: 'positive',
12
+ info: 'neutral',
13
+ } satisfies {[key: string]: ThemeColorStateToneKey}
14
+
15
+ export const BUTTON_TONE = {
16
+ error: 'critical',
17
+ warning: 'caution',
18
+ success: 'positive',
19
+ info: 'neutral',
20
+ } satisfies {[key: string]: ButtonTone}
6
21
 
7
22
  export const TextBox = styled(Flex)`
8
23
  overflow-x: auto;
9
24
  `
10
25
 
11
- const loadingAnimation = keyframes`
12
- 0% {
13
- width: 0;
14
- }
15
- 100% {
16
- width: 100%;
26
+ export const StyledToast = styled(Card)`
27
+ pointer-events: all;
28
+ width: 100%;
29
+ position: relative;
30
+ overflow: hidden;
31
+ overflow: clip;
32
+
33
+ &[data-has-duration] {
34
+ padding-bottom: calc(${LOADING_BAR_HEIGHT}px / 2);
17
35
  }
18
36
  `
19
37
 
20
- const LOADING_BAR_HEIGHT = 2
21
-
22
- export function rootStyles(
23
- props: {$duration?: number; tone: ThemeColorStateToneKey} & ThemeProps,
24
- ): ReturnType<typeof css> {
25
- const {color} = getTheme_v2(props.theme)
26
-
27
- const loadingBarColor = color.button.default[props.tone].enabled.bg
28
-
29
- if (!props.$duration)
30
- return css`
31
- pointer-events: all;
32
- & > * {
33
- opacity: var(${POPOVER_MOTION_CONTENT_OPACITY_PROPERTY}, 1);
34
- will-change: opacity;
35
- }
36
- `
38
+ export const LoadingBar = styled.div`
39
+ display: flex;
40
+ position: absolute;
41
+ bottom: 0px;
42
+ top: 0px;
43
+ left: 0px;
44
+ right: 0px;
45
+ pointer-events: none;
46
+ z-index: -1;
47
+ overflow: hidden;
48
+ overflow: clip;
49
+ background: transparent;
50
+ align-items: flex-end;
51
+ will-change: opacity;
52
+ `
37
53
 
38
- return css`
39
- pointer-events: all;
40
- width: 100%;
41
- position: relative;
42
- overflow: hidden;
43
- overflow: clip;
44
- padding-bottom: ${LOADING_BAR_HEIGHT}px;
45
- &::before {
46
- content: '';
47
- position: absolute;
48
- bottom: 0px;
49
- height: ${LOADING_BAR_HEIGHT}px;
50
- background: ${loadingBarColor};
51
- animation-name: ${loadingAnimation};
52
- animation-duration: ${props.$duration}ms;
53
- animation-fill-mode: both;
54
- animation-timing-function: linear;
55
- opacity: var(${POPOVER_MOTION_CONTENT_OPACITY_PROPERTY}, 1);
56
- will-change: width;
57
- }
54
+ export const LoadingBarMask = styled(Card)`
55
+ position: absolute;
56
+ top: 0;
57
+ left: -${LOADING_BAR_HEIGHT}px;
58
+ right: -${LOADING_BAR_HEIGHT}px;
59
+ bottom: ${LOADING_BAR_HEIGHT}px;
60
+ z-index: 1;
61
+ `
58
62
 
59
- & > * {
60
- opacity: var(${POPOVER_MOTION_CONTENT_OPACITY_PROPERTY}, 1);
61
- will-change: opacity;
62
- }
63
- `
63
+ type LoadingBarProgressProps = Omit<React.ComponentProps<typeof Card>, 'tone'> & {
64
+ tone: ThemeColorStateToneKey
64
65
  }
66
+ export const LoadingBarProgress = styled<React.ComponentType<LoadingBarProgressProps>>(Card)`
67
+ display: block;
68
+ height: 100%;
69
+ width: 100%;
70
+ transform-origin: 0% 50%;
71
+ background-color: ${(props) => {
72
+ const {color} = getTheme_v2(props.theme)
73
+
74
+ return color.button.default[props.tone].enabled.bg
75
+ }};
76
+ `
@@ -1,10 +1,17 @@
1
1
  import {CloseIcon} from '@sanity/icons'
2
- import {ThemeColorStateToneKey} from '@sanity/ui/theme'
3
- import {styled} from 'styled-components'
4
- import {Box, Button, Flex, Stack, Text, Card} from '../../primitives'
5
- import {ThemeProps} from '../../styles'
6
- import type {ButtonTone} from '../../types'
7
- import {rootStyles, TextBox} from './styles'
2
+ import {motion, type Variant, type Variants} from 'framer-motion'
3
+ import {usePrefersReducedMotion} from '../../hooks/usePrefersReducedMotion'
4
+ import {Box, Button, Flex, Stack, Text} from '../../primitives'
5
+
6
+ import {
7
+ LoadingBar,
8
+ LoadingBarProgress,
9
+ BUTTON_TONE,
10
+ STATUS_CARD_TONE,
11
+ TextBox,
12
+ StyledToast,
13
+ LoadingBarMask,
14
+ } from './styles'
8
15
 
9
16
  /**
10
17
  * @public
@@ -12,27 +19,14 @@ import {rootStyles, TextBox} from './styles'
12
19
  export interface ToastProps {
13
20
  closable?: boolean
14
21
  description?: React.ReactNode
15
- onClose?: () => void
22
+ onClose: () => void
16
23
  radius?: number | number[]
17
24
  title?: React.ReactNode
18
25
  status?: 'error' | 'warning' | 'success' | 'info'
19
26
  duration?: number
27
+ updatedAt?: number
20
28
  }
21
29
 
22
- const STATUS_CARD_TONE: {[key: string]: ThemeColorStateToneKey} = {
23
- error: 'critical',
24
- warning: 'caution',
25
- success: 'positive',
26
- info: 'neutral',
27
- } as const
28
-
29
- const BUTTON_TONE = {
30
- error: 'critical',
31
- warning: 'caution',
32
- success: 'positive',
33
- info: 'neutral',
34
- } satisfies {[key: string]: ButtonTone}
35
-
36
30
  const ROLES = {
37
31
  error: 'alert',
38
32
  warning: 'alert',
@@ -40,9 +34,9 @@ const ROLES = {
40
34
  info: 'alert',
41
35
  } as const
42
36
 
43
- const StyledToast = styled(Card)<{$duration?: number; tone: ThemeColorStateToneKey} & ThemeProps>(
44
- rootStyles,
45
- )
37
+ // Support pattern used by Sanity Studio, that works around the lack of `duration: Infinity` support in older @sanity/ui versions
38
+ // https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
39
+ const LONG_ENOUGH_BUT_NOT_TOO_LONG = 1000 * 60 * 60 * 24 * 24
46
40
 
47
41
  /**
48
42
  * The `Toast` component gives feedback to users when an action has taken place.
@@ -52,25 +46,63 @@ const StyledToast = styled(Card)<{$duration?: number; tone: ThemeColorStateToneK
52
46
  * @public
53
47
  */
54
48
  export function Toast(
55
- props: ToastProps & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'ref' | 'title'>,
49
+ props: ToastProps &
50
+ Omit<
51
+ React.HTMLProps<HTMLDivElement>,
52
+ | 'as'
53
+ | 'height'
54
+ | 'ref'
55
+ | 'title'
56
+ | 'onAnimationStart'
57
+ | 'onDragStart'
58
+ | 'onDragEnd'
59
+ | 'onDrag'
60
+ >,
56
61
  ): React.JSX.Element {
57
- const {closable, description, duration, onClose, radius = 3, title, status, ...restProps} = props
62
+ const {
63
+ closable,
64
+ description,
65
+ duration,
66
+ onClose,
67
+ radius = 3,
68
+ title,
69
+ status,
70
+ updatedAt,
71
+ ...restProps
72
+ } = props
58
73
  const cardTone = status ? STATUS_CARD_TONE[status] : 'default'
59
74
  const buttonTone = status ? BUTTON_TONE[status] : 'default'
60
75
  const role = status ? ROLES[status] : 'status'
61
76
 
77
+ const prefersReducedMotion = usePrefersReducedMotion()
78
+
79
+ const visualDuration: number = prefersReducedMotion ? 0 : 0.26
80
+ const transition = visualDuration ? {type: 'spring', visualDuration, bounce: 0.25} : {duration: 0}
81
+
82
+ const hasDuration = duration && isFinite(duration) && duration < LONG_ENOUGH_BUT_NOT_TOO_LONG
83
+ const initial: ContainerVariants[] = ['hidden', 'initial']
84
+ const animate: ContainerVariants[] = ['visible', 'slideIn']
85
+ const exit: ContainerVariants[] = ['hidden', 'slideOut']
86
+
62
87
  return (
63
- <StyledToast
88
+ <MotionToast
64
89
  data-ui="Toast"
65
90
  role={role}
66
91
  {...restProps}
67
- marginTop={3}
92
+ data-has-duration={hasDuration ? '' : undefined}
93
+ custom={visualDuration}
68
94
  radius={radius}
69
95
  shadow={2}
70
96
  tone={cardTone}
71
- $duration={duration}
97
+ forwardedAs="li"
98
+ layout="position"
99
+ variants={container}
100
+ initial={initial}
101
+ animate={animate}
102
+ exit={exit}
103
+ transition={transition}
72
104
  >
73
- <Flex align="flex-start">
105
+ <MotionFlex align="flex-start" variants={content} transition={transition}>
74
106
  <TextBox flex={1} padding={3}>
75
107
  <Stack space={3}>
76
108
  {title && (
@@ -79,9 +111,9 @@ export function Toast(
79
111
  </Text>
80
112
  )}
81
113
  {description && (
82
- <Text muted size={1}>
114
+ <MotionText muted size={1} variants={content} transition={transition}>
83
115
  {description}
84
- </Text>
116
+ </MotionText>
85
117
  )}
86
118
  </Stack>
87
119
  </TextBox>
@@ -99,9 +131,66 @@ export function Toast(
99
131
  />
100
132
  </Box>
101
133
  )}
102
- </Flex>
103
- </StyledToast>
134
+ </MotionFlex>
135
+ {hasDuration && (
136
+ <MotionLoadingBar variants={content} transition={transition}>
137
+ <LoadingBarMask tone={cardTone} radius={radius} />
138
+ <MotionLoadingBarProgress
139
+ key={`progress-${updatedAt}`}
140
+ tone={cardTone}
141
+ initial={{scaleX: 0}}
142
+ animate={{scaleX: 1}}
143
+ transition={{delay: visualDuration, duration: duration / 1_000, ease: 'linear'}}
144
+ onAnimationComplete={onClose}
145
+ />
146
+ </MotionLoadingBar>
147
+ )}
148
+ </MotionToast>
104
149
  )
105
150
  }
106
151
 
107
152
  Toast.displayName = 'Toast'
153
+
154
+ const container = {
155
+ initial: {y: 32, scale: 0.5, zIndex: 1},
156
+ hidden: {opacity: 0},
157
+ visible: (visualDuration: number) => {
158
+ if (!visualDuration) return {opacity: 1}
159
+
160
+ return {
161
+ opacity: 1,
162
+ transition: {
163
+ when: 'beforeChildren',
164
+ staggerChildren: visualDuration / 3,
165
+ duration: visualDuration / 3,
166
+ },
167
+ }
168
+ },
169
+ slideIn: {
170
+ y: 0,
171
+ scale: 1,
172
+ },
173
+ slideOut: {
174
+ zIndex: 0,
175
+ scale: 0.75,
176
+ },
177
+ } satisfies Variants
178
+ type ContainerVariants = keyof typeof container
179
+
180
+ const content = {
181
+ initial: {
182
+ willChange: 'transform',
183
+ },
184
+ hidden: {
185
+ opacity: 0,
186
+ },
187
+ visible: {
188
+ opacity: 1,
189
+ },
190
+ } satisfies Partial<Record<ContainerVariants, Variant>>
191
+
192
+ const MotionToast = motion.create(StyledToast)
193
+ const MotionFlex = motion.create(Flex)
194
+ const MotionText = motion.create(Text)
195
+ const MotionLoadingBar = motion.create(LoadingBar)
196
+ const MotionLoadingBarProgress = motion.create(LoadingBarProgress)
@@ -0,0 +1,50 @@
1
+ import {styled} from 'styled-components'
2
+ import {Grid} from '../../primitives/grid'
3
+ import {useLayer} from '../../utils'
4
+
5
+ /**
6
+ * @public
7
+ */
8
+ export interface ToastLayerProps {
9
+ children: React.ReactNode
10
+ padding?: number | number[]
11
+ paddingX?: number | number[]
12
+ paddingY?: number | number[]
13
+ gap?: number | number[]
14
+ }
15
+
16
+ /**
17
+ * @internal
18
+ */
19
+ export function ToastLayer(props: ToastLayerProps): React.JSX.Element {
20
+ const {children, padding = 4, paddingX, paddingY, gap = 3} = props
21
+ const {zIndex} = useLayer()
22
+
23
+ return (
24
+ <StyledLayer
25
+ forwardedAs="ul"
26
+ data-ui="ToastProvider"
27
+ padding={padding}
28
+ paddingX={paddingX}
29
+ paddingY={paddingY}
30
+ gap={gap}
31
+ columns={1}
32
+ style={{zIndex}}
33
+ >
34
+ {children}
35
+ </StyledLayer>
36
+ )
37
+ }
38
+
39
+ ToastLayer.displayName = 'ToastLayer'
40
+
41
+ const StyledLayer = styled(Grid)`
42
+ box-sizing: border-box;
43
+ position: fixed;
44
+ right: 0;
45
+ bottom: 0;
46
+ list-style: none;
47
+ pointer-events: none;
48
+ max-width: 420px;
49
+ width: 100%;
50
+ `