@os-design/core 1.0.199 → 1.0.200

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 (99) hide show
  1. package/package.json +21 -13
  2. package/src/@types/emotion.d.ts +7 -0
  3. package/src/Alert/index.tsx +112 -0
  4. package/src/Avatar/index.tsx +173 -0
  5. package/src/Avatar/utils/nameToInitials.ts +12 -0
  6. package/src/Avatar/utils/strToHue.ts +13 -0
  7. package/src/AvatarSkeleton/index.tsx +29 -0
  8. package/src/Breadcrumb/index.tsx +93 -0
  9. package/src/BreadcrumbItem/index.tsx +83 -0
  10. package/src/Button/ButtonContent.tsx +91 -0
  11. package/src/Button/index.tsx +225 -0
  12. package/src/Button/utils/useButtonColors.ts +84 -0
  13. package/src/Checkbox/index.tsx +225 -0
  14. package/src/CheckboxSkeleton/index.tsx +50 -0
  15. package/src/DatePicker/DatePickerCalendar.tsx +220 -0
  16. package/src/DatePicker/index.tsx +568 -0
  17. package/src/Drawer/index.tsx +212 -0
  18. package/src/Form/FormConfigContext.ts +16 -0
  19. package/src/Form/index.tsx +49 -0
  20. package/src/FormDivider/index.tsx +74 -0
  21. package/src/FormItem/index.tsx +118 -0
  22. package/src/Gallery/Status.tsx +62 -0
  23. package/src/Gallery/index.tsx +290 -0
  24. package/src/GlobalStyles/index.tsx +17 -0
  25. package/src/GlobalStyles/resetStyles.ts +17 -0
  26. package/src/GlobalStyles/typographyStyles.ts +78 -0
  27. package/src/HeaderSkeleton/index.tsx +64 -0
  28. package/src/Image/index.tsx +104 -0
  29. package/src/ImageSkeleton/index.tsx +22 -0
  30. package/src/Input/index.tsx +330 -0
  31. package/src/Input/utils/getFocusableElements.ts +8 -0
  32. package/src/InputNumber/index.tsx +208 -0
  33. package/src/InputNumber/utils/defaultLocale.ts +9 -0
  34. package/src/InputPassword/index.tsx +201 -0
  35. package/src/InputPassword/utils/defaultLocale.ts +11 -0
  36. package/src/InputSearch/index.tsx +111 -0
  37. package/src/InputSearch/utils/defaultLocale.ts +9 -0
  38. package/src/InputSkeleton/index.tsx +28 -0
  39. package/src/Layout/LayoutContext.ts +21 -0
  40. package/src/Layout/index.tsx +44 -0
  41. package/src/Link/index.tsx +129 -0
  42. package/src/LinkButton/index.tsx +100 -0
  43. package/src/List/WindowScroller.tsx +53 -0
  44. package/src/List/index.tsx +255 -0
  45. package/src/List/utils/bodyPointerEvents.ts +24 -0
  46. package/src/List/utils/frameTimeout.ts +36 -0
  47. package/src/List/utils/useRWLoadNext.ts +38 -0
  48. package/src/ListItem/index.tsx +92 -0
  49. package/src/ListItemActions/index.tsx +207 -0
  50. package/src/ListItemLink/index.tsx +63 -0
  51. package/src/ListSkeleton/index.tsx +115 -0
  52. package/src/LogoLink/index.tsx +93 -0
  53. package/src/LogoLink/logo.example.svg +18 -0
  54. package/src/Menu/index.tsx +128 -0
  55. package/src/Menu/utils/useFocusWithArrows.ts +50 -0
  56. package/src/MenuDivider/index.tsx +22 -0
  57. package/src/MenuGroup/index.tsx +190 -0
  58. package/src/MenuItem/index.tsx +108 -0
  59. package/src/Modal/index.tsx +411 -0
  60. package/src/Modal/utils/defaultLocale.ts +9 -0
  61. package/src/Navigation/index.tsx +214 -0
  62. package/src/Navigation/utils/useScrollFlags.ts +39 -0
  63. package/src/NavigationItem/index.tsx +136 -0
  64. package/src/PageContent/index.tsx +99 -0
  65. package/src/PageHeader/index.tsx +246 -0
  66. package/src/PageHeader/utils/defaultLocale.ts +9 -0
  67. package/src/PageHeaderInputSearch/index.tsx +145 -0
  68. package/src/PageHeaderInputSearch/utils/defaultLocale.ts +16 -0
  69. package/src/PageHeaderSkeleton/index.tsx +33 -0
  70. package/src/ParagraphSkeleton/index.tsx +65 -0
  71. package/src/Popover/index.tsx +243 -0
  72. package/src/Popover/utils/usePopoverPosition.ts +216 -0
  73. package/src/Progress/index.tsx +100 -0
  74. package/src/RadioGroup/index.tsx +165 -0
  75. package/src/RadioGroupSkeleton/index.tsx +36 -0
  76. package/src/Result/index.tsx +109 -0
  77. package/src/ScrollButton/index.tsx +159 -0
  78. package/src/ScrollButton/utils/useContainerPosition.ts +41 -0
  79. package/src/ScrollButton/utils/useVisibility.ts +56 -0
  80. package/src/Select/index.tsx +970 -0
  81. package/src/Select/utils/defaultLocale.ts +11 -0
  82. package/src/Skeleton/index.tsx +52 -0
  83. package/src/Switch/index.tsx +217 -0
  84. package/src/SwitchSkeleton/index.tsx +30 -0
  85. package/src/Tag/index.tsx +75 -0
  86. package/src/TagLink/index.tsx +53 -0
  87. package/src/TagList/index.tsx +95 -0
  88. package/src/TagListSkeleton/index.tsx +38 -0
  89. package/src/TagSkeleton/index.tsx +40 -0
  90. package/src/TextArea/index.tsx +231 -0
  91. package/src/TextAreaSkeleton/index.tsx +20 -0
  92. package/src/ThemeSwitcher/index.tsx +39 -0
  93. package/src/TimePicker/index.tsx +142 -0
  94. package/src/Video/index.tsx +41 -0
  95. package/src/index.ts +125 -0
  96. package/src/message/AlertIcon.tsx +50 -0
  97. package/src/message/Message.tsx +108 -0
  98. package/src/message/index.tsx +64 -0
  99. package/src/message/styles.ts +25 -0
@@ -0,0 +1,290 @@
1
+ import styled from '@emotion/styled';
2
+ import { Left, Right } from '@os-design/icons';
3
+ import { ThemeOverrider, clr } from '@os-design/theming';
4
+ import {
5
+ isTouchDevice,
6
+ omitEmotionProps,
7
+ useForwardedRef,
8
+ useSize,
9
+ } from '@os-design/utils';
10
+ import React, {
11
+ MouseEventHandler,
12
+ TouchEventHandler,
13
+ forwardRef,
14
+ useCallback,
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ } from 'react';
20
+ import Button from '../Button';
21
+ import Image, { ImageProps } from '../Image';
22
+ import GalleryStatus from './Status';
23
+
24
+ interface ContainerProps {
25
+ heightPercent: number;
26
+ }
27
+ const Container = styled(
28
+ 'div',
29
+ omitEmotionProps('heightPercent')
30
+ )<ContainerProps>`
31
+ position: relative;
32
+ padding-bottom: ${(p) => p.heightPercent}%;
33
+
34
+ display: flex;
35
+ justify-content: center;
36
+
37
+ background-color: ${(p) => clr(p.theme.galleryColorBg)};
38
+ border-radius: ${(p) => p.theme.borderRadius}em;
39
+ overflow: hidden;
40
+ `;
41
+
42
+ const StyledImage = styled(Image)`
43
+ position: absolute;
44
+ width: auto;
45
+ height: 100%;
46
+ border-radius: 0;
47
+ `;
48
+
49
+ const NavButton = styled(Button)`
50
+ position: absolute;
51
+ top: 50%;
52
+ transform: translateY(-50%);
53
+
54
+ background-color: hsla(0, 0%, 0%, 0.5);
55
+ backdrop-filter: blur(0.2em);
56
+ `;
57
+
58
+ const LeftButton = styled(NavButton)`
59
+ left: 0.2em;
60
+ `;
61
+
62
+ const RightButton = styled(NavButton)`
63
+ right: 0.2em;
64
+ `;
65
+
66
+ type JsxDivProps = Omit<JSX.IntrinsicElements['div'], 'ref'>;
67
+ export interface GalleryProps extends JsxDivProps {
68
+ /**
69
+ * The image urls.
70
+ */
71
+ urls: string[] | ReadonlyArray<string>;
72
+ /**
73
+ * The aspect ratio of the gallery.
74
+ * E.g. [16,9] – 16:9.
75
+ */
76
+ aspectRatio?: [number, number];
77
+ /**
78
+ * The props of the image component.
79
+ */
80
+ imageProps?: Omit<ImageProps, 'url'>;
81
+ /**
82
+ * Whether the navigation buttons is shown.
83
+ */
84
+ hideArrows?: boolean;
85
+ /**
86
+ * The children that can be displayed on top of the image.
87
+ * E.g. tags.
88
+ */
89
+ children?: React.ReactNode;
90
+ }
91
+
92
+ const MIN_DIST_PX = 30;
93
+ const MAX_ANGLE = 30;
94
+
95
+ /**
96
+ * The image gallery. Change the cursor/touch position to change images.
97
+ * The lib 'lazysizes/plugins/attrchange/ls.attrchange' must be imported.
98
+ */
99
+ const Gallery = forwardRef<HTMLDivElement, GalleryProps>(
100
+ (
101
+ {
102
+ urls,
103
+ aspectRatio = [16, 9],
104
+ imageProps = {},
105
+ hideArrows = false,
106
+ children,
107
+ ...rest
108
+ },
109
+ ref
110
+ ) => {
111
+ const [innerContainerRef, mergedContainerRef] = useForwardedRef(ref);
112
+
113
+ const [imageUrl, setImageUrl] = useState<string | undefined>(undefined);
114
+ const [imageIndex, setImageIndex] = useState(urls.length > 0 ? 0 : null);
115
+
116
+ const heightPercent = useMemo(
117
+ () => Math.round((aspectRatio[1] / aspectRatio[0]) * 1000000) / 10000,
118
+ [aspectRatio]
119
+ );
120
+
121
+ const imageIndexRef = useRef(imageIndex);
122
+ useEffect(() => {
123
+ imageIndexRef.current = imageIndex;
124
+ }, [imageIndex]);
125
+
126
+ // Update the image if the index was changed
127
+ useEffect(() => {
128
+ setImageUrl(imageIndex !== null ? urls[imageIndex] : undefined);
129
+ }, [imageIndex, urls]);
130
+
131
+ const startTouchPosRef = useRef<{ x: number; y: number } | null>(null);
132
+
133
+ const size = useSize(innerContainerRef);
134
+ const sizeRef = useRef(size);
135
+ useEffect(() => {
136
+ sizeRef.current = size;
137
+ }, [size]);
138
+
139
+ const statusHeight = useMemo(
140
+ () => Math.round(size.height / 70),
141
+ [size.height]
142
+ );
143
+
144
+ const updateGalleryImage = useCallback(
145
+ (clientX: number) => {
146
+ if (!innerContainerRef.current) return;
147
+ const { x } = innerContainerRef.current.getBoundingClientRect();
148
+ const widthPerImage = sizeRef.current.width / urls.length;
149
+ const xPos = clientX - x;
150
+ if (xPos < 0) return;
151
+ const nextIndex = Math.floor(xPos / widthPerImage);
152
+ if (imageIndexRef.current !== nextIndex) {
153
+ setImageIndex(nextIndex);
154
+ }
155
+ },
156
+ [innerContainerRef, urls.length]
157
+ );
158
+
159
+ const prev = useCallback(() => {
160
+ const index = imageIndexRef.current;
161
+ if (index === null) return;
162
+ setImageIndex(index > 0 ? index - 1 : urls.length - 1);
163
+ }, [urls.length]);
164
+
165
+ const next = useCallback(() => {
166
+ const index = imageIndexRef.current;
167
+ if (index === null) return;
168
+ setImageIndex(index < urls.length - 1 ? index + 1 : 0);
169
+ }, [urls.length]);
170
+
171
+ const left = useCallback<MouseEventHandler>(
172
+ (e) => {
173
+ prev();
174
+ e.preventDefault();
175
+ },
176
+ [prev]
177
+ );
178
+
179
+ const right = useCallback<MouseEventHandler>(
180
+ (e) => {
181
+ next();
182
+ e.preventDefault();
183
+ },
184
+ [next]
185
+ );
186
+
187
+ const mouseMoveHandler = useCallback<MouseEventHandler<HTMLDivElement>>(
188
+ (e) => updateGalleryImage(e.clientX),
189
+ [updateGalleryImage]
190
+ );
191
+
192
+ const touchStartHandler = useCallback<TouchEventHandler<HTMLDivElement>>(
193
+ (e) => {
194
+ const { clientX, clientY } = e.touches[0];
195
+ startTouchPosRef.current = { x: clientX, y: clientY };
196
+ },
197
+ []
198
+ );
199
+
200
+ const touchMoveHandler = useCallback<TouchEventHandler<HTMLDivElement>>(
201
+ (e) => {
202
+ if (!startTouchPosRef.current) return;
203
+ const { x, y } = startTouchPosRef.current;
204
+ const { clientX, clientY } = e.touches[0];
205
+ const diffX = Math.abs(x - clientX);
206
+ const diffY = Math.abs(y - clientY);
207
+ const diff = Math.sqrt(diffX ** 2 + diffY ** 2);
208
+ if (diff < MIN_DIST_PX) return;
209
+ const angle = (Math.atan(diffY / diffX) * 180) / Math.PI;
210
+ if (angle > MAX_ANGLE) {
211
+ startTouchPosRef.current = null;
212
+ return;
213
+ }
214
+ if (x < clientX) prev();
215
+ else next();
216
+ startTouchPosRef.current = null;
217
+ },
218
+ [next, prev]
219
+ );
220
+
221
+ const touchEndHandler = useCallback<
222
+ TouchEventHandler<HTMLDivElement>
223
+ >(() => {
224
+ startTouchPosRef.current = null;
225
+ }, []);
226
+
227
+ const handlers = useMemo(() => {
228
+ if (isTouchDevice()) {
229
+ return {
230
+ onTouchStart: touchStartHandler,
231
+ onTouchMove: touchMoveHandler,
232
+ onTouchEnd: touchEndHandler,
233
+ };
234
+ }
235
+ return {
236
+ onMouseMove: mouseMoveHandler,
237
+ };
238
+ }, [
239
+ mouseMoveHandler,
240
+ touchEndHandler,
241
+ touchMoveHandler,
242
+ touchStartHandler,
243
+ ]);
244
+
245
+ return (
246
+ <ThemeOverrider activeTheme='dark'>
247
+ <Container
248
+ heightPercent={heightPercent}
249
+ {...handlers}
250
+ {...rest}
251
+ ref={mergedContainerRef}
252
+ >
253
+ <StyledImage url={imageUrl} {...imageProps} />
254
+ {urls.length > 1 && imageIndex !== null && (
255
+ <>
256
+ <GalleryStatus
257
+ count={urls.length}
258
+ current={imageIndex}
259
+ height={statusHeight}
260
+ />
261
+ {isTouchDevice() && !hideArrows && (
262
+ <ThemeOverrider overrides={{ colorPrimary: [0, 0, 100] }}>
263
+ <LeftButton
264
+ type='ghost'
265
+ wide='never'
266
+ size='small'
267
+ onClick={left}
268
+ >
269
+ <Left />
270
+ </LeftButton>
271
+ <RightButton
272
+ type='ghost'
273
+ wide='never'
274
+ size='small'
275
+ onClick={right}
276
+ >
277
+ <Right />
278
+ </RightButton>
279
+ </ThemeOverrider>
280
+ )}
281
+ </>
282
+ )}
283
+ {children}
284
+ </Container>
285
+ </ThemeOverrider>
286
+ );
287
+ }
288
+ );
289
+
290
+ export default Gallery;
@@ -0,0 +1,17 @@
1
+ import { Global } from '@emotion/react';
2
+ import { useVh } from '@os-design/utils';
3
+ import React from 'react';
4
+
5
+ import resetStyles from './resetStyles';
6
+ import typographyStyles from './typographyStyles';
7
+
8
+ const GlobalStyles: React.FC = () => {
9
+ useVh();
10
+ return (
11
+ <Global styles={(theme) => [resetStyles(theme), typographyStyles(theme)]} />
12
+ );
13
+ };
14
+
15
+ GlobalStyles.displayName = 'GlobalStyles';
16
+
17
+ export default GlobalStyles;
@@ -0,0 +1,17 @@
1
+ import { css } from '@emotion/react';
2
+ import { SerializedStyles } from '@emotion/serialize';
3
+ import { Theme } from '@os-design/theming';
4
+
5
+ const resetStyles = (theme: Theme): SerializedStyles => css`
6
+ body {
7
+ margin: 0;
8
+ }
9
+
10
+ p,
11
+ figure,
12
+ pre {
13
+ margin: 0 0 ${theme.paragraphMarginBottom}em;
14
+ }
15
+ `;
16
+
17
+ export default resetStyles;
@@ -0,0 +1,78 @@
1
+ import { css } from '@emotion/react';
2
+ import { SerializedStyles } from '@emotion/serialize';
3
+ import { m } from '@os-design/media';
4
+ import { Theme, clr } from '@os-design/theming';
5
+ import fp from 'facepaint';
6
+
7
+ const headingsFontSizeStyles = (fontSize: number[]) => {
8
+ const headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
9
+ return fp(headings, { literal: true })({
10
+ fontSize: fontSize.map((item) => `${item}em`),
11
+ });
12
+ };
13
+
14
+ const headingsMarginTopStyles = (marginTop: number[]) => {
15
+ const headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].map(
16
+ (h) => `${h}:not(:first-of-type)`
17
+ );
18
+ return fp(headings, { literal: true })({
19
+ marginTop: marginTop.map((item) => `${item}em`),
20
+ });
21
+ };
22
+
23
+ const typographyStyles = (theme: Theme): SerializedStyles => css`
24
+ html,
25
+ button,
26
+ input,
27
+ textarea,
28
+ select {
29
+ font-family: 'Helvetica Neue', Helvetica, sans-serif;
30
+ }
31
+
32
+ html,
33
+ input {
34
+ color: ${clr(theme.colorText)};
35
+ }
36
+
37
+ html {
38
+ line-height: ${theme.lineHeight};
39
+
40
+ // Sets the font smoothing
41
+ -webkit-font-smoothing: antialiased;
42
+ -moz-osx-font-smoothing: grayscale;
43
+ text-rendering: optimizeLegibility;
44
+
45
+ // Sets the base font size and increases it on large screens
46
+ ${fp([m.min.md, m.min.xxl])({
47
+ fontSize: theme.fontSize.map((s) => `${s}px`),
48
+ })};
49
+ }
50
+
51
+ body {
52
+ background-color: ${clr(theme.colorBg)};
53
+ }
54
+
55
+ h1,
56
+ h2,
57
+ h3,
58
+ h4,
59
+ h5,
60
+ h6 {
61
+ font-weight: bold;
62
+ line-height: 1.2;
63
+ margin: 0 0 ${theme.headingsMarginBottom}em;
64
+ }
65
+
66
+ // Sets the margin top of headings
67
+ ${headingsMarginTopStyles(theme.headingsMarginTop)};
68
+
69
+ // Sets the base font size of headings
70
+ ${headingsFontSizeStyles(theme.headingsFontSize)};
71
+
72
+ // Sets the font size of headings on large screens
73
+ ${m.min.md} {
74
+ ${headingsFontSizeStyles(theme.headingsFontSizeMd)}
75
+ }
76
+ `;
77
+
78
+ export default typographyStyles;
@@ -0,0 +1,64 @@
1
+ import { css } from '@emotion/react';
2
+
3
+ import styled from '@emotion/styled';
4
+ import { m } from '@os-design/media';
5
+
6
+ import { omitEmotionProps } from '@os-design/utils';
7
+ import React, { forwardRef } from 'react';
8
+
9
+ import Skeleton, { SkeletonProps } from '../Skeleton';
10
+
11
+ export interface HeaderSkeletonProps extends SkeletonProps {
12
+ /**
13
+ * The header type.
14
+ * @default 1
15
+ */
16
+ type?: 1 | 2 | 3 | 4 | 5 | 6;
17
+ /**
18
+ * Whether the header has top and bottom margins.
19
+ * @default false
20
+ */
21
+ hasMargin?: boolean;
22
+ }
23
+
24
+ const hasMarginStyles = (p) =>
25
+ p.hasMargin &&
26
+ css`
27
+ margin: ${p.theme.headingsMarginTop[p.type - 1]}em 0
28
+ ${p.theme.headingsMarginBottom}em;
29
+ `;
30
+
31
+ const MULTIPLIER = 0.9;
32
+
33
+ type StyledHeaderSkeletonProps = Required<
34
+ Pick<HeaderSkeletonProps, 'type' | 'hasMargin'>
35
+ >;
36
+ const StyledHeaderSkeleton = styled(
37
+ Skeleton,
38
+ omitEmotionProps('type', 'hasMargin')
39
+ )<StyledHeaderSkeletonProps>`
40
+ font-size: ${(p) => p.theme.headingsFontSize[p.type - 1] * MULTIPLIER}em;
41
+ ${m.min.md} {
42
+ font-size: ${(p) => p.theme.headingsFontSizeMd[p.type - 1] * MULTIPLIER}em;
43
+ }
44
+ ${hasMarginStyles};
45
+ `;
46
+
47
+ /**
48
+ * Provides a header placeholder while a user waits for the content to load.
49
+ */
50
+ const HeaderSkeleton = forwardRef<HTMLDivElement, HeaderSkeletonProps>(
51
+ ({ type = 1, hasMargin = false, width = '100%', ...rest }, ref) => (
52
+ <StyledHeaderSkeleton
53
+ type={type}
54
+ hasMargin={hasMargin}
55
+ width={width}
56
+ {...rest}
57
+ ref={ref}
58
+ />
59
+ )
60
+ );
61
+
62
+ HeaderSkeleton.displayName = 'HeaderSkeleton';
63
+
64
+ export default HeaderSkeleton;
@@ -0,0 +1,104 @@
1
+ import { css } from '@emotion/react';
2
+
3
+ import styled from '@emotion/styled';
4
+ import { omitEmotionProps } from '@os-design/utils';
5
+ import React, { forwardRef, useCallback } from 'react';
6
+
7
+ const EMPTY_IMAGE =
8
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mO8+R8AArcB2pIvCSwAAAAASUVORK5CYII=';
9
+
10
+ type JsxImgProps = Omit<JSX.IntrinsicElements['img'], 'sizes' | 'ref'>;
11
+ export interface ImageProps extends JsxImgProps {
12
+ /**
13
+ * The image URL.
14
+ * @default undefined
15
+ */
16
+ url?: string;
17
+ /**
18
+ * All available sizes of the image.
19
+ * @default [72, 192, 512, 1024, 2560]
20
+ */
21
+ sizes?: number[];
22
+ /**
23
+ * The image size if the browser does not support lazy loading.
24
+ * @default 72
25
+ */
26
+ defaultSize?: number;
27
+ /**
28
+ * Whether the image is cropped.
29
+ * @default false
30
+ */
31
+ cropped?: boolean;
32
+ /**
33
+ * Sets object-fit: cover.
34
+ * @default false
35
+ */
36
+ cover?: boolean;
37
+ }
38
+
39
+ const coverStyles = (p) =>
40
+ p.cover &&
41
+ css`
42
+ height: 100%;
43
+ object-fit: cover;
44
+ `;
45
+
46
+ type StyledImageProps = Pick<ImageProps, 'cover'>;
47
+ const StyledImage = styled('img', omitEmotionProps('cover'))<StyledImageProps>`
48
+ display: block; // To remove the indent under the image
49
+ width: 100%;
50
+ border-radius: ${(p) => p.theme.borderRadius}em;
51
+ ${coverStyles};
52
+ `;
53
+
54
+ /**
55
+ * The image with lazy loading. Required lazysizes.
56
+ * Should be loaded by @os-team/image-storage.
57
+ */
58
+ const Image = forwardRef<HTMLImageElement, ImageProps>(
59
+ (
60
+ {
61
+ url,
62
+ sizes = [72, 192, 512, 1024, 2560],
63
+ defaultSize = 72,
64
+ cropped = false,
65
+ cover = false,
66
+ className,
67
+ ...rest
68
+ },
69
+ ref
70
+ ) => {
71
+ const getUrl = useCallback(
72
+ (size: number) => `${url}-${size}${cropped ? '-c' : ''}`,
73
+ [url, cropped]
74
+ );
75
+
76
+ if (!url) {
77
+ return (
78
+ <StyledImage
79
+ src={EMPTY_IMAGE}
80
+ className={className}
81
+ {...rest}
82
+ ref={ref}
83
+ />
84
+ );
85
+ }
86
+
87
+ return (
88
+ <StyledImage
89
+ src={getUrl(defaultSize)}
90
+ srcSet={EMPTY_IMAGE}
91
+ data-sizes='auto'
92
+ data-srcset={sizes.map((size) => `${getUrl(size)} ${size}w`).join(', ')}
93
+ cover={cover}
94
+ className={[className, 'lazyload'].filter((i) => i).join(' ')}
95
+ {...rest}
96
+ ref={ref}
97
+ />
98
+ );
99
+ }
100
+ );
101
+
102
+ Image.displayName = 'Image';
103
+
104
+ export default Image;
@@ -0,0 +1,22 @@
1
+ import styled from '@emotion/styled';
2
+
3
+ import React, { forwardRef } from 'react';
4
+
5
+ import Skeleton, { SkeletonProps } from '../Skeleton';
6
+
7
+ export type ImageSkeletonProps = Omit<SkeletonProps, 'width'>;
8
+
9
+ const StyledImageSkeleton = styled(Skeleton)`
10
+ height: 100%;
11
+ `;
12
+
13
+ /**
14
+ * Provides an image placeholder while a user waits for the content to load.
15
+ */
16
+ const ImageSkeleton = forwardRef<HTMLDivElement, ImageSkeletonProps>(
17
+ (props, ref) => <StyledImageSkeleton width='100%' {...props} ref={ref} />
18
+ );
19
+
20
+ ImageSkeleton.displayName = 'ImageSkeleton';
21
+
22
+ export default ImageSkeleton;