@sanity/ui 2.14.2 → 2.14.4-canary.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.2",
3
+ "version": "2.14.4-canary.0",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -61,32 +61,6 @@
61
61
  "_visual-editing.js",
62
62
  "theme.js"
63
63
  ],
64
- "scripts": {
65
- "build": "run-s clean pkg:build pkg:check figma:pkg:build",
66
- "clean": "rimraf .workshop dist",
67
- "commit": "cz",
68
- "cypress:dev": "run-p dev cypress:open",
69
- "cypress:open": "cypress open",
70
- "cypress:run": "cypress run",
71
- "dev": "run-p storybook:dev workshop:dev",
72
- "figma:pkg:build": "cd figma && pnpm build",
73
- "format": "prettier --write --cache --ignore-unknown .",
74
- "lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
75
- "pkg:build": "pkg build --strict",
76
- "pkg:check": "pkg --strict",
77
- "prepare": "husky install",
78
- "prepack": "pnpm build",
79
- "release": "semantic-release",
80
- "storybook:build": "storybook build",
81
- "storybook:dev": "storybook dev -p 6006",
82
- "test": "jest",
83
- "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
84
- "ts:check": "tsc",
85
- "watch": "pkg watch --strict",
86
- "workshop:build": "workshop build",
87
- "workshop:dev": "workshop dev",
88
- "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
89
- },
90
64
  "commitlint": {
91
65
  "extends": [
92
66
  "@commitlint/config-conventional"
@@ -120,7 +94,7 @@
120
94
  "@sanity/color": "^3.0.6",
121
95
  "@sanity/icons": "^3.6.0",
122
96
  "csstype": "^3.1.3",
123
- "framer-motion": "^12.4.3",
97
+ "framer-motion": "^12.4.7",
124
98
  "react-compiler-runtime": "19.0.0-beta-e1e972c-20250221",
125
99
  "react-refractor": "^2.2.0",
126
100
  "use-effect-event": "^1.0.2"
@@ -135,21 +109,21 @@
135
109
  "@sanity/pkg-utils": "^6.13.4",
136
110
  "@sanity/prettier-config": "^1.0.3",
137
111
  "@sanity/semantic-release-preset": "^5.0.0",
138
- "@sanity/ui-workshop": "^2.0.29",
139
- "@storybook/addon-a11y": "^8.5.8",
140
- "@storybook/addon-docs": "^8.5.8",
141
- "@storybook/addon-essentials": "^8.5.8",
142
- "@storybook/addon-interactions": "^8.5.8",
143
- "@storybook/addon-links": "^8.5.8",
144
- "@storybook/addon-mdx-gfm": "^8.5.8",
145
- "@storybook/addon-storysource": "^8.5.8",
146
- "@storybook/addon-themes": "^8.5.8",
147
- "@storybook/blocks": "^8.5.8",
148
- "@storybook/manager-api": "^8.5.8",
149
- "@storybook/react": "^8.5.8",
150
- "@storybook/react-vite": "^8.5.8",
151
- "@storybook/test": "^8.5.8",
152
- "@storybook/theming": "^8.5.8",
112
+ "@sanity/ui-workshop": "^2.0.30",
113
+ "@storybook/addon-a11y": "^8.6.2",
114
+ "@storybook/addon-docs": "^8.6.2",
115
+ "@storybook/addon-essentials": "^8.6.2",
116
+ "@storybook/addon-interactions": "^8.6.2",
117
+ "@storybook/addon-links": "^8.6.2",
118
+ "@storybook/addon-mdx-gfm": "^8.6.2",
119
+ "@storybook/addon-storysource": "^8.6.2",
120
+ "@storybook/addon-themes": "^8.6.2",
121
+ "@storybook/blocks": "^8.6.2",
122
+ "@storybook/manager-api": "^8.6.2",
123
+ "@storybook/react": "^8.6.2",
124
+ "@storybook/react-vite": "^8.6.2",
125
+ "@storybook/test": "^8.6.2",
126
+ "@storybook/theming": "^8.6.2",
153
127
  "@testing-library/dom": "^10.4.0",
154
128
  "@testing-library/jest-dom": "^6.6.3",
155
129
  "@testing-library/react": "^16.2.0",
@@ -171,7 +145,7 @@
171
145
  "cypress-real-events": "^1.14.0",
172
146
  "cz-conventional-changelog": "^3.3.0",
173
147
  "eslint": "^8.57.1",
174
- "eslint-config-prettier": "^10.0.1",
148
+ "eslint-config-prettier": "^10.0.2",
175
149
  "eslint-plugin-boundaries": "^5.0.1",
176
150
  "eslint-plugin-import": "^2.31.0",
177
151
  "eslint-plugin-jsx-a11y": "^6.10.2",
@@ -195,7 +169,7 @@
195
169
  "rimraf": "^5.0.5",
196
170
  "semantic-release": "^24.2.3",
197
171
  "start-server-and-test": "^2.0.10",
198
- "storybook": "^8.5.8",
172
+ "storybook": "^8.6.2",
199
173
  "styled-components": "^6.1.15",
200
174
  "tsconfig-paths": "^4.2.0",
201
175
  "typescript": "5.7.3",
@@ -208,25 +182,37 @@
208
182
  "react-is": "^18 || >=19.0.0-0",
209
183
  "styled-components": "^5.2 || ^6"
210
184
  },
211
- "packageManager": "pnpm@9.15.5",
212
185
  "engines": {
213
186
  "node": ">=14.0.0"
214
187
  },
215
188
  "publishConfig": {
216
189
  "access": "public"
217
190
  },
218
- "pnpm": {
219
- "overrides": {
220
- "@types/react": "$@types/react",
221
- "@types/react-dom": "$@types/react-dom",
222
- "@types/react-is": "$@types/react-is",
223
- "conventional-changelog-conventionalcommits": ">= 8.0.0",
224
- "react": "$react",
225
- "react-dom": "$react-dom",
226
- "react-is": "$react-is"
227
- }
228
- },
229
191
  "esm.sh": {
230
192
  "bundle": false
193
+ },
194
+ "scripts": {
195
+ "build": "run-s clean pkg:build pkg:check figma:pkg:build",
196
+ "clean": "rimraf .workshop dist",
197
+ "commit": "cz",
198
+ "cypress:dev": "run-p dev cypress:open",
199
+ "cypress:open": "cypress open",
200
+ "cypress:run": "cypress run",
201
+ "dev": "run-p storybook:dev workshop:dev",
202
+ "figma:pkg:build": "cd figma && pnpm build",
203
+ "format": "prettier --write --cache --ignore-unknown .",
204
+ "lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
205
+ "pkg:build": "pkg build --strict",
206
+ "pkg:check": "pkg --strict",
207
+ "release": "semantic-release",
208
+ "storybook:build": "storybook build",
209
+ "storybook:dev": "storybook dev -p 6006",
210
+ "test": "jest",
211
+ "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
212
+ "ts:check": "tsc",
213
+ "watch": "pkg watch --strict",
214
+ "workshop:build": "workshop build",
215
+ "workshop:dev": "workshop dev",
216
+ "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
231
217
  }
232
- }
218
+ }
@@ -1,64 +1,68 @@
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
+ will-change: opacity, transform;
33
+
34
+ &[data-has-duration] {
35
+ padding-bottom: calc(${LOADING_BAR_HEIGHT}px / 2);
17
36
  }
18
37
  `
19
38
 
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
- `
37
-
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
- }
39
+ export const LoadingBar = styled.div`
40
+ display: flex;
41
+ position: absolute;
42
+ bottom: 0px;
43
+ top: 0px;
44
+ left: 0px;
45
+ right: 0px;
46
+ pointer-events: none;
47
+ z-index: -1;
48
+ overflow: hidden;
49
+ overflow: clip;
50
+ background: transparent;
51
+ align-items: flex-end;
52
+ will-change: opacity;
53
+ `
58
54
 
59
- & > * {
60
- opacity: var(${POPOVER_MOTION_CONTENT_OPACITY_PROPERTY}, 1);
61
- will-change: opacity;
62
- }
63
- `
55
+ type LoadingBarProgressProps = Omit<React.ComponentProps<typeof Card>, 'tone'> & {
56
+ tone: ThemeColorStateToneKey
64
57
  }
58
+ export const LoadingBarProgress = styled<React.ComponentType<LoadingBarProgressProps>>(Card)`
59
+ display: block;
60
+ height: 100%;
61
+ width: 100%;
62
+ transform-origin: 0% 50%;
63
+ background-color: ${(props) => {
64
+ const {color} = getTheme_v2(props.theme)
65
+
66
+ return color.button.default[props.tone].enabled.bg
67
+ }};
68
+ `
@@ -1,10 +1,16 @@
1
1
  import {CloseIcon} from '@sanity/icons'
2
- import {ThemeColorStateToneKey} from '@sanity/ui/theme'
3
- import {styled} from 'styled-components'
2
+ import {motion, type Variants} from 'framer-motion'
3
+ import {usePrefersReducedMotion} from '../../hooks/usePrefersReducedMotion'
4
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'
5
+
6
+ import {
7
+ LoadingBar,
8
+ LoadingBarProgress,
9
+ BUTTON_TONE,
10
+ STATUS_CARD_TONE,
11
+ TextBox,
12
+ StyledToast,
13
+ } from './styles'
8
14
 
9
15
  /**
10
16
  * @public
@@ -12,27 +18,14 @@ import {rootStyles, TextBox} from './styles'
12
18
  export interface ToastProps {
13
19
  closable?: boolean
14
20
  description?: React.ReactNode
15
- onClose?: () => void
21
+ onClose: () => void
16
22
  radius?: number | number[]
17
23
  title?: React.ReactNode
18
24
  status?: 'error' | 'warning' | 'success' | 'info'
19
25
  duration?: number
26
+ updatedAt?: number
20
27
  }
21
28
 
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
29
  const ROLES = {
37
30
  error: 'alert',
38
31
  warning: 'alert',
@@ -40,10 +33,6 @@ const ROLES = {
40
33
  info: 'alert',
41
34
  } as const
42
35
 
43
- const StyledToast = styled(Card)<{$duration?: number; tone: ThemeColorStateToneKey} & ThemeProps>(
44
- rootStyles,
45
- )
46
-
47
36
  /**
48
37
  * The `Toast` component gives feedback to users when an action has taken place.
49
38
  *
@@ -52,25 +41,62 @@ const StyledToast = styled(Card)<{$duration?: number; tone: ThemeColorStateToneK
52
41
  * @public
53
42
  */
54
43
  export function Toast(
55
- props: ToastProps & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'ref' | 'title'>,
44
+ props: ToastProps &
45
+ Omit<
46
+ React.HTMLProps<HTMLDivElement>,
47
+ | 'as'
48
+ | 'height'
49
+ | 'ref'
50
+ | 'title'
51
+ | 'onAnimationStart'
52
+ | 'onDragStart'
53
+ | 'onDragEnd'
54
+ | 'onDrag'
55
+ >,
56
56
  ): React.JSX.Element {
57
- const {closable, description, duration, onClose, radius = 3, title, status, ...restProps} = props
57
+ const {
58
+ closable,
59
+ description,
60
+ duration,
61
+ onClose,
62
+ radius = 3,
63
+ title,
64
+ status,
65
+ updatedAt,
66
+ ...restProps
67
+ } = props
58
68
  const cardTone = status ? STATUS_CARD_TONE[status] : 'default'
59
69
  const buttonTone = status ? BUTTON_TONE[status] : 'default'
60
70
  const role = status ? ROLES[status] : 'status'
61
71
 
72
+ const prefersReducedMotion = usePrefersReducedMotion()
73
+
74
+ // eslint-disable-next-line prefer-const
75
+ let visualDuration = prefersReducedMotion ? 0 : 0.26
76
+ // visualDuration = 5
77
+ const transition = visualDuration ? {type: 'spring', visualDuration, bounce: 0.25} : {duration: 0}
78
+
79
+ const hasDuration = duration && isFinite(duration)
80
+
62
81
  return (
63
- <StyledToast
82
+ <MotionToast
64
83
  data-ui="Toast"
65
84
  role={role}
66
85
  {...restProps}
67
- marginTop={3}
86
+ data-has-duration={hasDuration ? '' : undefined}
87
+ custom={visualDuration satisfies number}
68
88
  radius={radius}
69
89
  shadow={2}
70
90
  tone={cardTone}
71
- $duration={duration}
91
+ forwardedAs="li"
92
+ layout="position"
93
+ variants={container}
94
+ initial={['hidden', 'initial'] satisfies ContainerVariants[]}
95
+ animate={['visible', 'slideIn'] satisfies ContainerVariants[]}
96
+ exit={['hidden', 'slideOut'] satisfies ContainerVariants[]}
97
+ transition={transition}
72
98
  >
73
- <Flex align="flex-start">
99
+ <MotionFlex align="flex-start" variants={content} transition={transition}>
74
100
  <TextBox flex={1} padding={3}>
75
101
  <Stack space={3}>
76
102
  {title && (
@@ -79,9 +105,9 @@ export function Toast(
79
105
  </Text>
80
106
  )}
81
107
  {description && (
82
- <Text muted size={1}>
108
+ <MotionText muted size={1} variants={content} transition={transition}>
83
109
  {description}
84
- </Text>
110
+ </MotionText>
85
111
  )}
86
112
  </Stack>
87
113
  </TextBox>
@@ -99,9 +125,76 @@ export function Toast(
99
125
  />
100
126
  </Box>
101
127
  )}
102
- </Flex>
103
- </StyledToast>
128
+ </MotionFlex>
129
+ {hasDuration && (
130
+ <MotionLoadiongBar variants={content} transition={transition}>
131
+ <Card
132
+ tone={cardTone}
133
+ radius={radius}
134
+ style={{
135
+ position: 'absolute',
136
+ right: -2,
137
+ left: -2,
138
+ top: 0,
139
+ bottom: 2,
140
+ zIndex: 1,
141
+ }}
142
+ />
143
+ <MotionLoadiongBarProgress
144
+ key={`${duration}-${updatedAt}`}
145
+ tone={cardTone}
146
+ initial={{scaleX: 0}}
147
+ animate={{scaleX: 1}}
148
+ transition={{delay: visualDuration, duration: duration / 1_000, ease: 'linear'}}
149
+ onAnimationComplete={onClose}
150
+ />
151
+ </MotionLoadiongBar>
152
+ )}
153
+ </MotionToast>
104
154
  )
105
155
  }
106
156
 
107
157
  Toast.displayName = 'Toast'
158
+
159
+ const container = {
160
+ initial: {y: 32, scale: 0.5, zIndex: 1},
161
+ hidden: {opacity: 0},
162
+ visible: (visualDuration: number) => {
163
+ if (!visualDuration) return {opacity: 1}
164
+
165
+ return {
166
+ opacity: 1,
167
+ transition: {
168
+ when: 'beforeChildren',
169
+ staggerChildren: visualDuration / 3,
170
+ duration: visualDuration / 3,
171
+ },
172
+ }
173
+ },
174
+ slideIn: {
175
+ y: 0,
176
+ scale: 1,
177
+ },
178
+ slideOut: {
179
+ zIndex: 0,
180
+ scale: 0.75,
181
+ },
182
+ } satisfies Variants
183
+ type ContainerVariants = keyof typeof container
184
+
185
+ const content = {
186
+ hidden: {
187
+ opacity: 0,
188
+ },
189
+ visible: {
190
+ opacity: 1,
191
+ },
192
+ } satisfies Variants
193
+
194
+ const MotionToast = motion.create(StyledToast)
195
+ const MotionFlex = motion.create(Flex)
196
+
197
+ const MotionText = motion.create(Text)
198
+
199
+ const MotionLoadiongBar = motion.create(LoadingBar)
200
+ const MotionLoadiongBarProgress = 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
+ `