@fibery/ui-kit 4.1.0 → 4.2.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 (49) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +3 -5
  3. package/src/antd/ant-upload.tsx +13 -13
  4. package/src/antd/auto-complete.tsx +10 -10
  5. package/src/antd/input-number.tsx +55 -45
  6. package/src/antd/styles.ts +28 -22
  7. package/src/antd/tabs.tsx +28 -28
  8. package/src/color-filter.ts +1 -1
  9. package/src/comment.tsx +2 -2
  10. package/src/count-badge.tsx +3 -11
  11. package/src/design-system/colors.ts +57 -37
  12. package/src/design-system/def.ts +1 -1
  13. package/src/dom-utils.ts +5 -12
  14. package/src/download-file.ts +36 -0
  15. package/src/dropdown-menu/index.tsx +0 -1
  16. package/src/file-item/file-menu-items.tsx +29 -11
  17. package/src/file-item/file-preview-actions.tsx +52 -11
  18. package/src/file-item/share-file.ts +53 -0
  19. package/src/file-item.tsx +9 -9
  20. package/src/html-styles.ts +0 -1
  21. package/src/icons/ast/gauge.ts +12 -0
  22. package/src/icons/ast/index.tsx +3 -1
  23. package/src/icons/ast/people-edit.ts +12 -0
  24. package/src/icons/react/gauge.tsx +18 -0
  25. package/src/icons/react/index.tsx +3 -1
  26. package/src/icons/react/people-edit.tsx +18 -0
  27. package/src/icons/svg/gauge.svg +5 -0
  28. package/src/icons/svg/people-edit.svg +4 -0
  29. package/src/images-gallery/images-gallery.tsx +0 -1
  30. package/src/images-gallery/slide-buttons.tsx +49 -16
  31. package/src/is-in-popup.ts +0 -3
  32. package/src/loading-sausage.tsx +3 -3
  33. package/src/palettes/diff-colors.test.ts +1 -1
  34. package/src/palettes/inspect.defs.colors.neutral-arch.test.ts +94 -26
  35. package/src/palettes/inspect.defs.colors.neutral-user.test.ts +94 -26
  36. package/src/palettes/inspect.defs.colors.warm-arch.test.ts +94 -26
  37. package/src/palettes/inspect.defs.colors.warm-user.test.ts +94 -26
  38. package/src/palettes/show-colors.test.ts +2 -1
  39. package/src/palettes/testkit.ts +3 -1
  40. package/src/popover/index.tsx +20 -11
  41. package/src/popover/popup-modifiers-context.ts +1 -2
  42. package/src/select/components/option.tsx +0 -1
  43. package/src/select/index.tsx +1 -0
  44. package/src/select/select-in-popover.tsx +1 -3
  45. package/src/select/select.tsx +2 -0
  46. package/src/thematic.tsx +0 -1
  47. package/src/theme-provider.test.tsx +1 -1
  48. package/src/theming.generated.css +110 -50
  49. package/src/theming.generated.ts +127 -50
package/src/comment.tsx CHANGED
@@ -89,9 +89,9 @@ export const actionsClassName = css`
89
89
  right: 0;
90
90
  z-index: 1;
91
91
  padding: ${space.s2 + 1}px;
92
- background-color: ${themeVars.actionMenuBg};
92
+ background-color: ${themeVars.colorBgFloatingPanel};
93
93
  border-radius: ${border.radius8}px;
94
- box-shadow: ${themeVars.entityCardShadow};
94
+ box-shadow: ${themeVars.shadowFloatingPanel};
95
95
  gap: ${space.s2}px;
96
96
 
97
97
  .${defaultCommentStyle}:hover & {
@@ -7,6 +7,7 @@ import {fontWeight, space, themeVars} from "./design-system";
7
7
  type Props = {
8
8
  className?: string;
9
9
  count: number;
10
+ limit?: number;
10
11
  };
11
12
 
12
13
  const countStyle = css`
@@ -34,17 +35,8 @@ const warningStyle = css`
34
35
  color: ${themeVars.textColor};
35
36
  `;
36
37
 
37
- const countBadgeLimitOverride = Number.parseInt(
38
- new URLSearchParams(window.location.search).get("count-badge-limit") ?? ""
39
- );
40
- const COUNT_BADGE_LIMIT = Number.isInteger(countBadgeLimitOverride) ? countBadgeLimitOverride : 99;
41
-
42
- export function CountBadge({className, count}: Props) {
43
- return (
44
- <span className={cx(countStyle, countBadgeStyle, className)}>
45
- {count <= COUNT_BADGE_LIMIT ? count : `${COUNT_BADGE_LIMIT}+`}
46
- </span>
47
- );
38
+ export function CountBadge({className, count, limit = 99}: Props) {
39
+ return <span className={cx(countStyle, countBadgeStyle, className)}>{count <= limit ? count : `${limit}+`}</span>;
48
40
  }
49
41
 
50
42
  export function CountBadgeWithNoLimits({count, warning}: {count: number | string; warning?: boolean}) {
@@ -107,8 +107,9 @@ export function defsFromPalette({
107
107
  level2_6: [base[2], baseDark[6]],
108
108
 
109
109
  // muted bg (code, form fields)
110
- level3exotic: [base[3], blackish], // FIXME try blackA[3] | baseDark[3] to make it less exotic
110
+ level3: [base[3], baseDark[3]],
111
111
  level3_1: [base[3], baseDark[1]],
112
+ level3_2: [base[3], baseDark[2]],
112
113
  level3_4: [base[3], baseDark[4]],
113
114
  level3_6: [base[3], baseDark[6]],
114
115
  level3_7: [base[3], baseDark[7]],
@@ -167,9 +168,9 @@ export function defsFromPalette({
167
168
 
168
169
  // --- Input borders (stroke shadows) ---
169
170
  const inputStrokes = {
170
- disabled: [stroke1(alpha(base[6], 0.2)), stroke1(alpha(baseDark[8], 0.2))],
171
- default: [stroke1(alpha(base[6], 0.7)), stroke1(alpha(baseDark[8], 0.7))],
172
- hover: [stroke1(base[7]), stroke1(baseDark[8])],
171
+ disabled: [stroke1(alpha(base[6], 0.6)), stroke1(alpha(baseDark[8], 0.3))],
172
+ default: [stroke1(alpha(base[6], 0.8)), stroke1(alpha(baseDark[8], 0.4))],
173
+ hover: [stroke1(base[7]), stroke1(baseDark[7])],
173
174
  focus: [stroke1(base[8]), stroke1(baseDark[9])],
174
175
  error: [stroke1(red[11]), stroke1(red[7])],
175
176
  errorHover: [stroke1(alpha(red[11], 0.4)), stroke1(alpha(red[7], 0.4))],
@@ -190,6 +191,15 @@ export function defsFromPalette({
190
191
  ],
191
192
  } satisfies Defs;
192
193
 
194
+ // --- Floating panel (inbox item hover, rich text format bar, batch actions toolbar) ---
195
+ const floatingPanel = {
196
+ bg: raised.level2,
197
+ shadow: [
198
+ steps([stroke1(alpha(base[10], 0.15)), elevationSL(alpha(base[11], 0.15))]),
199
+ steps([stroke1(baseDark[6]), elevationSL(alpha(baseDark[1], 0.5))]),
200
+ ],
201
+ } satisfies Defs;
202
+
193
203
  // --- Card/container shadows ---
194
204
  const cardShadows = {
195
205
  subtle: [
@@ -232,7 +242,8 @@ export function defsFromPalette({
232
242
 
233
243
  const shadows = {
234
244
  // Elevations
235
- surfaceElevationS: [elevationS(blackA[3]), elevationS(whiteA[3])],
245
+ // dark uses a subtle dark shadow (baseDark[1]) like entityBoxShadows — NOT a white glow
246
+ surfaceElevationS: [elevationS(blackA[3]), elevationS(alpha(baseDark[1], 0.05))],
236
247
  surfaceElevationM: [elevationM(blackA[5]), elevationM(whiteA[5])],
237
248
  surfaceElevationL: [elevationL(blackA[5]), elevationL(whiteA[5])],
238
249
 
@@ -250,16 +261,6 @@ export function defsFromPalette({
250
261
  shadowSidebar: [edgeLeft(alpha(base[10], 0.1)), edgeLeft(alpha(baseDark[1], 0.8))],
251
262
  } as const;
252
263
 
253
- // names proposal: forbid color in keys (they all are colors) — restructure it better (separate shadows, borders etc)
254
- // inspired by tailwind:
255
- // bg-* Sets the background color of an element
256
- // text-* Sets the text color of an element
257
- // border-* Sets the border color of an element
258
- // shadow-* Sets the color of box shadows
259
- // fill-* Sets the fill color of SVG elements
260
- // stroke-* Sets the stroke color of SVG elements
261
- // TODO elevation = raised * shadow
262
-
263
264
  const transparentAlways = same(transparent);
264
265
  const whiteColor = same(white);
265
266
  const blackColor = same(black);
@@ -349,11 +350,10 @@ export function defsFromPalette({
349
350
  const bgTabHover = [alpha(base[11], 0.1), alpha(baseDark[11], 0.1)] as const;
350
351
 
351
352
  const viewHighlightBg = [accent[3], accentDark[3]] as const;
352
- const notificationSelected = [accent[3], accentDark[4]] as const;
353
- const bgCellGridDisabledOrHighlighted = [accent[4], accentDark[5]] as const;
354
- const notificationSelectedHover = [accent[4], accentDark[6]] as const;
355
353
 
356
- const fgSelection = [alpha(accent[9], 0.2), alpha(accentDark[9], 0.4)] as const;
354
+ const notificationSelected = [accent[3], accentDark[2]] as const;
355
+ const notificationSelectedHover = [accent[4], accentDark[3]] as const;
356
+ const bgCellGridDisabledOrHighlighted = [accent[4], accentDark[5]] as const;
357
357
 
358
358
  const warningColor = [alpha(yellow[6], 0.6), alpha(yellowDark[9], 0.4)] as const;
359
359
  const bgBtnGhostAccentHover = [alpha(accent[4], 0.8), alpha(accentDark[4], 0.8)] as const;
@@ -363,11 +363,14 @@ export function defsFromPalette({
363
363
  const bgBtnSoftNeutralDefault = [base[3], baseDark[3]] as const;
364
364
  const bgBtnSoftNeutralHover = [base[7], baseDark[7]] as const;
365
365
  const bgBtnSoftDestructiveHover = [red[6], redDark[6]] as const;
366
+ const bgBtnSoftAccentDefault = [accent[3], accentDark[4]] as const;
367
+
366
368
  const borderFocusRingAccent = same(alpha(accent[9], 0.3));
367
369
  const borderFocusRingNeutral = same(alpha(base[9], 0.3));
368
370
  const borderFocusRingDestructive = same(alpha(red[9], 0.3));
369
371
  const textWhiteboardSelectionLockedOrDisabled = [alpha(base[9], 0.5), alpha(baseDark[9], 0.5)] as const;
370
372
 
373
+ const fgSelection = [alpha(accent[9], 0.2), alpha(accentDark[9], 0.4)] as const;
371
374
  const fgSelectedText = same(base[2]);
372
375
  const fgSolidActiveText = same(alpha(white, 0.8));
373
376
 
@@ -426,15 +429,24 @@ export function defsFromPalette({
426
429
  // Accent base
427
430
  colorAccent: primary.accent,
428
431
 
429
- // Rubbish dump
430
- /** @deprecated */
432
+ //to be sorted. these are still in use
431
433
  darkenPrimary: same(getDarkenColor(accent[9])),
432
434
  whiteColor,
433
435
  blackColor,
434
- mainBg: surface.level3exotic,
436
+ mainBg: surface.level3,
435
437
  panelBg: surface.level0,
438
+ panelBorder: [stroke1(alpha(base[8], 0.2)), stroke1(alpha(baseDark[8], 0.2))],
436
439
  panelContentBg: surface.level2_1,
437
- colorBgRelationContainer: surface.level2_1,
440
+
441
+ //Database settings screens
442
+ dbSettingsSidebarBg: surface.level2,
443
+ dbSettingsSidebarItemHover: surface.level4,
444
+ dbSettingsSidebarIconBg: raised.level1,
445
+ dbSettingsSidebarIconBorder: shadowMenuHeaderAvatar,
446
+
447
+ dbSettingsCardBg: surface.level2,
448
+ dbSettingsCardBorder: [stroke1(alpha(base[6], 0.8)), stroke1(alpha(baseDark[6], 0.6))],
449
+ colorBgRelationContainer: surface.level1_1,
438
450
 
439
451
  // AI colors
440
452
  colorAI: fg.ai,
@@ -444,10 +456,14 @@ export function defsFromPalette({
444
456
  colorBgAIFloatingPanelContent: surface.level1_3,
445
457
  colorBgAISidebarContent: surface.level1,
446
458
 
459
+ // Floating panels (hover action toolbars, selection toolbar, rich-text format bar)
460
+ colorBgFloatingPanel: floatingPanel.bg,
461
+ shadowFloatingPanel: floatingPanel.shadow,
462
+
447
463
  // Other colors
448
464
  pageContentBg: surface.level0elevated,
449
- colorBgPopup: raised.level2,
450
- menuBg: surface.level3exotic,
465
+ colorBgPopup: [white, baseDark[3]],
466
+ menuBg: surface.level3,
451
467
  menuTextColor: fgMenuEtc,
452
468
  menuItemHoverColor: base5_6,
453
469
  menuSelectedTextColor: fgSelectedText,
@@ -482,7 +498,7 @@ export function defsFromPalette({
482
498
  shortcutBorderColor: [alpha(base[8], 0.5), alpha(baseDark[10], 0.2)],
483
499
 
484
500
  // Input
485
- inputBgColor: surface.level0elevated,
501
+ inputBgColor: [white, baseDark[1]],
486
502
  inputDisabledBgColor: base6_6_opacity30,
487
503
  inputDisabledBorderColor: inputStrokes.disabled,
488
504
  inputCopyBgColor: surface.level3_4,
@@ -496,6 +512,7 @@ export function defsFromPalette({
496
512
  inputErrorBorderFocusColor: inputStrokes.errorFocus,
497
513
  inputErrorBorderFocusShadow: inputStrokes.errorFocusRing,
498
514
  inputBorderWarningColor: inputStrokes.warning,
515
+ //buttons and checkboxes and blockquote (why?)
499
516
  buttonPrimaryColor: primary.accent,
500
517
  buttonColor: fgAccentText,
501
518
  buttonPrimaryTextColor: [base[2], base[6]],
@@ -570,7 +587,8 @@ export function defsFromPalette({
570
587
 
571
588
  // Search
572
589
  colorSearchSubtitle: fg.muted,
573
- colorBgSearchRowHover: bgActionsMenuItemHover,
590
+ colorBgSearchPanel: [white, baseDark[2]],
591
+ colorBgSearchRowHover: [base[4], baseDark[3]],
574
592
  colorBgSearchRowCreateHover: [accent[4], accentDark[4]],
575
593
  opacitySearchItemDone: opacities.drag,
576
594
 
@@ -618,7 +636,7 @@ export function defsFromPalette({
618
636
  entityCardDoneColor: bgEntityCardDone,
619
637
  entityCardShadow: cardShadows.subtle,
620
638
  entityCardShadowHover: cardShadows.subtle,
621
- colorBgSegmentedControl: [alpha(base[5], 0.8), alpha(baseDark[3], 0.8)],
639
+ colorBgSegmentedControl: [alpha(base[5], 0.8), alpha(baseDark[2], 0.8)],
622
640
  colorBgMenuHeaderAvatar: surface.level0elevated,
623
641
  shadowMenuHeaderAvatar: shadowMenuHeaderAvatar,
624
642
 
@@ -630,9 +648,9 @@ export function defsFromPalette({
630
648
  colorTextMenuItem: [alpha(base[12], 0.9), alpha(baseDark[12], 0.9)],
631
649
  colorBgMenuItem: transparentAlways,
632
650
  // :hover
633
- colorBgMenuItemHover: [base[5], baseDark[2]],
651
+ colorBgMenuItemHover: [base[5], baseDark[5]],
634
652
  // :focus
635
- colorBgMenuItemFocus: [base[6], baseDark[5]],
653
+ colorBgMenuItemFocus: [base[6], baseDark[6]],
636
654
  // Selected
637
655
  colorBgMenuItemSelected: [alpha(accent[5], 0.8), accentDark[3]],
638
656
  // :hover
@@ -700,7 +718,7 @@ export function defsFromPalette({
700
718
  filterGroup1Bg: surface.level2_6,
701
719
  filterGroup2Bg: surface.level3_7,
702
720
  filterGroupBorder: [blackA[3], whiteA[3]],
703
- colorBgNotificationHover: surface.level3_4,
721
+ colorBgNotificationHover: surface.level3_2,
704
722
  colorBgNotificationSelected: notificationSelected,
705
723
  colorBgNotificationSelectedHover: notificationSelectedHover,
706
724
  disabledInversedTextColor: blackA25,
@@ -727,6 +745,9 @@ export function defsFromPalette({
727
745
 
728
746
  // Views
729
747
  viewBg: surface.level1,
748
+ colorBgViewMetaItem: surface.level0elevated,
749
+ collectionViewBg: surface.level1_1,
750
+ collectionViewBorder: inputStrokes.disabled,
730
751
  viewSecondaryBg: surface.level2,
731
752
  viewHighlightBg: viewHighlightBg,
732
753
 
@@ -796,7 +817,7 @@ export function defsFromPalette({
796
817
  colorIconButtonOutlineNeutral: fg.neutral,
797
818
  colorIconButtonOutlineDestructive: fg.destructive,
798
819
  // soft
799
- colorBgButtonSoftAccentDefault: notificationSelected,
820
+ colorBgButtonSoftAccentDefault: bgBtnSoftAccentDefault,
800
821
  colorBgButtonSoftAccentHover: bgBtnSoftAccentHover,
801
822
  colorBgButtonSoftNeutralDefault: bgBtnSoftNeutralDefault,
802
823
  colorBgButtonSoftNeutralHover: bgBtnSoftNeutralHover,
@@ -856,7 +877,10 @@ export function defsFromPalette({
856
877
  colorTextEntityAvatarHover: primary.accent,
857
878
  colorBgMessageHighlighted: surface.level0elevated,
858
879
 
859
- // Text editor
880
+ // Comment/message row hover (subtle lift over panel, stays darker than the floating actions toolbar in dark theme)
881
+ colorBgCommentHover: surface.level2,
882
+
883
+ colorBgCodeEditor: raised.level1,
860
884
  colorBgEditorImageZoomed: surface.level3_6,
861
885
  colorMermaidPrimaryColor: [base[2], baseDark[8]],
862
886
  colorMermaidPrimaryBorderColor: fg.muted,
@@ -885,10 +909,6 @@ export function defsFromPalette({
885
909
  colorTextWhiteboardDefault: fgMenuEtc,
886
910
  colorTextWhiteboardDisabled: fg.muted,
887
911
  // FIXME bug?
888
- // Dark uses baseDark[1] (near-black at 60% opacity) as "accent text" on a dark background.
889
- // This reads as near-invisible dark text on dark bg.
890
- // Compare with colorUnitHeaderWhiteboardDefault (L772) which correctly uses baseDark[12] for dark.
891
- // Likely should be baseDark[12] too.
892
912
  colorTextWhiteboardAccent: [alpha(base[12], 0.6), alpha(baseDark[1], 0.6)],
893
913
  colorTextWhiteboardInversedDefault: whiteColor,
894
914
  instrumentsMenuBg: raised.level2,
@@ -8,7 +8,7 @@ export function mapDef<T, U>(def: ModeDef<T>, mapper: MapperDef<T, U>): ModeDef<
8
8
  export function mapDef<T, U>(def: ModeDef<T> | undefined, mapper: MapperDef<T | undefined, U>): ModeDef<U>;
9
9
  export function mapDef<T, U>(def: ModeDef<T> | undefined, mapper: MapperDef<T | undefined, U>): ModeDef<U> {
10
10
  if (typeof mapper === "function") {
11
- // oxlint-disable-next-line no-param-reassign
11
+ // eslint-disable-next-line no-param-reassign
12
12
  mapper = [mapper, mapper];
13
13
  }
14
14
  if (def === undefined) {
package/src/dom-utils.ts CHANGED
@@ -1,15 +1,8 @@
1
- const fail = (message: string) => {
2
- throw new Error(message);
3
- };
4
-
5
- let popupContainerElement: HTMLElement;
6
- export const getPopupContainerElement = (): HTMLElement => {
7
- if (!popupContainerElement) {
8
- popupContainerElement =
9
- document.getElementById("popup-container") || fail("popup-container was not found in document");
10
- }
11
- return popupContainerElement;
12
- };
1
+ // Popups render directly in body, like Radix portals do: later-opened floating
2
+ // elements come later in the DOM and paint above earlier ones, and no host
3
+ // HTML contract is required. Kept as a function so call sites stay unchanged
4
+ // until the custom popover is replaced with a Radix one.
5
+ export const getPopupContainerElement = (): HTMLElement => document.body;
13
6
 
14
7
  type LimitedMouseEvent = Pick<MouseEvent, "button" | "metaKey" | "altKey" | "ctrlKey" | "shiftKey">;
15
8
 
@@ -0,0 +1,36 @@
1
+ const blobToDataUrl = async (blobUrl: string) => {
2
+ const result = await fetch(blobUrl);
3
+ const blob = await result.blob();
4
+ const dataUrl = await new Promise<string>((resolve, reject) => {
5
+ const reader = new FileReader();
6
+ reader.readAsDataURL(blob);
7
+ reader.onloadend = () => resolve(reader.result as string);
8
+ reader.onerror = () => reject(new Error("Error fetching blob"));
9
+ reader.onabort = () => reject(new Error("Aborted"));
10
+ });
11
+ return dataUrl;
12
+ };
13
+
14
+ export const downloadFile = async (url: string, fileName?: string) => {
15
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
16
+ // @ts-ignore - injected by consumer app
17
+ if (window.__FIBERY_MOBILE_APP_API) {
18
+ let absoluteUrl = new URL(url, document.baseURI).href;
19
+ if (absoluteUrl.startsWith("blob:")) {
20
+ absoluteUrl = await blobToDataUrl(absoluteUrl);
21
+ }
22
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
23
+ // @ts-ignore - injected by consumer app
24
+ window.__FIBERY_MOBILE_APP_API.downloadFile(absoluteUrl, fileName);
25
+ return;
26
+ }
27
+
28
+ const a = document.createElement("a");
29
+ a.href = url;
30
+ a.download = fileName ?? "";
31
+ a.style.display = "none";
32
+
33
+ document.body.appendChild(a);
34
+ a.click();
35
+ document.body.removeChild(a);
36
+ };
@@ -15,7 +15,6 @@ export const Root = (dropdownMenuProps: DropdownMenuPrimitive.DropdownMenuProps)
15
15
  export const Trigger = forwardRef<
16
16
  HTMLButtonElement,
17
17
  DropdownMenuPrimitive.DropdownMenuTriggerProps & {triggerEvent?: string}
18
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
19
18
  >(({triggerEvent, className, children, ...otherProps}, ref) => {
20
19
  return (
21
20
  <DropdownMenuPrimitive.Trigger ref={ref} className={className} {...otherProps}>
@@ -7,9 +7,11 @@ import FileDownload from "../icons/react/file-download";
7
7
  import Link from "../icons/react/link";
8
8
  import Pencil from "../icons/react/pencil";
9
9
  import RicheditorOpenLink from "../icons/react/richeditor-open-link";
10
+ import Share from "../icons/react/share";
10
11
  import {useToast} from "../toast/toast-provider";
11
12
  import {isPhoneApp} from "../use-is-phone";
12
13
  import {addQueryParam} from "./add-query-param";
14
+ import {canShareFile, shareFile} from "./share-file";
13
15
  import type {UploadFileWithContent} from "./types";
14
16
 
15
17
  export function FileMenuItems({
@@ -26,6 +28,7 @@ export function FileMenuItems({
26
28
  disabled?: boolean;
27
29
  }): ReactNode {
28
30
  const toast = useToast();
31
+ const sharingEnabled = isPhoneApp() && canShareFile();
29
32
 
30
33
  return (
31
34
  <>
@@ -39,19 +42,34 @@ export function FileMenuItems({
39
42
  >
40
43
  Copy file URL
41
44
  </ActionsMenuItem>
42
- <a href={file.url} target="_blank" rel="noopener noreferrer">
43
- <ActionsMenuItem Icon={RicheditorOpenLink}>
44
- {isPhoneApp() ? "Open in browser" : "Open in new tab"}
45
- </ActionsMenuItem>
46
- </a>
47
- {isPhoneApp() ? (
48
- <ActionsMenuItem Icon={FileDownload} onSelect={onDownloadClick}>
49
- Download
45
+ {sharingEnabled ? (
46
+ <ActionsMenuItem
47
+ Icon={Share}
48
+ onSelect={() => {
49
+ shareFile(file.url, file.name, {toast}).catch((error) =>
50
+ toast.error({title: "Unable to share file", subTitle: error.message})
51
+ );
52
+ }}
53
+ >
54
+ Share
50
55
  </ActionsMenuItem>
51
56
  ) : (
52
- <a download={file.name} href={addQueryParam(file.url, "attachment", "true")}>
53
- <ActionsMenuItem Icon={FileDownload}>Download</ActionsMenuItem>
54
- </a>
57
+ <>
58
+ <a href={file.url} target="_blank" rel="noopener noreferrer">
59
+ <ActionsMenuItem Icon={RicheditorOpenLink}>
60
+ {isPhoneApp() ? "Open in browser" : "Open in new tab"}
61
+ </ActionsMenuItem>
62
+ </a>
63
+ {isPhoneApp() ? (
64
+ <ActionsMenuItem Icon={FileDownload} onSelect={onDownloadClick}>
65
+ Download
66
+ </ActionsMenuItem>
67
+ ) : (
68
+ <a download={file.name} href={addQueryParam(file.url, "attachment", "true")}>
69
+ <ActionsMenuItem Icon={FileDownload}>Download</ActionsMenuItem>
70
+ </a>
71
+ )}
72
+ </>
55
73
  )}
56
74
  {onRenameClick && file.status === "done" ? (
57
75
  <>
@@ -1,30 +1,71 @@
1
+ import {useCallback} from "react";
2
+
1
3
  import {ActionsMenuItem, ActionsMenuSeparator, AsyncActionsMenuItem} from "../actions-menu";
4
+ import {downloadFile} from "../download-file";
5
+ import {asError} from "../errors";
2
6
  import Delete from "../icons/react/delete";
3
7
  import FileDownload from "../icons/react/file-download";
4
8
  import RicheditorOpenLink from "../icons/react/richeditor-open-link";
9
+ import Share from "../icons/react/share";
10
+ import {useToast} from "../toast/toast-provider";
5
11
  import {isPhoneApp} from "../use-is-phone";
6
12
  import {addQueryParam} from "./add-query-param";
13
+ import {canShareFile, shareFile} from "./share-file";
7
14
 
8
15
  export function FilePreviewActions({
9
16
  url,
10
17
  name,
11
- onDownloadClick,
12
18
  onDeleteClick,
13
19
  }: {
14
20
  url: string;
15
21
  name: string;
16
- onDownloadClick?: () => void;
17
22
  onDeleteClick?: () => void | Promise<void>;
18
23
  }) {
24
+ const toast = useToast();
25
+ const onDownloadClick = useCallback(
26
+ (e: Event) => {
27
+ e?.preventDefault();
28
+ e?.stopPropagation();
29
+
30
+ downloadFile(addQueryParam(url, "attachment", "true"), name).catch((error) => {
31
+ toast.error({title: "Unable to download file", subTitle: asError(error).message});
32
+ });
33
+ },
34
+ [url, name, toast]
35
+ );
36
+
19
37
  if (isPhoneApp()) {
20
38
  return (
21
39
  <>
22
- <a href={url} target="_blank" rel="noopener noreferrer">
23
- <ActionsMenuItem Icon={RicheditorOpenLink}>Open in browser</ActionsMenuItem>
24
- </a>
25
- <ActionsMenuItem Icon={FileDownload} onSelect={onDownloadClick}>
26
- Download
27
- </ActionsMenuItem>
40
+ {canShareFile() ? (
41
+ <ActionsMenuItem
42
+ Icon={Share}
43
+ onSelect={() => {
44
+ shareFile(url, name, {toast}).catch((error) =>
45
+ toast.error({title: "Unable to share file", subTitle: asError(error).message})
46
+ );
47
+ }}
48
+ >
49
+ Share
50
+ </ActionsMenuItem>
51
+ ) : (
52
+ <>
53
+ <a href={url} target="_blank" rel="noopener noreferrer">
54
+ <ActionsMenuItem Icon={RicheditorOpenLink}>Open in browser</ActionsMenuItem>
55
+ </a>
56
+ <ActionsMenuItem Icon={FileDownload} onSelect={onDownloadClick}>
57
+ Download
58
+ </ActionsMenuItem>
59
+ </>
60
+ )}
61
+ {onDeleteClick ? (
62
+ <>
63
+ <ActionsMenuSeparator />
64
+ <AsyncActionsMenuItem Icon={Delete} onSelect={onDeleteClick} dangerous>
65
+ Delete
66
+ </AsyncActionsMenuItem>
67
+ </>
68
+ ) : null}
28
69
  </>
29
70
  );
30
71
  }
@@ -34,9 +75,9 @@ export function FilePreviewActions({
34
75
  <a href={url} target="_blank" rel="noopener noreferrer">
35
76
  <ActionsMenuItem Icon={RicheditorOpenLink}>Open in new tab</ActionsMenuItem>
36
77
  </a>
37
- <a download={name} href={addQueryParam(url, "attachment", "true")}>
38
- <ActionsMenuItem Icon={FileDownload}>Download</ActionsMenuItem>
39
- </a>
78
+ <ActionsMenuItem Icon={FileDownload} onSelect={onDownloadClick}>
79
+ Download
80
+ </ActionsMenuItem>
40
81
  {onDeleteClick ? (
41
82
  <>
42
83
  <ActionsMenuSeparator />
@@ -0,0 +1,53 @@
1
+ import type {ToastQueue} from "../toast/toast-queue";
2
+
3
+ const blobToDataUrl = (blob: Blob) =>
4
+ new Promise<string>((resolve, reject) => {
5
+ const reader = new FileReader();
6
+ reader.readAsDataURL(blob);
7
+ reader.onloadend = () => resolve(reader.result as string);
8
+ reader.onerror = () => reject(new Error("Error reading file"));
9
+ reader.onabort = () => reject(new Error("Aborted"));
10
+ });
11
+
12
+ export const canShareFile = (): boolean =>
13
+ Boolean(
14
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
15
+ // @ts-ignore - injected by consumer app
16
+ window.__FIBERY_MOBILE_APP_API?.shareFile
17
+ );
18
+
19
+ export const shareFile = async (
20
+ url: string,
21
+ fileName?: string,
22
+ opts?: {toast?: ToastQueue; loadingTitle?: string}
23
+ ): Promise<void> => {
24
+ let toastId: string | undefined;
25
+ const toastTimeoutId = opts?.toast
26
+ ? setTimeout(() => {
27
+ toastId = opts.toast?.loading({title: opts.loadingTitle ?? "Preparing file…"});
28
+ }, 300)
29
+ : undefined;
30
+ const hideLoadingToast = () => {
31
+ clearTimeout(toastTimeoutId);
32
+ if (toastId) {
33
+ opts?.toast?.close(toastId);
34
+ }
35
+ };
36
+
37
+ try {
38
+ const absoluteUrl = url.startsWith("/") ? `${window.origin}${url}` : url;
39
+ const response = await fetch(absoluteUrl);
40
+ if (!response.ok) {
41
+ throw new Error(`Failed to fetch file (${response.status})`);
42
+ }
43
+ const dataUrl = await blobToDataUrl(await response.blob());
44
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
45
+ // @ts-ignore - injected by consumer app
46
+ const sharePromise: Promise<void> = window.__FIBERY_MOBILE_APP_API.shareFile(dataUrl, fileName);
47
+ hideLoadingToast();
48
+ await sharePromise;
49
+ } catch (error) {
50
+ hideLoadingToast();
51
+ throw error;
52
+ }
53
+ };
package/src/file-item.tsx CHANGED
@@ -308,6 +308,8 @@ function FileActionsMenu({
308
308
  return (
309
309
  <ActionsMenu
310
310
  onOpenChange={setIsActionsOpened}
311
+ supportsMobile
312
+ mobileTitle={data.name}
311
313
  trigger={
312
314
  <ActionsButtonCompact
313
315
  className={css`
@@ -317,15 +319,13 @@ function FileActionsMenu({
317
319
  />
318
320
  }
319
321
  >
320
- {
321
- <FileMenuItems
322
- file={data}
323
- disabled={disabled}
324
- onDownloadClick={onDownloadClick}
325
- onRemoveClick={onRemoveClick}
326
- onRenameClick={onRenameClick}
327
- />
328
- }
322
+ <FileMenuItems
323
+ file={data}
324
+ disabled={disabled}
325
+ onDownloadClick={onDownloadClick}
326
+ onRemoveClick={onRemoveClick}
327
+ onRenameClick={onRenameClick}
328
+ />
329
329
  </ActionsMenu>
330
330
  );
331
331
  }
@@ -1,4 +1,3 @@
1
- /* eslint-disable max-lines */
2
1
  import {css} from "@linaria/core";
3
2
 
4
3
  import {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * This file was automatically generated. Do not edit manually.
3
+ *
4
+ * @see import("../svg/gauge.svg")
5
+ * @see import("../../../scripts/generate-icons.mjs")
6
+ */
7
+
8
+ import type {IconDefinition} from "../types";
9
+
10
+ const Gauge: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"viewBox":"0 0 18 18","fill":"none"},"children":[{"type":"element","tagName":"path","properties":{"d":"M9 1.8c1.461 0 2.82.437 3.955 1.185l-1.45.727A5.85 5.85 0 1 0 14.5 7.004l.864-1.372A7.2 7.2 0 1 1 9 1.8Zm1.457 8.252A1.795 1.795 0 0 1 9 10.8a1.8 1.8 0 0 1-.847-3.39L13.95 4.5l-3.493 5.552ZM9 8.1a.9.9 0 1 0 0 1.8.9.9 0 0 0 0-1.8Z","fill":"#635E57"},"children":[]}],"metadata":""}]},"name":"gauge"};
11
+
12
+ export default Gauge;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * This file was automatically generated. Do not edit manually.
3
- *
3
+ *
4
4
  * @see import("../../../scripts/generate-icons.mjs")
5
5
  */
6
6
 
@@ -121,6 +121,7 @@ export {default as Pin} from "./pin";
121
121
  export {default as PinFilled} from "./pin-filled";
122
122
  export {default as Photo} from "./photo";
123
123
  export {default as People} from "./people";
124
+ export {default as PeopleEdit} from "./people-edit";
124
125
  export {default as PeopleCheck} from "./people-check";
125
126
  export {default as Pencil} from "./pencil";
126
127
  export {default as PanelAdd} from "./panel-add";
@@ -209,6 +210,7 @@ export {default as GlobeSimple} from "./globe-simple";
209
210
  export {default as Gitlab} from "./gitlab";
210
211
  export {default as Github} from "./github";
211
212
  export {default as Gift} from "./gift";
213
+ export {default as Gauge} from "./gauge";
212
214
  export {default as FullscreenModeOn} from "./fullscreen-mode-on";
213
215
  export {default as FullscreenModeOnIcon} from "./fullscreen-mode-on-icon";
214
216
  export {default as FullscreenModeOffIcon} from "./fullscreen-mode-off-icon";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * This file was automatically generated. Do not edit manually.
3
+ *
4
+ * @see import("../svg/people-edit.svg")
5
+ * @see import("../../../scripts/generate-icons.mjs")
6
+ */
7
+
8
+ import type {IconDefinition} from "../types";
9
+
10
+ const PeopleEdit: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"viewBox":"0 0 20 20"},"children":[{"type":"element","tagName":"path","properties":{"d":"M14.44 9.791a1.75 1.75 0 0 1 2.475 0l.585.586a1.75 1.75 0 0 1 0 2.475l-3.16 3.16a.753.753 0 0 1-.326.192L11.218 17a.751.751 0 0 1-.927-.927l.796-2.796.033-.09a.751.751 0 0 1 .159-.235l3.16-3.161Zm-6.682.933a.715.715 0 0 1 .75.726c0 .414-.337.747-.75.777-.87.065-1.7.247-2.391.575-.87.412-1.5 1.043-1.768 1.977-.105.367-.115.922-.081 1.45.008.126.123.271.338.271h3.9a.75.75 0 0 1 0 1.5h-3.9c-.915 0-1.77-.671-1.835-1.673-.036-.556-.045-1.33.136-1.96.41-1.432 1.392-2.362 2.566-2.92.931-.442 1.993-.657 3.035-.723Zm8.096.128a.25.25 0 0 0-.354 0l-3.023 3.022-.374 1.313 1.313-.373 3.024-3.023a.25.25 0 0 0 0-.354l-.586-.585ZM8.505 2a3.65 3.65 0 1 1 0 7.301 3.65 3.65 0 0 1 0-7.301Zm0 1.5a2.15 2.15 0 1 0 .002 4.3 2.15 2.15 0 0 0-.002-4.3Z"},"children":[]}],"metadata":""}]},"name":"people-edit"};
11
+
12
+ export default PeopleEdit;