@fibery/ui-kit 1.39.0 → 1.40.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +3 -4
  3. package/src/button/select-button.tsx +11 -3
  4. package/src/collapsible/index.tsx +2 -2
  5. package/src/command-menu/index.tsx +4 -2
  6. package/src/copy-to-clipboard.ts +10 -0
  7. package/src/file-item-2.tsx +257 -150
  8. package/src/file-item.tsx +35 -18
  9. package/src/guide-link.tsx +1 -1
  10. package/src/html-styles.ts +2 -12
  11. package/src/icons/ast/FileImage.ts +1 -1
  12. package/src/icons/ast/Mermaid.ts +8 -0
  13. package/src/icons/ast/TextNoWrap.ts +8 -0
  14. package/src/icons/ast/TextWrap.ts +8 -0
  15. package/src/icons/ast/TypeId.ts +8 -0
  16. package/src/icons/ast/index.tsx +4 -0
  17. package/src/icons/react/Mermaid.tsx +13 -0
  18. package/src/icons/react/TextNoWrap.tsx +13 -0
  19. package/src/icons/react/TextWrap.tsx +13 -0
  20. package/src/icons/react/TypeId.tsx +13 -0
  21. package/src/icons/react/index.tsx +4 -0
  22. package/src/icons/svg/file-image.svg +2 -2
  23. package/src/icons/svg/mermaid.svg +3 -0
  24. package/src/icons/svg/text-no-wrap.svg +3 -0
  25. package/src/icons/svg/text-wrap.svg +3 -0
  26. package/src/icons/svg/type/id.svg +3 -0
  27. package/src/images-gallery/images-gallery-fall-through-provider.tsx +2 -1
  28. package/src/images-gallery/images-gallery.tsx +524 -185
  29. package/src/{images-gallery-2 → images-gallery}/slide-buttons.tsx +25 -13
  30. package/src/images-gallery/zoom.tsx +5 -94
  31. package/src/popover/index.tsx +5 -3
  32. package/src/select/components/menu-list-virtualized.tsx +123 -84
  33. package/src/select/components/menu.tsx +5 -3
  34. package/src/table.tsx +13 -10
  35. package/src/toggle-button/toggle-button.tsx +5 -0
  36. package/src/images-gallery-2/images-gallery-2.tsx +0 -522
  37. package/src/images-gallery-2/zoom.tsx +0 -35
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable max-lines */
2
2
  import {css, cx} from "@linaria/core";
3
- import {useCallback, useEffect, useState} from "react";
3
+ import {CSSProperties, useCallback, useEffect, useState} from "react";
4
4
  import {ActionsMenu, ActionsMenuItem, ActionsMenuSeparator} from "./actions-menu";
5
5
  import {ActionsButtonCompact} from "./button/actions-button-compact";
6
6
  import {border, colors, space, themeVars, fontSize, fontWeight, lineHeight} from "./design-system";
@@ -17,10 +17,12 @@ import Link from "./icons/react/Link";
17
17
  import RicheditorOpenLink from "./icons/react/RicheditorOpenLink";
18
18
  import Spinner from "./icons/react/Spinner";
19
19
  import AppWiki from "./icons/react/AppWiki";
20
- import {copyToClipboard} from "./copy-to-clipboard";
20
+ import {copyUrlToClipboard} from "./copy-to-clipboard";
21
21
  import {useToast} from "./toast/toast-provider";
22
22
  import {Icon} from "./icons/Icon";
23
23
  import {useImagesGalleryMethods, useImagesGalleryRegistry} from "./images-gallery/images-gallery";
24
+ import {isPhoneApp} from "./use-is-phone";
25
+ import Pencil from "./icons/react/Pencil";
24
26
 
25
27
  const fileItemStyle = css`
26
28
  display: block;
@@ -29,13 +31,20 @@ const fileItemStyle = css`
29
31
  background-size: cover;
30
32
  text-decoration: none;
31
33
  border-radius: ${space.s8}px;
32
- border: 1px solid ${themeVars.separatorColor};
33
34
  background-clip: padding-box;
34
35
  &:focus {
35
36
  text-decoration: none;
36
37
  }
37
38
  `;
38
39
 
40
+ const thumbnailStyle = css`
41
+ border: 1px solid ${themeVars.separatorColor};
42
+ `;
43
+
44
+ const iconOnlyStyle = css`
45
+ border: 1px solid ${themeVars.colorBorderButtonOutlineNeutralDefault};
46
+ `;
47
+
39
48
  const overlayStyle = css`
40
49
  width: 100%;
41
50
  height: 100%;
@@ -63,7 +72,15 @@ const fileOverlayStyle = css`
63
72
  background: transparent;
64
73
  `;
65
74
 
75
+ const actionsButtonStyle = css`
76
+ position: absolute;
77
+ top: ${space.s8}px;
78
+ right: ${space.s8}px;
79
+ opacity: 0;
80
+ `;
81
+
66
82
  export type FileItemType = {
83
+ uid: string;
67
84
  name: string;
68
85
  url: string;
69
86
  downloadUrl?: string | null;
@@ -79,14 +96,18 @@ export type FileItemType = {
79
96
 
80
97
  export type FileItemProps = {
81
98
  data: FileItemType;
99
+ onDownloadClick?: () => void;
82
100
  onRemoveClick?: (x: FileItemType) => void;
101
+ onRenameClick?: (x: {id: string; name: string}) => void;
83
102
  disabled?: boolean;
84
103
  };
85
104
 
86
105
  export function FileItem(props: FileItemProps) {
87
- const {name, url, thumbnailUrl, status, previewData: preview, contentType} = props.data;
106
+ const {onDownloadClick, onRemoveClick} = props;
107
+ const {name, thumbnailUrl, status, previewData: preview, contentType} = props.data;
88
108
  const isPending = status === "uploading" || status === "deleting";
89
- const isBackgroundReady = Boolean(thumbnailUrl && !isPending);
109
+ const [isThumbnailLoadFailed, setIsThumbnailLoadFailed] = useState(false);
110
+ const isBackgroundReady = !isThumbnailLoadFailed && Boolean(thumbnailUrl && !isPending);
90
111
  const [isActionsOpened, setIsActionsOpened] = useState(false);
91
112
 
92
113
  const showFileIcon = !isPending && !isBackgroundReady;
@@ -103,11 +124,12 @@ export function FileItem(props: FileItemProps) {
103
124
  imagesRegistry.set(preview.originalSrc, {
104
125
  type: preview.type,
105
126
  src: preview.originalSrc,
127
+ label: name,
106
128
  zoomed: false,
107
129
  setZoomed,
108
- actions: <FilePreviewActions url={preview.originalSrc} name={name} />,
130
+ actions: <FilePreviewActions url={preview.originalSrc} name={name} onDownloadClick={onDownloadClick} />,
109
131
  });
110
- }, [imagesRegistry, name, preview]);
132
+ }, [imagesRegistry, name, preview, onDownloadClick]);
111
133
 
112
134
  useEffect(() => {
113
135
  if (!preview) {
@@ -124,79 +146,86 @@ export function FileItem(props: FileItemProps) {
124
146
  imagesRegistry.set(preview.originalSrc, {
125
147
  type: preview.type,
126
148
  src: preview.originalSrc,
149
+ label: name,
127
150
  zoomed,
128
151
  setZoomed,
129
- actions: <FilePreviewActions url={preview.originalSrc} name={name} />,
152
+ actions: <FilePreviewActions url={preview.originalSrc} name={name} onDownloadClick={onDownloadClick} />,
130
153
  });
131
154
  }
132
- }, [galleryMethods, zoomed, preview, imagesRegistry, name]);
155
+ }, [galleryMethods, zoomed, preview, imagesRegistry, name, onDownloadClick]);
133
156
 
134
157
  const onContainerClick = useCallback(() => {
135
158
  if (preview) {
136
159
  setZoomed(true);
137
160
  } else {
138
- stratFileDownload(url, name);
161
+ onDownloadClick?.();
139
162
  }
140
- }, [name, preview, url]);
163
+ }, [preview, onDownloadClick]);
141
164
 
142
165
  return (
143
- <div
144
- className={cx(
145
- css`
146
- flex: 1 0 0;
147
- height: 100%;
148
- `,
149
- isBackgroundReady &&
150
- css`
151
- &:hover .${overlayStyle} {
152
- opacity: 1;
153
- background-position: 0 0;
154
- color: ${themeVars.whiteColor};
155
- background-color: rgba(0, 0, 0, 0.6);
156
- }
157
- `,
158
- isBackgroundReady &&
159
- isActionsOpened &&
160
- css`
161
- .${overlayStyle} {
162
- opacity: 1;
163
- background-position: 0 0;
164
- color: ${themeVars.whiteColor};
165
- background-color: rgba(0, 0, 0, 0.6);
166
- }
167
- `
168
- )}
169
- >
166
+ <>
170
167
  <div
171
- style={isBackgroundReady ? {backgroundImage: `url(${thumbnailUrl})`} : {}}
172
- onClick={onContainerClick}
173
168
  className={cx(
174
169
  css`
175
- flex-shrink: 0;
170
+ flex: 1 0 0;
171
+ height: 100%;
176
172
  `,
177
- fileItemStyle,
178
- isPending &&
173
+ isBackgroundReady &&
179
174
  css`
180
- ${{backgroundColor: themeVars.entityCardSelectedColor}}
175
+ &:hover .${overlayStyle} {
176
+ opacity: 1;
177
+ background-position: 0 0;
178
+ color: ${themeVars.whiteColor};
179
+ background-color: rgba(0, 0, 0, 0.6);
180
+ }
181
+ `,
182
+ isBackgroundReady &&
183
+ isActionsOpened &&
184
+ css`
185
+ .${overlayStyle} {
186
+ opacity: 1;
187
+ background-position: 0 0;
188
+ color: ${themeVars.whiteColor};
189
+ background-color: rgba(0, 0, 0, 0.6);
190
+ }
191
+ `,
192
+ !isBackgroundReady &&
193
+ css`
194
+ &:hover .${overlayStyle} {
195
+ background-color: ${themeVars.colorBgMenuItemHover};
196
+ }
197
+ &:hover .${actionsButtonStyle} {
198
+ opacity: 1;
199
+ }
200
+ `,
201
+ !isBackgroundReady &&
202
+ isActionsOpened &&
203
+ css`
204
+ .${overlayStyle} {
205
+ background-color: ${themeVars.colorBgMenuItemHover};
206
+ }
207
+ .${actionsButtonStyle} {
208
+ opacity: 1;
209
+ }
181
210
  `
182
211
  )}
183
212
  >
184
- {isPending && (
185
- <div
186
- className={css`
187
- display: flex;
188
- flex-direction: column;
189
- justify-content: center; /* centers vertically */
190
- align-items: center; /* centers horizontally */
191
- height: 100%;
192
- width: 100%;
193
- `}
194
- >
195
- <Spinner />
196
- </div>
197
- )}
198
- {showFileIcon && (
199
- <div className={cx(overlayStyle, fileOverlayStyle)}>
213
+ <div
214
+ style={isBackgroundReady ? {backgroundImage: `url(${thumbnailUrl})`} : {}}
215
+ onClick={onContainerClick}
216
+ className={cx(
217
+ css`
218
+ flex-shrink: 0;
219
+ `,
220
+ fileItemStyle,
221
+ showFileIcon ? iconOnlyStyle : thumbnailStyle,
222
+ isPending &&
223
+ css`
224
+ ${{backgroundColor: themeVars.entityCardSelectedColor}}
225
+ `
226
+ )}
227
+ >
228
+ {isPending && (
200
229
  <div
201
230
  className={css`
202
231
  display: flex;
@@ -205,99 +234,122 @@ export function FileItem(props: FileItemProps) {
205
234
  align-items: center; /* centers horizontally */
206
235
  height: 100%;
207
236
  width: 100%;
208
- padding-top: ${space.s1}px;
209
- gap: ${space.s2}px;
210
237
  `}
211
238
  >
212
- <Icon containerSize={40} iconSize={40}>
213
- <FileIcon contentType={contentType} />
214
- </Icon>
239
+ <Spinner />
240
+ </div>
241
+ )}
242
+ {showFileIcon && (
243
+ <div className={cx(overlayStyle, fileOverlayStyle)}>
215
244
  <div
216
245
  className={css`
217
- align-content: center;
218
-
219
246
  display: flex;
220
- align-items: center;
221
- gap: 10px;
222
- flex-shrink: 0;
223
- color: ${themeVars.colorTextMenuItem};
224
- text-overflow: ellipsis;
225
- overflow: hidden;
247
+ flex-direction: column;
248
+ justify-content: center; /* centers vertically */
249
+ align-items: center; /* centers horizontally */
250
+ height: 100%;
251
+ width: 100%;
252
+ padding-top: ${space.s1}px;
253
+ gap: ${space.s2}px;
226
254
  `}
227
255
  >
228
- <FileTitle
229
- title={name}
256
+ <Icon containerSize={40} iconSize={40}>
257
+ <FileIcon contentType={contentType} />
258
+ </Icon>
259
+ <div
230
260
  className={css`
231
- -webkit-line-clamp: 1;
232
- -webkit-box-orient: vertical;
233
- overflow: hidden;
261
+ align-content: center;
262
+
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 10px;
266
+ flex-shrink: 0;
267
+ color: ${themeVars.colorTextMenuItem};
234
268
  text-overflow: ellipsis;
269
+ overflow: hidden;
235
270
  `}
236
- />
271
+ >
272
+ <FileTitle
273
+ title={name}
274
+ className={css`
275
+ -webkit-line-clamp: 1;
276
+ -webkit-box-orient: vertical;
277
+ overflow: hidden;
278
+ text-overflow: ellipsis;
279
+ `}
280
+ />
281
+ </div>
237
282
  </div>
238
- </div>
239
- <div
240
- className={css`
241
- position: absolute;
242
- top: ${space.s8}px;
243
- right: ${space.s8}px;
244
- `}
245
- >
246
- <FileActionsMenu
247
- setIsActionsOpened={setIsActionsOpened}
248
- data={props.data}
249
- disabled={props.disabled}
250
- isBackgroundReady={isBackgroundReady}
251
- onRemoveClick={props.onRemoveClick}
252
- />
253
- </div>
254
- </div>
255
- )}
256
- {!isPending && !showFileIcon && (
257
- <div className={cx(overlayStyle, isBackgroundReady ? imageOverlayStyle : fileOverlayStyle)}>
258
- <div
259
- className={css`
260
- position: relative;
261
- display: flex;
262
- flex-direction: column;
263
- justify-content: space-between; /* Top + bottom placement */
264
- width: 100%;
265
- height: 100%;
266
- overflow: hidden;
267
- box-sizing: border-box;
268
- gap: ${space.s8}px;
269
- `}
270
- >
271
- <div
272
- className={css`
273
- align-self: flex-end;
274
- `}
275
- >
283
+ <div className={actionsButtonStyle}>
276
284
  <FileActionsMenu
277
285
  setIsActionsOpened={setIsActionsOpened}
278
286
  data={props.data}
279
287
  disabled={props.disabled}
280
- isBackgroundReady={isBackgroundReady}
281
- onRemoveClick={props.onRemoveClick}
288
+ onDownloadClick={onDownloadClick}
289
+ onRemoveClick={onRemoveClick}
290
+ onRenameClick={props.onRenameClick}
282
291
  />
283
292
  </div>
293
+ </div>
294
+ )}
295
+ {!isPending && !showFileIcon && (
296
+ <div className={cx(overlayStyle, isBackgroundReady ? imageOverlayStyle : fileOverlayStyle)}>
284
297
  <div
285
298
  className={css`
299
+ position: relative;
286
300
  display: flex;
287
- align-self: flex-start;
288
- align-items: flex-end;
289
- text-overflow: ellipsis;
290
- //overflow: hidden;
301
+ flex-direction: column;
302
+ justify-content: space-between; /* Top + bottom placement */
303
+ width: 100%;
304
+ height: 100%;
305
+ overflow: hidden;
291
306
  box-sizing: border-box;
307
+ gap: ${space.s8}px;
292
308
  `}
293
309
  >
294
- <FileTitle title={name} />
310
+ <div
311
+ className={css`
312
+ align-self: flex-end;
313
+ `}
314
+ >
315
+ <FileActionsMenu
316
+ setIsActionsOpened={setIsActionsOpened}
317
+ data={props.data}
318
+ disabled={props.disabled}
319
+ actionsButtonStyle={
320
+ isBackgroundReady ? makeButtonColors(colors.inversedTextColor, "ghost") : undefined
321
+ }
322
+ onDownloadClick={onDownloadClick}
323
+ onRemoveClick={props.onRemoveClick}
324
+ onRenameClick={props.onRenameClick}
325
+ />
326
+ </div>
327
+ <div
328
+ className={css`
329
+ display: flex;
330
+ align-self: flex-start;
331
+ align-items: flex-end;
332
+ text-overflow: ellipsis;
333
+ //overflow: hidden;
334
+ box-sizing: border-box;
335
+ `}
336
+ >
337
+ <FileTitle title={name} />
338
+ </div>
295
339
  </div>
296
340
  </div>
297
- </div>
298
- )}
341
+ )}
342
+ </div>
343
+ {thumbnailUrl ? (
344
+ <img
345
+ style={{display: "none"}}
346
+ onError={() => setIsThumbnailLoadFailed(true)}
347
+ src={thumbnailUrl}
348
+ alt="invisible"
349
+ />
350
+ ) : null}
299
351
  </div>
300
- </div>
352
+ </>
301
353
  );
302
354
  }
303
355
 
@@ -305,14 +357,18 @@ function FileActionsMenu({
305
357
  data,
306
358
  disabled,
307
359
  setIsActionsOpened,
360
+ onDownloadClick,
308
361
  onRemoveClick,
309
- isBackgroundReady,
362
+ onRenameClick,
363
+ actionsButtonStyle,
310
364
  }: {
311
365
  data: FileItemType;
366
+ onDownloadClick?: () => void;
312
367
  onRemoveClick?: (data: FileItemType) => void;
368
+ onRenameClick?: (data: {id: string; name: string}) => void;
313
369
  disabled?: boolean;
314
370
  setIsActionsOpened: (val: boolean) => void;
315
- isBackgroundReady: boolean;
371
+ actionsButtonStyle?: CSSProperties;
316
372
  }) {
317
373
  const toast = useToast();
318
374
 
@@ -324,14 +380,14 @@ function FileActionsMenu({
324
380
  className={css`
325
381
  z-index: 1;
326
382
  `}
327
- style={isBackgroundReady ? makeButtonColors(colors.inversedTextColor, "ghost") : undefined}
383
+ style={actionsButtonStyle}
328
384
  />
329
385
  }
330
386
  >
331
387
  <ActionsMenuItem
332
388
  Icon={Link}
333
389
  onSelect={() => {
334
- copyToClipboard(`${window.origin}${data.url}`)
390
+ copyUrlToClipboard({url: `${window.origin}${data.url}`, label: data.name})
335
391
  .then(() => toast.success({title: "Link copied to clipboard"}))
336
392
  .catch((error) => toast.error({title: "Unable to copy link to clipboard", subTitle: error.message}));
337
393
  }}
@@ -339,11 +395,27 @@ function FileActionsMenu({
339
395
  Copy file URL
340
396
  </ActionsMenuItem>
341
397
  <a href={data.url} target="_blank" rel="noopener noreferrer">
342
- <ActionsMenuItem Icon={RicheditorOpenLink}>Open in new tab</ActionsMenuItem>
343
- </a>
344
- <a download={data.name} href={`${data.url}?attachment`}>
345
- <ActionsMenuItem Icon={FileDownload}>Download</ActionsMenuItem>
398
+ <ActionsMenuItem Icon={RicheditorOpenLink}>
399
+ {isPhoneApp() ? "Open in browser" : "Open in new tab"}
400
+ </ActionsMenuItem>
346
401
  </a>
402
+ {isPhoneApp() ? (
403
+ <ActionsMenuItem Icon={FileDownload} onSelect={onDownloadClick}>
404
+ Download
405
+ </ActionsMenuItem>
406
+ ) : (
407
+ <a download={data.name} href={`${data.url}?attachment`}>
408
+ <ActionsMenuItem Icon={FileDownload}>Download</ActionsMenuItem>
409
+ </a>
410
+ )}
411
+ {onRenameClick && data.status === "done" ? (
412
+ <>
413
+ <ActionsMenuSeparator />
414
+ <ActionsMenuItem Icon={Pencil} onSelect={() => onRenameClick?.({id: data.uid, name: data.name})}>
415
+ Rename
416
+ </ActionsMenuItem>
417
+ </>
418
+ ) : null}
347
419
  <ActionsMenuSeparator />
348
420
  <ActionsMenuItem Icon={Delete} onSelect={() => onRemoveClick?.(data)} dangerous disabled={disabled}>
349
421
  Delete
@@ -353,6 +425,10 @@ function FileActionsMenu({
353
425
  }
354
426
 
355
427
  function FileTitle({title, className}: {title: string; className?: string}) {
428
+ const parts = title.split(".");
429
+ const ext = parts.length > 1 ? "." + parts[parts.length - 1] : "";
430
+ const name = title.substring(0, title.length - ext.length);
431
+
356
432
  return (
357
433
  <div
358
434
  className={cx(
@@ -368,7 +444,7 @@ function FileTitle({title, className}: {title: string; className?: string}) {
368
444
  overflow: hidden;
369
445
  font-size: ${fontSize.mini}px;
370
446
  font-weight: ${fontWeight.medium};
371
- line-height: ${lineHeight.narrow};
447
+ line-height: ${lineHeight.heading};
372
448
  text-transform: none;
373
449
  text-overflow: ellipsis;
374
450
  -webkit-line-clamp: 1;
@@ -382,12 +458,51 @@ function FileTitle({title, className}: {title: string; className?: string}) {
382
458
  className
383
459
  )}
384
460
  >
385
- {title}
461
+ <div
462
+ className={css`
463
+ display: flex;
464
+ min-width: 0;
465
+ align-items: center;
466
+ max-width: 120px;
467
+ `}
468
+ >
469
+ <span
470
+ className={css`
471
+ white-space: nowrap;
472
+ overflow: hidden;
473
+ text-overflow: ellipsis;
474
+ /* ellipsis shows ONLY if this span overflows */
475
+ min-width: 0;
476
+ `}
477
+ >
478
+ {name}
479
+ </span>
480
+ <span
481
+ className={css`
482
+ flex: 0 0 auto;
483
+ `}
484
+ >
485
+ {ext}
486
+ </span>
487
+ </div>
386
488
  </div>
387
489
  );
388
490
  }
389
491
 
390
- function FilePreviewActions({url, name}: {url: string; name: string}) {
492
+ function FilePreviewActions({url, name, onDownloadClick}: {url: string; name: string; onDownloadClick?: () => void}) {
493
+ if (isPhoneApp()) {
494
+ return (
495
+ <>
496
+ <a href={url} target="_blank" rel="noopener noreferrer">
497
+ <ActionsMenuItem Icon={RicheditorOpenLink}>Open in browser</ActionsMenuItem>
498
+ </a>
499
+ <ActionsMenuItem Icon={FileDownload} onSelect={onDownloadClick}>
500
+ Download
501
+ </ActionsMenuItem>
502
+ </>
503
+ );
504
+ }
505
+
391
506
  return (
392
507
  <>
393
508
  <a href={url} target="_blank" rel="noopener noreferrer">
@@ -400,18 +515,10 @@ function FilePreviewActions({url, name}: {url: string; name: string}) {
400
515
  );
401
516
  }
402
517
 
403
- function stratFileDownload(url: string, filename: string) {
404
- const a = document.createElement("a");
405
- a.href = `${url}?attachment`;
406
- a.download = filename;
407
- a.style.display = "none";
408
-
409
- document.body.appendChild(a);
410
- a.click();
411
- document.body.removeChild(a);
412
- }
413
-
414
- function FileIcon({contentType}: {contentType: string}) {
518
+ function FileIcon({contentType}: {contentType?: string}) {
519
+ if (!contentType) {
520
+ return <AppWiki />;
521
+ }
415
522
  if (contentType.startsWith("image/")) {
416
523
  return <FileImage />;
417
524
  }