@os-design/upload 1.0.202 → 1.0.203

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": "@os-design/upload",
3
- "version": "1.0.202",
3
+ "version": "1.0.203",
4
4
  "license": "UNLICENSED",
5
5
  "repository": "git@gitlab.com:os-team/libs/os-design.git",
6
6
  "main": "dist/cjs/index.js",
@@ -14,7 +14,15 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "src",
19
+ "!**/*.test.ts",
20
+ "!**/*.test.tsx",
21
+ "!**/__tests__",
22
+ "!**/*.stories.tsx",
23
+ "!**/*.stories.mdx",
24
+ "!**/*.example.tsx",
25
+ "!**/*.emotion.d.ts"
18
26
  ],
19
27
  "sideEffects": false,
20
28
  "scripts": {
@@ -29,11 +37,11 @@
29
37
  "access": "public"
30
38
  },
31
39
  "dependencies": {
32
- "@os-design/core": "^1.0.199",
33
- "@os-design/icons": "^1.0.47",
34
- "@os-design/styles": "^1.0.44",
35
- "@os-design/theming": "^1.0.42",
36
- "@os-design/utils": "^1.0.61",
40
+ "@os-design/core": "^1.0.200",
41
+ "@os-design/icons": "^1.0.48",
42
+ "@os-design/styles": "^1.0.45",
43
+ "@os-design/theming": "^1.0.43",
44
+ "@os-design/utils": "^1.0.62",
37
45
  "react-dropzone": "^14.2.3"
38
46
  },
39
47
  "devDependencies": {
@@ -46,5 +54,5 @@
46
54
  "react": ">=18",
47
55
  "react-dom": ">=18"
48
56
  },
49
- "gitHead": "bbd193f118a3128033d4d99ffcb4e96fe06f0dba"
57
+ "gitHead": "3d6b264027712ef81a75379fe3fde3c76c3079af"
50
58
  }
@@ -0,0 +1,7 @@
1
+ import '@emotion/react';
2
+ import { Theme as BaseTheme } from '@os-design/theming';
3
+
4
+ declare module '@emotion/react' {
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
6
+ export interface Theme extends BaseTheme {}
7
+ }
@@ -0,0 +1,305 @@
1
+ import { css } from '@emotion/react';
2
+ import styled from '@emotion/styled';
3
+ import { Button, Image } from '@os-design/core';
4
+ import { Delete, Picture } from '@os-design/icons';
5
+ import {
6
+ WithSize,
7
+ resetFocusStyles,
8
+ sizeStyles,
9
+ transitionStyles,
10
+ } from '@os-design/styles';
11
+ import { ThemeOverrider, clr } from '@os-design/theming';
12
+
13
+ import {
14
+ isTouchDevice,
15
+ omitEmotionProps,
16
+ useForwardedState,
17
+ } from '@os-design/utils';
18
+ import React, { forwardRef, useCallback, useMemo } from 'react';
19
+ import { Accept, useDropzone } from 'react-dropzone';
20
+ import defaultLocale, { ImageUploadLocale } from './utils/defaultLocale';
21
+
22
+ type JsxDivProps = Omit<
23
+ JSX.IntrinsicElements['div'],
24
+ 'defaultValue' | 'onChange' | 'ref'
25
+ >;
26
+ export interface ImageUploadProps extends JsxDivProps, WithSize {
27
+ /**
28
+ * The url of the image.
29
+ * @default undefined
30
+ */
31
+ url?: string;
32
+ /**
33
+ * Allowed image formats.
34
+ * @default undefined
35
+ */
36
+ accept?: Accept;
37
+ /**
38
+ * The locale of the component.
39
+ * @default undefined
40
+ */
41
+ locale?: ImageUploadLocale;
42
+ /**
43
+ * The selected local file, or null if the image is marked as deleted.
44
+ * @default undefined
45
+ */
46
+ value?: File | null;
47
+ /**
48
+ * The default value.
49
+ * @default undefined
50
+ */
51
+ defaultValue?: File | null;
52
+ /**
53
+ * The change event handler.
54
+ * @default undefined
55
+ */
56
+ onChange?: (value: File | null) => void;
57
+ }
58
+
59
+ const overlayHasImageStyles = (p) =>
60
+ p.hasImage &&
61
+ css`
62
+ background-color: hsla(0, 0%, 0%, ${p.theme.imageUploadOverlayOpacity});
63
+ color: hsl(0, 0%, 100%);
64
+ `;
65
+
66
+ const overlayHasImageHoverStyles = (p) =>
67
+ p.hasImage &&
68
+ !p.isDragActive &&
69
+ css`
70
+ @media (hover: hover) {
71
+ opacity: 0;
72
+
73
+ &:hover {
74
+ opacity: 1;
75
+ }
76
+ }
77
+ `;
78
+
79
+ interface OverlayProps {
80
+ isDragActive: boolean;
81
+ hasImage: boolean;
82
+ }
83
+ export const Overlay = styled(
84
+ 'div',
85
+ omitEmotionProps('isDragActive', 'hasImage')
86
+ )<OverlayProps>`
87
+ ${resetFocusStyles};
88
+ position: absolute;
89
+ top: 0;
90
+ right: 0;
91
+ bottom: 0;
92
+ left: 0;
93
+
94
+ display: flex;
95
+ flex-direction: column;
96
+ justify-content: center;
97
+ align-items: center;
98
+
99
+ text-align: center;
100
+ padding: ${(p) => p.theme.imageUploadOverlayPadding}em;
101
+
102
+ ${overlayHasImageStyles};
103
+ ${overlayHasImageHoverStyles}
104
+ ${transitionStyles('opacity')};
105
+ `;
106
+
107
+ const contentIsDragActiveStyles = (p) =>
108
+ p.isDragActive &&
109
+ css`
110
+ transform: scale(0.92);
111
+ `;
112
+
113
+ type ContentProps = Pick<OverlayProps, 'isDragActive'>;
114
+ const Content = styled('div', omitEmotionProps('isDragActive'))<ContentProps>`
115
+ ${contentIsDragActiveStyles};
116
+ ${transitionStyles('transform')};
117
+ `;
118
+
119
+ const PictureIcon = styled(Picture)`
120
+ opacity: 0.8;
121
+ font-size: 2.5em;
122
+ `;
123
+
124
+ const Title = styled.div`
125
+ opacity: 0.8;
126
+ `;
127
+
128
+ const DeleteButtonContainer = styled.div`
129
+ position: absolute;
130
+ top: 0.5em;
131
+ right: 0.5em;
132
+ `;
133
+
134
+ const hoverStyles = (p) => css`
135
+ border-color: ${clr(p.theme.imageUploadNoImageHoverColorBorder)};
136
+ `;
137
+
138
+ const notHasImageStyles = (p) =>
139
+ !p.hasImage &&
140
+ css`
141
+ background-color: ${clr(p.theme.imageUploadNoImageColorBg)};
142
+ border: 2px dashed ${clr(p.theme.imageUploadNoImageColorBorder)};
143
+
144
+ @media (hover: hover) {
145
+ &:hover {
146
+ ${hoverStyles(p)};
147
+ }
148
+ }
149
+ `;
150
+
151
+ const focusStyles = (p) => css`
152
+ &:focus-within {
153
+ & > div {
154
+ opacity: 1;
155
+ }
156
+ ${hoverStyles(p)};
157
+ }
158
+ `;
159
+
160
+ const isDragActiveStyles = (p) =>
161
+ p.isDragActive &&
162
+ css`
163
+ ${hoverStyles(p)};
164
+ `;
165
+
166
+ const LocalImage = styled.img`
167
+ width: 100%;
168
+ vertical-align: top;
169
+ `;
170
+
171
+ const RemoteImage = styled(Image)`
172
+ vertical-align: top;
173
+ `;
174
+
175
+ interface ContainerProps extends WithSize {
176
+ isDragActive: boolean;
177
+ hasImage: boolean;
178
+ }
179
+
180
+ const Container = styled(
181
+ 'div',
182
+ omitEmotionProps('isDragActive', 'hasImage', 'size')
183
+ )<ContainerProps>`
184
+ position: relative;
185
+ cursor: pointer;
186
+ overflow: hidden;
187
+ box-sizing: border-box;
188
+
189
+ max-width: ${(p) => p.theme.imageUploadMaxWidth}em;
190
+ min-height: ${(p) => p.theme.imageUploadMinHeight}em;
191
+ border-radius: ${(p) => p.theme.borderRadius}em;
192
+ color: ${(p) => clr(p.theme.colorText)};
193
+
194
+ ${notHasImageStyles};
195
+ ${focusStyles};
196
+ ${isDragActiveStyles};
197
+ ${sizeStyles};
198
+ ${transitionStyles('border')};
199
+ `;
200
+
201
+ /**
202
+ * The component to upload an image.
203
+ */
204
+ const ImageUpload = forwardRef<HTMLDivElement, ImageUploadProps>(
205
+ (
206
+ {
207
+ url,
208
+ accept = { 'image/*': ['.jpeg', '.png', '.webp'] },
209
+ locale = defaultLocale,
210
+ value,
211
+ defaultValue,
212
+ onChange,
213
+ size,
214
+ ...rest
215
+ },
216
+ ref
217
+ ) => {
218
+ const touchDevice = useMemo(() => isTouchDevice(), []);
219
+ const [forwardedValue, setForwardedValue] = useForwardedState({
220
+ value,
221
+ defaultValue,
222
+ onChange,
223
+ });
224
+
225
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
226
+ accept,
227
+ multiple: false,
228
+ onDrop: ([file]) => setForwardedValue(file),
229
+ });
230
+
231
+ const source = useMemo(() => {
232
+ if (forwardedValue === null) return null; // If the user select an image
233
+ if (forwardedValue) return URL.createObjectURL(forwardedValue); // If the image already exists
234
+ if (url) return url;
235
+ return null;
236
+ }, [forwardedValue, url]);
237
+
238
+ const hasImage = useMemo(() => !!source, [source]);
239
+
240
+ const renderImage = useCallback(() => {
241
+ if (!source) return null;
242
+ if (typeof forwardedValue === 'object' && forwardedValue !== null) {
243
+ return <LocalImage src={source} />;
244
+ }
245
+ return <RemoteImage url={source} />;
246
+ }, [forwardedValue, source]);
247
+
248
+ return (
249
+ <Container
250
+ isDragActive={isDragActive}
251
+ hasImage={hasImage}
252
+ size={size}
253
+ onMouseDown={(e) => e.preventDefault()}
254
+ {...rest}
255
+ ref={ref}
256
+ >
257
+ <input {...getInputProps()} />
258
+ {renderImage()}
259
+
260
+ <Overlay
261
+ isDragActive={isDragActive}
262
+ hasImage={hasImage}
263
+ {
264
+ ...getRootProps({
265
+ onKeyDown: (e) => {
266
+ if (e.key === 'Backspace') setForwardedValue(null);
267
+ },
268
+ } as any) // eslint-disable-line @typescript-eslint/no-explicit-any
269
+ }
270
+ >
271
+ <Content isDragActive={isDragActive}>
272
+ <PictureIcon />
273
+ <Title>{touchDevice ? locale.touchTitle : locale.dropTitle}</Title>
274
+ </Content>
275
+
276
+ {hasImage && (
277
+ <ThemeOverrider
278
+ overrides={{
279
+ buttonGhostColorText: [0, 0, 100],
280
+ buttonGhostColorBgHover: [0, 0, 100, 0.1],
281
+ }}
282
+ >
283
+ <DeleteButtonContainer>
284
+ <Button
285
+ type='ghost'
286
+ wide='never'
287
+ onClick={(e) => {
288
+ setForwardedValue(null);
289
+ e.stopPropagation();
290
+ }}
291
+ >
292
+ <Delete />
293
+ </Button>
294
+ </DeleteButtonContainer>
295
+ </ThemeOverrider>
296
+ )}
297
+ </Overlay>
298
+ </Container>
299
+ );
300
+ }
301
+ );
302
+
303
+ ImageUpload.displayName = 'ImageUpload';
304
+
305
+ export default ImageUpload;
@@ -0,0 +1,11 @@
1
+ export interface ImageUploadLocale {
2
+ touchTitle: string;
3
+ dropTitle: string;
4
+ }
5
+
6
+ const defaultLocale: ImageUploadLocale = {
7
+ touchTitle: 'Click here to choose an image',
8
+ dropTitle: 'Drop an image here or click to choose it',
9
+ };
10
+
11
+ export default defaultLocale;
@@ -0,0 +1,32 @@
1
+ import styled from '@emotion/styled';
2
+ import { Skeleton, SkeletonProps } from '@os-design/core';
3
+
4
+ import { sizeStyles, WithSize } from '@os-design/styles';
5
+ import { omitEmotionProps } from '@os-design/utils';
6
+ import React, { forwardRef } from 'react';
7
+
8
+ export type ImageUploadSkeletonProps = Omit<SkeletonProps, 'width'> & WithSize;
9
+
10
+ const StyledImageUploadSkeleton = styled(
11
+ Skeleton,
12
+ omitEmotionProps('size')
13
+ )<WithSize>`
14
+ max-width: ${(p) => p.theme.imageUploadMaxWidth}em;
15
+ height: ${(p) => p.theme.imageUploadMinHeight}em;
16
+ ${sizeStyles};
17
+ `;
18
+
19
+ /**
20
+ * Provides an image upload placeholder while a user waits for
21
+ * the content to load.
22
+ */
23
+ const ImageUploadSkeleton = forwardRef<
24
+ HTMLDivElement,
25
+ ImageUploadSkeletonProps
26
+ >((props, ref) => (
27
+ <StyledImageUploadSkeleton width='100%' {...props} ref={ref} />
28
+ ));
29
+
30
+ ImageUploadSkeleton.displayName = 'ImageUploadSkeleton';
31
+
32
+ export default ImageUploadSkeleton;
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './ImageUpload';
2
+ export { default as ImageUpload } from './ImageUpload';
3
+ export * from './ImageUploadSkeleton';
4
+ export { default as ImageUploadSkeleton } from './ImageUploadSkeleton';