@sanity/ui 2.0.0-alpha.1 → 2.0.0-alpha.11

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.0.0-alpha.1",
3
+ "version": "2.0.0-alpha.11",
4
4
  "sideEffects": false,
5
5
  "types": "./dist/index.d.ts",
6
6
  "source": "./src/index.ts",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@floating-ui/react-dom": "2.0.0",
55
- "@sanity/color": "^2.2.5",
55
+ "@sanity/color": "3.0.0-beta.5",
56
56
  "@sanity/icons": "^2.4.1",
57
57
  "csstype": "^3.1.2",
58
58
  "framer-motion": "^10.16.2",
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Left/right border offsets for header + footer components (in px).
3
+ */
4
+ export const BORDER_OFFSET_X: number = 12
@@ -8,12 +8,13 @@ import {
8
8
  isHTMLElement,
9
9
  } from '../../helpers'
10
10
  import {useArrayProp, useClickOutside, useForwardedRef, useGlobalKeyDown} from '../../hooks'
11
- import {Box, Button, Card, Container, Flex, Text} from '../../primitives'
11
+ import {Box, Button, Card, CardProps, Container, Flex, Text} from '../../primitives'
12
12
  import {ResponsivePaddingProps, ResponsiveWidthProps} from '../../primitives/types'
13
13
  import {responsivePaddingStyle, ResponsivePaddingStyleProps} from '../../styles/internal'
14
14
  import {ThemeColorSchemeKey, useTheme} from '../../theme'
15
15
  import {DialogPosition, Radius} from '../../types'
16
16
  import {Layer, LayerProps, Portal, useBoundaryElement, useLayer, usePortal} from '../../utils'
17
+ import {BORDER_OFFSET_X} from './constants'
17
18
  import {
18
19
  dialogStyle,
19
20
  responsiveDialogPositionStyle,
@@ -71,6 +72,15 @@ interface DialogCardProps extends ResponsiveWidthProps {
71
72
  shadow: number | number[]
72
73
  }
73
74
 
75
+ interface DialogHeaderProps extends CardProps {
76
+ $hasScrolledFromTop: boolean
77
+ $isContentScrollable: boolean
78
+ }
79
+ interface DialogFooterProps extends CardProps {
80
+ $hasScrolledToBottom: boolean
81
+ $isContentScrollable: boolean
82
+ }
83
+
74
84
  function isTargetWithinScope(
75
85
  boundaryElement: HTMLElement | null,
76
86
  portalElement: HTMLElement | null,
@@ -118,7 +128,7 @@ const DialogLayout = styled(Flex)`
118
128
  width: 100%;
119
129
  `
120
130
 
121
- const DialogHeader = styled(Card)`
131
+ const DialogHeader = styled(Box)<DialogHeaderProps>`
122
132
  position: relative;
123
133
  z-index: 2;
124
134
 
@@ -126,10 +136,13 @@ const DialogHeader = styled(Card)`
126
136
  content: '';
127
137
  display: block;
128
138
  position: absolute;
129
- left: 0;
130
- right: 0;
139
+ left: ${BORDER_OFFSET_X}px;
140
+ right: ${BORDER_OFFSET_X}px;
131
141
  bottom: -1px;
132
- border-bottom: 1px solid var(--card-hairline-soft-color);
142
+ border-bottom: 1px solid var(--card-border-color);
143
+ width: auto;
144
+ opacity: ${(props) => (props.$isContentScrollable && props.$hasScrolledFromTop ? 1 : 0)};
145
+ transition: opacity 200ms ease-in;
133
146
  }
134
147
  `
135
148
 
@@ -140,10 +153,22 @@ const DialogContent = styled(Box)`
140
153
  outline: none;
141
154
  `
142
155
 
143
- const DialogFooter = styled(Box)`
156
+ const DialogFooter = styled(Box)<DialogFooterProps>`
144
157
  position: relative;
145
158
  z-index: 3;
146
- border-top: 1px solid var(--card-hairline-soft-color);
159
+
160
+ &:before {
161
+ content: '';
162
+ display: block;
163
+ position: absolute;
164
+ left: ${BORDER_OFFSET_X}px;
165
+ right: ${BORDER_OFFSET_X}px;
166
+ top: -1px;
167
+ border-top: 1px solid var(--card-border-color);
168
+ width: auto;
169
+ opacity: ${(props) => (props.$isContentScrollable && !props.$hasScrolledToBottom ? 1 : 0)};
170
+ transition: opacity 200ms ease-in;
171
+ }
147
172
  `
148
173
 
149
174
  const DialogCard = forwardRef(function DialogCard(
@@ -180,6 +205,9 @@ const DialogCard = forwardRef(function DialogCard(
180
205
  const labelId = `${id}_label`
181
206
  const showCloseButton = Boolean(onClose) && hideCloseButton === false
182
207
  const showHeader = Boolean(header) || showCloseButton
208
+ const [isContentScrollable, setIsContentScrollable] = useState(false)
209
+ const [hasScrolledFromTop, setHasScrolledFromTop] = useState(false)
210
+ const [hasScrolledToBottom, setHasScrolledToBottom] = useState(false)
183
211
 
184
212
  useEffect(() => {
185
213
  if (!autoFocus) return
@@ -248,41 +276,75 @@ const DialogCard = forwardRef(function DialogCard(
248
276
  [contentRef],
249
277
  )
250
278
 
279
+ const handleContentUpdate = useCallback(() => {
280
+ if (localContentRef.current) {
281
+ setIsContentScrollable(
282
+ localContentRef.current.clientHeight < localContentRef.current.scrollHeight,
283
+ )
284
+ setHasScrolledFromTop(localContentRef.current.scrollTop > 0)
285
+ setHasScrolledToBottom(
286
+ localContentRef.current.scrollTop >=
287
+ localContentRef.current.scrollHeight - localContentRef.current.clientHeight,
288
+ )
289
+ }
290
+ }, [])
291
+
292
+ useEffect(() => {
293
+ const resizeObserver = new ResizeObserver(handleContentUpdate)
294
+
295
+ if (localContentRef.current) {
296
+ resizeObserver.observe(localContentRef.current)
297
+ }
298
+
299
+ return () => {
300
+ resizeObserver.disconnect()
301
+ }
302
+ }, [contentRef, handleContentUpdate])
303
+
251
304
  return (
252
305
  <DialogContainer data-ui="DialogCard" width={width}>
253
306
  <DialogCardRoot radius={radius} ref={setRef} scheme={scheme} shadow={shadow}>
254
307
  <DialogLayout direction="column">
255
308
  {showHeader && (
256
- <DialogHeader>
257
- <Flex>
258
- <Box flex={1} padding={4}>
309
+ <DialogHeader
310
+ $isContentScrollable={isContentScrollable}
311
+ $hasScrolledFromTop={hasScrolledFromTop}
312
+ padding={3}
313
+ >
314
+ <Flex align="center">
315
+ <Box flex={1} padding={3}>
259
316
  {header && (
260
- <Text id={labelId} weight="semibold">
317
+ <Text id={labelId} size={1} weight="medium">
261
318
  {header}
262
319
  </Text>
263
320
  )}
264
321
  </Box>
265
322
  {showCloseButton && (
266
- <Box padding={2}>
267
- <Button
268
- aria-label="Close dialog"
269
- disabled={!onClose}
270
- icon={CloseIcon}
271
- mode="bleed"
272
- onClick={onClose}
273
- padding={3}
274
- />
275
- </Box>
323
+ <Button
324
+ aria-label="Close dialog"
325
+ disabled={!onClose}
326
+ icon={CloseIcon}
327
+ mode="bleed"
328
+ onClick={onClose}
329
+ padding={3}
330
+ />
276
331
  )}
277
332
  </Flex>
278
333
  </DialogHeader>
279
334
  )}
280
335
 
281
- <DialogContent flex={1} ref={setContentRef} tabIndex={-1}>
336
+ <DialogContent flex={1} ref={setContentRef} tabIndex={-1} onScroll={handleContentUpdate}>
282
337
  {children}
283
338
  </DialogContent>
284
339
 
285
- {footer && <DialogFooter>{footer}</DialogFooter>}
340
+ {footer && (
341
+ <DialogFooter
342
+ $isContentScrollable={isContentScrollable}
343
+ $hasScrolledToBottom={hasScrolledToBottom}
344
+ >
345
+ {footer}
346
+ </DialogFooter>
347
+ )}
286
348
  </DialogLayout>
287
349
  </DialogCardRoot>
288
350
  </DialogContainer>
@@ -4,6 +4,7 @@ import {isValidElementType} from 'react-is'
4
4
  import {useArrayProp} from '../../hooks'
5
5
  import {Box, Flex, Popover, PopoverProps, Text} from '../../primitives'
6
6
  import {Selectable} from '../../primitives/_selectable'
7
+ import {useRootTheme} from '../../theme'
7
8
  import {Radius, SelectableTone} from '../../types'
8
9
  import {Menu} from './menu'
9
10
  import {useMenu} from './useMenu'
@@ -33,7 +34,7 @@ export function MenuGroup(
33
34
  const {
34
35
  as = 'button',
35
36
  children,
36
- fontSize,
37
+ fontSize = 1,
37
38
  icon,
38
39
  onClick,
39
40
  padding = 3,
@@ -45,6 +46,7 @@ export function MenuGroup(
45
46
  ...restProps
46
47
  } = props
47
48
  const menu = useMenu()
49
+ const {scheme} = useRootTheme()
48
50
  const {
49
51
  activeElement,
50
52
  mount,
@@ -166,6 +168,7 @@ export function MenuGroup(
166
168
  data-selected={!withinMenu && active ? '' : undefined}
167
169
  $radius={useArrayProp(radius)}
168
170
  $tone={tone}
171
+ $scheme={scheme}
169
172
  onClick={handleClick}
170
173
  onKeyDown={handleKeyDown}
171
174
  onMouseEnter={handleMouseEnter}
@@ -173,28 +176,24 @@ export function MenuGroup(
173
176
  tabIndex={-1}
174
177
  type={as === 'button' ? 'button' : undefined}
175
178
  >
176
- <Box padding={padding}>
177
- <Flex>
178
- {icon && (
179
- <Text size={fontSize}>
180
- {isValidElement(icon) && icon}
181
- {isValidElementType(icon) && createElement(icon)}
182
- </Text>
183
- )}
184
-
185
- <Box flex={1} marginLeft={icon ? space : undefined}>
186
- <Text size={fontSize} textOverflow="ellipsis">
187
- {text}
188
- </Text>
189
- </Box>
190
-
191
- <Box marginLeft={space}>
192
- <Text size={fontSize}>
193
- <ChevronRightIcon />
194
- </Text>
195
- </Box>
196
- </Flex>
197
- </Box>
179
+ <Flex gap={space} padding={padding}>
180
+ {icon && (
181
+ <Text size={fontSize}>
182
+ {isValidElement(icon) && icon}
183
+ {isValidElementType(icon) && createElement(icon)}
184
+ </Text>
185
+ )}
186
+
187
+ <Box flex={1}>
188
+ <Text size={fontSize} textOverflow="ellipsis" weight="medium">
189
+ {text}
190
+ </Text>
191
+ </Box>
192
+
193
+ <Text size={fontSize}>
194
+ <ChevronRightIcon />
195
+ </Text>
196
+ </Flex>
198
197
  </Selectable>
199
198
  </Popover>
200
199
  )
@@ -12,6 +12,7 @@ import {useArrayProp, useForwardedRef} from '../../hooks'
12
12
  import {Box, Flex, Text} from '../../primitives'
13
13
  import {Selectable} from '../../primitives/_selectable'
14
14
  import {ResponsivePaddingProps, ResponsiveRadiusProps} from '../../primitives/types'
15
+ import {useRootTheme} from '../../theme'
15
16
  import {SelectableTone} from '../../types/selectable'
16
17
  import {Hotkeys} from '../hotkeys'
17
18
  import {useMenu} from './useMenu'
@@ -44,7 +45,7 @@ export const MenuItem = forwardRef(function MenuItem(
44
45
  as = 'button',
45
46
  children,
46
47
  disabled,
47
- fontSize = 2,
48
+ fontSize = 1,
48
49
  hotkeys,
49
50
  icon,
50
51
  iconRight,
@@ -64,6 +65,7 @@ export const MenuItem = forwardRef(function MenuItem(
64
65
  tone = 'default',
65
66
  ...restProps
66
67
  } = props
68
+ const {scheme} = useRootTheme()
67
69
  const menu = useMenu()
68
70
  const {
69
71
  activeElement,
@@ -120,7 +122,8 @@ export const MenuItem = forwardRef(function MenuItem(
120
122
  forwardedAs={as}
121
123
  $radius={useArrayProp(radius)}
122
124
  $padding={useArrayProp(0)}
123
- $tone={tone}
125
+ $tone={disabled ? 'default' : tone}
126
+ $scheme={scheme}
124
127
  disabled={disabled}
125
128
  onClick={handleClick}
126
129
  onMouseEnter={onItemMouseEnter}
@@ -131,43 +134,34 @@ export const MenuItem = forwardRef(function MenuItem(
131
134
  type={as === 'button' ? 'button' : undefined}
132
135
  >
133
136
  {(icon || text || iconRight) && (
134
- <Box as="span" {...paddingProps}>
135
- <Flex as="span">
136
- {icon && (
137
- <Text size={fontSize}>
138
- {isValidElement(icon) && icon}
139
- {isValidElementType(icon) && createElement(icon)}
140
- </Text>
141
- )}
137
+ <Flex as="span" gap={space} align="center" {...paddingProps}>
138
+ {icon && (
139
+ <Text size={fontSize}>
140
+ {isValidElement(icon) && icon}
141
+ {isValidElementType(icon) && createElement(icon)}
142
+ </Text>
143
+ )}
142
144
 
143
- {text && (
144
- <Box
145
- flex={1}
146
- marginLeft={icon ? space : undefined}
147
- marginRight={iconRight ? space : undefined}
148
- >
149
- <Text size={fontSize} textOverflow="ellipsis">
150
- {text}
151
- </Text>
152
- </Box>
153
- )}
145
+ {text && (
146
+ <Box flex={1}>
147
+ <Text size={fontSize} textOverflow="ellipsis" weight="medium">
148
+ {text}
149
+ </Text>
150
+ </Box>
151
+ )}
154
152
 
155
- {hotkeys && (
156
- <Box marginLeft={space} style={{marginTop: -4, marginBottom: -4}}>
157
- <Hotkeys fontSize={fontSize} keys={hotkeys} />
158
- </Box>
159
- )}
153
+ {hotkeys && (
154
+ <Hotkeys fontSize={fontSize} keys={hotkeys} style={{marginTop: -4, marginBottom: -4}} />
155
+ )}
160
156
 
161
- {iconRight && (
162
- <Text size={fontSize}>
163
- {isValidElement(iconRight) && iconRight}
164
- {isValidElementType(iconRight) && createElement(iconRight)}
165
- </Text>
166
- )}
167
- </Flex>
168
- </Box>
157
+ {iconRight && (
158
+ <Text size={fontSize}>
159
+ {isValidElement(iconRight) && iconRight}
160
+ {isValidElementType(iconRight) && createElement(iconRight)}
161
+ </Text>
162
+ )}
163
+ </Flex>
169
164
  )}
170
-
171
165
  {children && (
172
166
  <Box as="span" {...paddingProps}>
173
167
  {children}
@@ -99,7 +99,6 @@ export function selectableColorStyle(
99
99
  &:hover {
100
100
  ${_colorVarsStyle(base, tone.hovered)}
101
101
  }
102
-
103
102
  &:active {
104
103
  ${_colorVarsStyle(base, tone.pressed)}
105
104
  }
@@ -4,7 +4,7 @@ import {useArrayProp} from '../../hooks'
4
4
  import {responsiveRadiusStyle, ResponsiveRadiusStyleProps} from '../../styles/internal'
5
5
  import {BadgeMode, BadgeTone} from '../../types'
6
6
  import {Box, BoxProps} from '../box'
7
- import {Label} from '../label'
7
+ import {Text} from '../text'
8
8
  import {ResponsiveRadiusProps} from '../types'
9
9
  import {badgeStyle} from './styles'
10
10
  import {BadgeStyleProps} from './types'
@@ -33,7 +33,7 @@ export const Badge = forwardRef(function Badge(
33
33
  ) {
34
34
  const {
35
35
  children,
36
- fontSize,
36
+ fontSize = 1,
37
37
  mode = 'default',
38
38
  padding = 1,
39
39
  radius = 2,
@@ -51,7 +51,9 @@ export const Badge = forwardRef(function Badge(
51
51
  padding={useArrayProp(padding)}
52
52
  ref={ref}
53
53
  >
54
- <Label size={fontSize}>{children}</Label>
54
+ <Text size={fontSize} weight="medium">
55
+ {children}
56
+ </Text>
55
57
  </Root>
56
58
  )
57
59
  })
@@ -76,6 +76,7 @@ export const Card = forwardRef(function Card(
76
76
  const rootTheme = useRootTheme()
77
77
  const tone = toneProp === 'inherit' ? rootTheme.tone : toneProp
78
78
 
79
+ // TODO: Consider adding the wrapper approach for nested cards in which the tones are not changing, avoid unnecessary ThemeColorProvider
79
80
  return (
80
81
  <ThemeColorProvider scheme={scheme} tone={tone}>
81
82
  <Root
@@ -60,7 +60,7 @@ export const TextArea = forwardRef(function TextArea(
60
60
  disabled = false,
61
61
  fontSize = 2,
62
62
  padding = 3,
63
- radius = 1,
63
+ radius = 2,
64
64
  weight,
65
65
  ...restProps
66
66
  } = props
@@ -156,7 +156,7 @@ export const TextInput = forwardRef(function TextInput(
156
156
  onClear,
157
157
  padding: paddingProp = 3,
158
158
  prefix,
159
- radius: radiusProp = 1,
159
+ radius: radiusProp = 2,
160
160
  readOnly,
161
161
  space: spaceProp = 3,
162
162
  suffix,
@@ -1,4 +1,4 @@
1
- import {RGB} from '../types'
1
+ import {RGB, RGBA} from '../types'
2
2
 
3
3
  function multiplyChannel(b: number, s: number) {
4
4
  return b * s
@@ -9,7 +9,7 @@ function multiplyChannel(b: number, s: number) {
9
9
  * Source: https://www.w3.org/TR/compositing-1/#blendingmultiply
10
10
  * @internal
11
11
  */
12
- export function multiply(b: RGB, s: RGB): RGB {
12
+ export function multiply(b: RGB | RGBA, s: RGB | RGBA): RGB {
13
13
  return {
14
14
  r: Math.round(clamp(multiplyChannel(b.r / 255, s.r / 255) * 255)),
15
15
  g: Math.round(clamp(multiplyChannel(b.g / 255, s.g / 255) * 255)),
@@ -1,4 +1,4 @@
1
- import {RGB} from '../types'
1
+ import {RGB, RGBA} from '../types'
2
2
 
3
3
  function screenChannel(b: number, s: number) {
4
4
  return b + s - b * s
@@ -9,7 +9,7 @@ function screenChannel(b: number, s: number) {
9
9
  * Source: https://www.w3.org/TR/compositing-1/#blendingscreen
10
10
  * @internal
11
11
  */
12
- export function screen(b: RGB, s: RGB): RGB {
12
+ export function screen(b: RGB | RGBA, s: RGB | RGBA): RGB {
13
13
  return {
14
14
  r: Math.round(clamp(screenChannel(b.r / 255, s.r / 255) * 255)),
15
15
  g: Math.round(clamp(screenChannel(b.g / 255, s.g / 255) * 255)),
@@ -1,4 +1,4 @@
1
- import {HSL, RGB} from './types'
1
+ import {HSL, RGB, RGBA} from './types'
2
2
 
3
3
  /**
4
4
  * @internal
@@ -23,6 +23,20 @@ export function hexToRgb(hex: string): RGB {
23
23
  }
24
24
  }
25
25
 
26
+ /**
27
+ * @internal
28
+ */
29
+ export function rgbaToRGBA(rgba: string): RGBA {
30
+ const values = rgba.replace(/rgba\(|\)/g, '').split(',')
31
+
32
+ return {
33
+ r: parseInt(values[0]),
34
+ g: parseInt(values[1]),
35
+ b: parseInt(values[2]),
36
+ a: parseFloat(values[3]),
37
+ }
38
+ }
39
+
26
40
  /**
27
41
  * @internal
28
42
  */
@@ -1,5 +1,5 @@
1
- import {hexToRgb, hslToRgb} from './convert'
2
- import {HSL, RGB} from './types'
1
+ import {hexToRgb, hslToRgb, rgbaToRGBA} from './convert'
2
+ import {HSL, RGB, RGBA} from './types'
3
3
 
4
4
  const HEX_CHARS = '0123456789ABCDEFabcdef'
5
5
 
@@ -37,7 +37,7 @@ function parseHsl(str: string): HSL {
37
37
  /**
38
38
  * @internal
39
39
  */
40
- export function parseColor(color: unknown): RGB {
40
+ export function parseColor(color: unknown): RGB | RGBA {
41
41
  if (!color) return {r: 0, g: 0, b: 0}
42
42
 
43
43
  if (typeof color !== 'string') {
@@ -52,5 +52,9 @@ export function parseColor(color: unknown): RGB {
52
52
  return hslToRgb(parseHsl(color))
53
53
  }
54
54
 
55
+ if (color.startsWith('rgba(')) {
56
+ return rgbaToRGBA(color)
57
+ }
58
+
55
59
  throw new Error(`parseColor: unexpected color format: "${color}"`)
56
60
  }
@@ -5,6 +5,17 @@ export interface RGB {
5
5
  r: number
6
6
  g: number
7
7
  b: number
8
+ a?: undefined
9
+ }
10
+
11
+ /**
12
+ * @internal
13
+ */
14
+ export interface RGBA {
15
+ r: number
16
+ g: number
17
+ b: number
18
+ a: number
8
19
  }
9
20
 
10
21
  /**
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @internal
3
+ * @deprecated Use studioTheme instead.
4
+ */
5
+ export function createColorTheme(): null {
6
+ return null
7
+ }
@@ -0,0 +1 @@
1
+ export * from './createColorTheme'