@fibery/ui-kit 1.38.0 → 1.38.1

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 (88) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/config/jest/testFrameworkSetup.js +1 -1
  3. package/package.json +16 -15
  4. package/src/abbreviate-name.ts +42 -0
  5. package/src/actions-menu/actions-menu-item.tsx +14 -5
  6. package/src/actions-menu/actions-menu-props.tsx +1 -0
  7. package/src/actions-menu/actions-menu.tsx +4 -1
  8. package/src/actions-menu/context-actions-menu.tsx +22 -0
  9. package/src/animated-height-container.tsx +24 -5
  10. package/src/antd/ant-upload.tsx +1 -1
  11. package/src/antd/input.tsx +4 -0
  12. package/src/avatar.tsx +1 -27
  13. package/src/button/base-button.tsx +2 -0
  14. package/src/button/button.tsx +3 -2
  15. package/src/button/icon-button.tsx +3 -3
  16. package/src/card-container.tsx +3 -2
  17. package/src/checkbox.tsx +17 -0
  18. package/src/collapsible-section.tsx +3 -1
  19. package/src/date-picker/autocomplete.tsx +11 -5
  20. package/src/date-picker/date-range-input.tsx +5 -1
  21. package/src/date-picker/date-range-picker-popup-content.tsx +31 -13
  22. package/src/date-picker/single-date-input.tsx +5 -1
  23. package/src/date-picker/styles.ts +20 -0
  24. package/src/design-system.ts +24 -0
  25. package/src/dropdown-menu/index.tsx +11 -1
  26. package/src/emoji-picker/emoji-picker-preview-footer.tsx +4 -0
  27. package/src/emoji-picker/emoji.tsx +4 -3
  28. package/src/emoji-picker/primitives/emoji.tsx +49 -51
  29. package/src/emoji-picker/primitives/footer.tsx +6 -0
  30. package/src/emoji-picker/primitives/search.tsx +8 -0
  31. package/src/emoji-picker/primitives/skin-tone.tsx +12 -2
  32. package/src/fibermoji-placeholder.tsx +1 -27
  33. package/src/file-item-2.tsx +408 -0
  34. package/src/file-item.tsx +63 -12
  35. package/src/header-layout.tsx +11 -0
  36. package/src/html-styles.ts +13 -4
  37. package/src/icons/ast/Back.ts +1 -1
  38. package/src/icons/ast/ExtensionFiles.ts +1 -1
  39. package/src/icons/ast/FileUpload.ts +1 -1
  40. package/src/icons/ast/SwitchToMention.ts +8 -0
  41. package/src/icons/ast/SwitchToText.ts +8 -0
  42. package/src/icons/ast/SwitchToWidget.ts +8 -0
  43. package/src/icons/ast/ViewGallery.ts +8 -0
  44. package/src/icons/ast/index.tsx +4 -0
  45. package/src/icons/react/SwitchToMention.tsx +13 -0
  46. package/src/icons/react/SwitchToText.tsx +13 -0
  47. package/src/icons/react/SwitchToWidget.tsx +13 -0
  48. package/src/icons/react/ViewGallery.tsx +13 -0
  49. package/src/icons/react/index.tsx +4 -0
  50. package/src/icons/svg/back.svg +3 -1
  51. package/src/icons/svg/extension/files.svg +3 -1
  52. package/src/icons/svg/file-upload.svg +3 -6
  53. package/src/icons/svg/switch-to-mention.svg +3 -0
  54. package/src/icons/svg/switch-to-text.svg +4 -0
  55. package/src/icons/svg/switch-to-widget.svg +4 -0
  56. package/src/icons/svg/view-gallery.svg +4 -0
  57. package/src/images-gallery/images-gallery-fall-through-provider.tsx +11 -0
  58. package/src/images-gallery/images-gallery.tsx +13 -10
  59. package/src/images-gallery/zoom.tsx +13 -2
  60. package/src/images-gallery-2/images-gallery-2.tsx +442 -0
  61. package/src/images-gallery-2/slide-buttons.tsx +230 -0
  62. package/src/images-gallery-2/zoom.tsx +35 -0
  63. package/src/kbd.tsx +46 -0
  64. package/src/modal-menu/modal-menu-item.tsx +10 -2
  65. package/src/modal-menu/modal-menu-trigger.tsx +19 -2
  66. package/src/notification-dot.tsx +16 -7
  67. package/src/platform-symbols.ts +9 -0
  68. package/src/platform.ts +2 -0
  69. package/src/popover/index.tsx +31 -8
  70. package/src/popover/modifiers.tsx +26 -19
  71. package/src/reactions/reaction-button.tsx +323 -101
  72. package/src/reactions/reaction-picker.tsx +14 -1
  73. package/src/reactions/reactions.tsx +11 -1
  74. package/src/reactions/tootltip.tsx +55 -0
  75. package/src/rich-text-embed-error.tsx +2 -1
  76. package/src/select/components/group-heading.tsx +8 -0
  77. package/src/select/components/menu-list-virtualized.tsx +6 -2
  78. package/src/select/components/option.tsx +4 -0
  79. package/src/select/select-control-settings-context.tsx +11 -2
  80. package/src/select/select-in-popover.tsx +10 -7
  81. package/src/select/styles.ts +21 -1
  82. package/src/tab-nav/tab-nav.tsx +1 -1
  83. package/src/toggle.tsx +8 -0
  84. package/src/tooltip.tsx +27 -18
  85. package/src/type-badge.tsx +5 -2
  86. package/src/unit/unit-with-tooltip.tsx +1 -1
  87. package/src/use-is-phone.tsx +27 -6
  88. package/src/shortcut-badge.tsx +0 -29
@@ -0,0 +1,442 @@
1
+ import PhotoSwipeLightbox, {type PhotoSwipeEventsMap} from "photoswipe/lightbox";
2
+ import "photoswipe/style.css";
3
+ import {
4
+ ImagesGalleryMethodsProvider,
5
+ ImagesGalleryRegistryProvider,
6
+ ImagesGalleryStateProvider,
7
+ } from "../images-gallery/images-gallery";
8
+ import {PropsWithChildren, useCallback, useEffect, useMemo, useReducer, useRef, useState} from "react";
9
+ import {css} from "@linaria/core";
10
+ import {createRoot} from "react-dom/client";
11
+ import {space, themeVars} from "../design-system";
12
+ import {useToast} from "../toast/toast-provider";
13
+ import {ToastQueue} from "../toast/toast-queue";
14
+ import {useThemeMode} from "../theme-provider";
15
+ import {ThemeMode} from "../theme-settings";
16
+ import {SlideButtons} from "./slide-buttons";
17
+
18
+ type Registry = Map<
19
+ string,
20
+ {
21
+ src: string;
22
+ type: "image" | "video" | "iframe";
23
+ actions?: React.ReactNode;
24
+ zoomed: boolean;
25
+ setZoomed: (zoomed: boolean) => void;
26
+ }
27
+ >;
28
+
29
+ type Methods = {
30
+ setVisible: (visible: boolean) => void;
31
+ setActivated: (activated: boolean) => void;
32
+ update: () => void;
33
+ nextImage: () => void;
34
+ prevImage: () => void;
35
+ };
36
+
37
+ const galleryMainClass = css`
38
+ z-index: 1000;
39
+ & .pswp__button--arrow--prev {
40
+ left: ${space.s20}px;
41
+ height: ${space.s28}px;
42
+ width: ${space.s28}px;
43
+ margin-top: -${space.s14}px;
44
+ }
45
+ & .pswp__button--arrow--next {
46
+ right: ${space.s20}px;
47
+ height: ${space.s28}px;
48
+ width: ${space.s28}px;
49
+ margin-top: -${space.s14}px;
50
+ }
51
+
52
+ & .pswp__button--zoom {
53
+ top: ${space.s20}px;
54
+ height: ${space.s28}px;
55
+ width: ${space.s28}px;
56
+ }
57
+
58
+ & .pswp__counter {
59
+ margin-top: ${space.s20}px;
60
+ margin-inline-start: ${space.s24}px;
61
+ }
62
+
63
+ & .pswp__video-container {
64
+ padding: ${space.s60}px;
65
+ }
66
+
67
+ & .pswp__video-container video {
68
+ width: 100%;
69
+ height: 100%;
70
+ object-fit: contain;
71
+ display: block;
72
+ background-color: black;
73
+ }
74
+
75
+ & .pswp__iframe-container {
76
+ padding: 0 ${space.s60}px 0 ${space.s60}px;
77
+ display: flex;
78
+ justify-content: center;
79
+ align-items: center;
80
+ }
81
+
82
+ & .pswp__iframe-container iframe {
83
+ width: 100%;
84
+ height: 100%;
85
+ max-width: min(822px, calc(100vw - 40px));
86
+ object-fit: contain;
87
+ display: block;
88
+ color: ${themeVars.textColor};
89
+ background-color: ${themeVars.colorBgPopup};
90
+ border: none;
91
+ }
92
+ `;
93
+
94
+ export function ImagesGalleryProvider2({children}: PropsWithChildren) {
95
+ const [imagesRegistry] = useState<Registry>(() => new Map());
96
+ const [galleryState, dispatchState] = useReducer(imagesGalleryReducer, [], initialState);
97
+
98
+ const lightbox = useRef<PhotoSwipeLightbox | null>(null);
99
+ const markAllNotZoomed = useCallback(() => {
100
+ for (const node of imagesRegistry.values()) {
101
+ if (node.zoomed) {
102
+ node.setZoomed(false);
103
+ }
104
+ }
105
+ }, [imagesRegistry]);
106
+
107
+ useEffect(() => {
108
+ return () => {
109
+ lightbox.current?.destroy();
110
+ lightbox.current = null;
111
+ markAllNotZoomed();
112
+ };
113
+ }, [markAllNotZoomed]);
114
+
115
+ const methods: Methods = useMemo(() => {
116
+ return {
117
+ setVisible: () => {},
118
+ setActivated: (activated: boolean) => {
119
+ dispatchState({type: "set-activated", payload: activated});
120
+ },
121
+ update: () => {
122
+ dispatchState({type: "inc"});
123
+ },
124
+ nextImage: () => {
125
+ lightbox.current?.pswp?.next();
126
+ },
127
+ prevImage: () => {
128
+ lightbox.current?.pswp?.prev();
129
+ },
130
+ };
131
+ }, []);
132
+
133
+ const toast = useToast();
134
+ const themeMode = useThemeMode();
135
+
136
+ useEffect(() => {
137
+ if (galleryState.activated) {
138
+ const value = initGallery(imagesRegistry, toast, themeMode);
139
+ value.on("close", () => {
140
+ dispatchState({type: "set-activated", payload: false});
141
+ markAllNotZoomed();
142
+ value.destroy();
143
+ });
144
+
145
+ lightbox.current = value;
146
+ }
147
+
148
+ return () => {
149
+ lightbox.current?.destroy();
150
+ lightbox.current = null;
151
+ };
152
+ }, [galleryState.activated, imagesRegistry, markAllNotZoomed, toast, themeMode]);
153
+
154
+ return (
155
+ <ImagesGalleryRegistryProvider value={imagesRegistry}>
156
+ <ImagesGalleryMethodsProvider value={methods}>
157
+ <ImagesGalleryStateProvider value={galleryState}>{children}</ImagesGalleryStateProvider>
158
+ </ImagesGalleryMethodsProvider>
159
+ </ImagesGalleryRegistryProvider>
160
+ );
161
+ }
162
+
163
+ type State = {version: "1" | "2"; visible: boolean; activated: boolean; _inc: number};
164
+ type Actions = {type: "set-visible"; payload: boolean} | {type: "set-activated"; payload: boolean} | {type: "inc"};
165
+
166
+ function initialState(): State {
167
+ return {version: "2", visible: false, activated: false, _inc: 0};
168
+ }
169
+
170
+ function imagesGalleryReducer(state: State, action: Actions) {
171
+ if (action.type === "set-visible") {
172
+ return {...state, visible: action.payload};
173
+ }
174
+ if (action.type === "set-activated") {
175
+ return {...state, activated: action.payload};
176
+ }
177
+ if (action.type === "inc") {
178
+ return {...state, _inc: state._inc + 1};
179
+ }
180
+ return state;
181
+ }
182
+
183
+ type GalleryEntry = {
184
+ type: string;
185
+ src: string;
186
+ zoomed: boolean;
187
+ setZoomed: (value: boolean) => void;
188
+ state: "none" | "loading" | "error" | "loaded";
189
+ width?: number;
190
+ height?: number;
191
+ };
192
+
193
+ function initGallery(imagesRegistry: Registry, toastQueue: ToastQueue, themeMode: ThemeMode) {
194
+ const items = [...imagesRegistry.values()];
195
+ const dataSource: GalleryEntry[] = items.map((item) => ({
196
+ ...item,
197
+ state: "none",
198
+ }));
199
+
200
+ const zoomedIndex = dataSource.findIndex((item) => item.zoomed);
201
+
202
+ const lightbox = new PhotoSwipeLightbox({
203
+ dataSource,
204
+ mainClass: galleryMainClass,
205
+ pswpModule: () => import("photoswipe"),
206
+ preloaderDelay: 1,
207
+ arrowPrev: false,
208
+ arrowNext: false,
209
+ close: false,
210
+ zoom: false,
211
+ loop: false,
212
+ });
213
+
214
+ lightbox.addFilter("numItems", (_numItems) => {
215
+ return dataSource.length;
216
+ });
217
+
218
+ lightbox.addFilter("itemData", (_itemData, index) => {
219
+ const item = dataSource[index];
220
+
221
+ if (item.type === "video" || item.type === "iframe") {
222
+ return item;
223
+ }
224
+
225
+ if (item.state === "loaded" || item.state === "error") {
226
+ return item;
227
+ }
228
+
229
+ return {};
230
+ });
231
+
232
+ lightbox.addFilter("isContentLoading", (_isContentLoading, content) => {
233
+ const item = dataSource[content.index];
234
+ if (item.type === "video" || item.type === "iframe") {
235
+ return false;
236
+ }
237
+ const complete = item.state === "loaded" || item.state === "error";
238
+ return !complete;
239
+ });
240
+
241
+ lightbox.addFilter("useContentPlaceholder", () => false);
242
+
243
+ lightbox.init();
244
+
245
+ lightbox.on("contentActivate", (e) => {
246
+ dataSource[e.content.index].setZoomed(true);
247
+ lightbox.pswp?.element?.focus();
248
+ });
249
+
250
+ lightbox.on("contentDeactivate", (e) => {
251
+ dataSource[e.content.index].setZoomed(false);
252
+ e.content.element?.querySelector("video")?.pause();
253
+ });
254
+
255
+ function refreshSlide(index: number) {
256
+ lightbox.pswp?.refreshSlideContent(index);
257
+ if (lightbox.pswp?.currIndex === index) {
258
+ lightbox.pswp?.updateSize(true);
259
+ }
260
+ }
261
+
262
+ const contentAppendTimeouts = [] as ReturnType<typeof setTimeout>[];
263
+ lightbox.on("contentAppend", (e) => {
264
+ // sometimes lightbox's slide is loaded with stale (empty) data
265
+ if (e.content.data !== dataSource[e.content.index]) {
266
+ contentAppendTimeouts.push(
267
+ setTimeout(() => {
268
+ refreshSlide(e.content.index);
269
+ }, 0)
270
+ );
271
+ }
272
+ });
273
+
274
+ lightbox.on("contentRemove", (e) => {
275
+ const item = dataSource[e.content.index];
276
+
277
+ if (item.type === "video" || item.type === "iframe") {
278
+ item.state = "none";
279
+ }
280
+ });
281
+
282
+ lightbox.on("contentLoad", (e) => {
283
+ const item = dataSource[e.content.index];
284
+
285
+ if (item.state === "loading") {
286
+ return;
287
+ }
288
+
289
+ if (item.state === "none") {
290
+ e.preventDefault();
291
+ const eventArgs = {
292
+ e,
293
+ item,
294
+ refreshSlide,
295
+ getPswp: () => lightbox.pswp,
296
+ };
297
+
298
+ switch (item.type) {
299
+ case "image": {
300
+ onImageContentLoad(eventArgs);
301
+ break;
302
+ }
303
+ case "video": {
304
+ onVideoContentLoad(eventArgs);
305
+ break;
306
+ }
307
+ case "iframe": {
308
+ onIframeContentLoad(eventArgs);
309
+ break;
310
+ }
311
+ default: {
312
+ throw new Error(`unexpected slide type: ${item.type}`);
313
+ }
314
+ }
315
+ } else if (item.state === "error") {
316
+ e.preventDefault();
317
+ e.content.onError();
318
+ }
319
+ });
320
+
321
+ const HIDE_UI_TIMEOUT_MS = 3000;
322
+ let hideTimeout: ReturnType<typeof setTimeout>;
323
+ let isHidden = false;
324
+
325
+ function hideUI() {
326
+ lightbox.pswp?.element?.classList.remove("pswp--ui-visible");
327
+ lightbox.pswp?.element?.classList.add("pswp--hide-cursor");
328
+ isHidden = true;
329
+ }
330
+
331
+ function showUI() {
332
+ if (isHidden) {
333
+ lightbox.pswp?.element?.classList.add("pswp--ui-visible");
334
+ lightbox.pswp?.element?.classList.remove("pswp--hide-cursor");
335
+ isHidden = false;
336
+ }
337
+ clearTimeout(hideTimeout);
338
+ hideTimeout = setTimeout(hideUI, HIDE_UI_TIMEOUT_MS);
339
+ }
340
+
341
+ lightbox.on("afterInit", () => {
342
+ const pswp = lightbox.pswp;
343
+
344
+ if (!pswp) {
345
+ return;
346
+ }
347
+
348
+ pswp.element?.addEventListener("mousemove", showUI);
349
+ pswp.element?.addEventListener("touchstart", showUI);
350
+
351
+ hideTimeout = setTimeout(hideUI, HIDE_UI_TIMEOUT_MS);
352
+ });
353
+
354
+ lightbox.on("uiRegister", () => {
355
+ lightbox.pswp?.ui?.registerElement({
356
+ name: "custom-nav",
357
+ order: 8,
358
+ isButton: false,
359
+ appendTo: "wrapper",
360
+ onInit: (el, pswp) => {
361
+ createRoot(el).render(<SlideButtons items={items} pswp={pswp} toastQueue={toastQueue} themeMode={themeMode} />);
362
+ },
363
+ });
364
+ });
365
+
366
+ lightbox.on("destroy", () => {
367
+ contentAppendTimeouts.forEach((timeoutId) => {
368
+ clearTimeout(timeoutId);
369
+ });
370
+ });
371
+
372
+ lightbox.loadAndOpen(zoomedIndex === -1 ? 0 : zoomedIndex);
373
+
374
+ return lightbox;
375
+ }
376
+
377
+ type ContentLoadArg = {
378
+ e: PhotoSwipeEventsMap["contentLoad"];
379
+ item: GalleryEntry;
380
+ refreshSlide: (index: number) => void;
381
+ getPswp: () => PhotoSwipeLightbox["pswp"];
382
+ };
383
+
384
+ function onImageContentLoad({e, item, refreshSlide}: ContentLoadArg) {
385
+ item.state = "loading";
386
+ const onLoad = () => {
387
+ item.state = "loaded";
388
+ item.width = img.naturalWidth;
389
+ item.height = img.naturalHeight;
390
+
391
+ refreshSlide(e.content.index);
392
+ e.content.onLoaded();
393
+ };
394
+
395
+ const img = new Image();
396
+ img.src = item.src;
397
+ if (img.complete) {
398
+ onLoad();
399
+ }
400
+ img.onload = () => onLoad();
401
+ img.onerror = () => {
402
+ item.state = "error";
403
+ refreshSlide(e.content.index);
404
+ e.content.onError();
405
+ };
406
+ }
407
+
408
+ function onVideoContentLoad({e, item, refreshSlide, getPswp}: ContentLoadArg) {
409
+ item.state = "loaded";
410
+
411
+ e.content.element = document.createElement("div");
412
+ e.content.element.className = "pswp__video-container";
413
+ e.content.element.onclick = () => {
414
+ getPswp()?.close();
415
+ };
416
+
417
+ const video = document.createElement("video");
418
+ video.controls = true;
419
+ video.src = item.src;
420
+ video.onerror = () => {
421
+ item.state = "error";
422
+ refreshSlide(e.content.index);
423
+ e.content.onError();
424
+ };
425
+ e.content.element.appendChild(video);
426
+ }
427
+
428
+ function onIframeContentLoad({e, item, getPswp}: ContentLoadArg) {
429
+ item.state = "loaded";
430
+
431
+ e.content.element = document.createElement("div");
432
+ e.content.element.className = "pswp__iframe-container";
433
+
434
+ e.content.element.onclick = () => {
435
+ getPswp()?.close();
436
+ };
437
+
438
+ const iframe = document.createElement("iframe");
439
+ iframe.src = item.src;
440
+ iframe.setAttribute("allowfullscreen", "");
441
+ e.content.element.appendChild(iframe);
442
+ }
@@ -0,0 +1,230 @@
1
+ import {PhotoSwipe} from "photoswipe/lightbox";
2
+ import {useEffect, useMemo, useState} from "react";
3
+ import {css} from "@linaria/core";
4
+ import {IconButton} from "../button/icon-button";
5
+ import Back from "../icons/react/Back";
6
+ import ArrowForward from "../icons/react/ArrowForward";
7
+ import LoupeZoomIn from "../icons/react/LoupeZoomIn";
8
+ import LoupeZoomOut from "../icons/react/LoupeZoomOut";
9
+ import RicheditorOpenLink from "../icons/react/RicheditorOpenLink";
10
+ import Link from "../icons/react/Link";
11
+ import Close from "../icons/react/Close";
12
+ import {colors, createInlineTheme, getThemeColors, space} from "../design-system";
13
+ import {Button} from "../button/button";
14
+ import {copyToClipboard} from "../copy-to-clipboard";
15
+ import {ToastProvider, useToast} from "../toast/toast-provider";
16
+ import {ToastQueue} from "../toast/toast-queue";
17
+ import {ActionsMenu} from "../actions-menu";
18
+ import {ActionsButtonCompact} from "../button/actions-button-compact";
19
+ import {ThemeProvider} from "../theme-provider";
20
+ import {ThemeMode} from "../theme-settings";
21
+
22
+ const barClassName = css`
23
+ position: absolute;
24
+ top: ${space.s20}px;
25
+ right: ${space.s20}px;
26
+
27
+ display: flex;
28
+ flex-direction: row;
29
+ align-items: center;
30
+ justify-content: flex-end; /* align content to the right */
31
+ gap: ${space.s8}px; /* space between children */
32
+ `;
33
+
34
+ type SlideState = {
35
+ currIndex: number;
36
+ numItems: number;
37
+ src: string;
38
+ actions?: React.ReactNode;
39
+ };
40
+
41
+ export function SlideButtons({
42
+ pswp,
43
+ toastQueue,
44
+ themeMode,
45
+ items,
46
+ }: {
47
+ pswp: PhotoSwipe;
48
+ toastQueue: ToastQueue;
49
+ themeMode: ThemeMode;
50
+ items: {
51
+ src: string;
52
+ actions?: React.ReactNode;
53
+ }[];
54
+ }) {
55
+ const [slideState, setSliceState] = useState<SlideState>(getSlideData(pswp, items));
56
+ useEffect(() => {
57
+ const handler = () => {
58
+ setSliceState(getSlideData(pswp, items));
59
+ };
60
+ pswp.on("slideActivate", handler);
61
+
62
+ return () => {
63
+ pswp.off("slideActivate", handler);
64
+ };
65
+ }, [items, pswp]);
66
+
67
+ return (
68
+ <ToastProvider toastQueue={toastQueue}>
69
+ <div className={barClassName}>
70
+ <CopyURLButton pswp={pswp} />
71
+ <ZoomButton pswp={pswp} />
72
+ {slideState.actions ? (
73
+ <SlideActionsMenu actions={slideState.actions} themeMode={themeMode} />
74
+ ) : (
75
+ <IconButton
76
+ size="medium"
77
+ color="neutral"
78
+ variant="solid"
79
+ onClick={() => window.open(slideState.src, "_blank")}
80
+ >
81
+ <RicheditorOpenLink />
82
+ </IconButton>
83
+ )}
84
+
85
+ <IconButton onClick={() => pswp.close()} size="medium" color="neutral" variant="solid">
86
+ <Close />
87
+ </IconButton>
88
+ </div>
89
+ <div className="pswp__button--arrow--prev pswp__button--arrow">
90
+ <IconButton
91
+ disabled={slideState.currIndex === 0}
92
+ onClick={() => pswp.prev()}
93
+ size="medium"
94
+ color="neutral"
95
+ variant="solid"
96
+ >
97
+ <Back />
98
+ </IconButton>
99
+ </div>
100
+ <div className="pswp__button--arrow--next pswp__button--arrow">
101
+ <IconButton
102
+ disabled={slideState.currIndex === slideState.numItems - 1}
103
+ onClick={() => pswp.next()}
104
+ size="medium"
105
+ color="neutral"
106
+ variant="solid"
107
+ >
108
+ <ArrowForward />
109
+ </IconButton>
110
+ </div>
111
+ </ToastProvider>
112
+ );
113
+ }
114
+
115
+ function SlideActionsMenu({themeMode, actions}: {themeMode: ThemeMode; actions?: React.ReactNode}) {
116
+ const [container, setContainer] = useState<HTMLDivElement | null>();
117
+ const {theme, styles} = useMemo(() => {
118
+ const theme = getThemeColors(colors.brandColors.blue, themeMode === "dark" ? "light" : "dark");
119
+ return {theme, styles: createInlineTheme(theme, emptyArray)};
120
+ }, [themeMode]);
121
+
122
+ return (
123
+ <div>
124
+ <ActionsMenu
125
+ contentStyles={css`
126
+ margin-top: ${space.s8}px;
127
+ `}
128
+ container={container}
129
+ trigger={<ActionsButtonCompact size="medium" color="neutral" variant="solid" />}
130
+ >
131
+ {actions}
132
+ </ActionsMenu>
133
+ <ThemeProvider theme={theme}>
134
+ <div style={styles} ref={setContainer}></div>
135
+ </ThemeProvider>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ function ZoomButton({pswp}: {pswp: PhotoSwipe}) {
141
+ const [zoomed, setZoomed] = useState(false);
142
+ const [isVisible, setIsVisible] = useState(false);
143
+
144
+ useEffect(() => {
145
+ if (!pswp) {
146
+ return undefined;
147
+ }
148
+
149
+ function handleZoomChange() {
150
+ const slide = pswp.currSlide;
151
+ if (!slide) {
152
+ return;
153
+ }
154
+
155
+ setIsVisible(slide.zoomLevels.fit !== 1);
156
+
157
+ const zoomed = slide.currZoomLevel > slide.zoomLevels.fit;
158
+ setZoomed(zoomed);
159
+ }
160
+
161
+ pswp.on("afterInit", handleZoomChange);
162
+ pswp.on("zoomPanUpdate", handleZoomChange);
163
+
164
+ return () => {
165
+ pswp.off("afterInit", handleZoomChange);
166
+ pswp.off("zoomPanUpdate", handleZoomChange);
167
+ };
168
+ }, [pswp]);
169
+
170
+ if (!isVisible) {
171
+ return null;
172
+ }
173
+
174
+ return (
175
+ <IconButton
176
+ onClick={() => {
177
+ setZoomed(!zoomed);
178
+ pswp.toggleZoom();
179
+ }}
180
+ size="medium"
181
+ color="neutral"
182
+ variant="solid"
183
+ >
184
+ {zoomed ? <LoupeZoomOut /> : <LoupeZoomIn />}
185
+ </IconButton>
186
+ );
187
+ }
188
+
189
+ function CopyURLButton({pswp}: {pswp: PhotoSwipe}) {
190
+ const toast = useToast();
191
+ return (
192
+ <Button
193
+ onClick={() => {
194
+ if (!pswp?.currSlide?.data.src) {
195
+ toast.error({title: "Unable to copy."});
196
+ return;
197
+ }
198
+
199
+ const src = new URL(pswp.currSlide.data.src, `${window.location}`).href;
200
+ if (src) {
201
+ copyToClipboard(src)
202
+ .then(() => {
203
+ toast.success({title: "Copied!"});
204
+ })
205
+ .catch(() => {
206
+ toast.error({title: "Unable to copy."});
207
+ });
208
+ }
209
+ }}
210
+ size="medium"
211
+ variant="solid"
212
+ color="neutral"
213
+ iconStart={<Link />}
214
+ type="button"
215
+ >
216
+ Copy file URL
217
+ </Button>
218
+ );
219
+ }
220
+
221
+ const emptyArray: string[] = [];
222
+
223
+ function getSlideData(pswp: PhotoSwipe, items: {src: string; actions?: React.ReactNode}[]) {
224
+ return {
225
+ currIndex: pswp.currIndex,
226
+ numItems: pswp.getNumItems(),
227
+ src: items[pswp.currIndex].src,
228
+ actions: items[pswp.currIndex].actions,
229
+ };
230
+ }
@@ -0,0 +1,35 @@
1
+ import React, {ReactNode, useCallback} from "react";
2
+ import "react-medium-image-zoom/dist/styles.css";
3
+
4
+ type Props = {
5
+ zoomed: boolean;
6
+ onZoomedChange: (value: boolean) => void;
7
+ children: ReactNode;
8
+ className?: string;
9
+ openOnClick?: boolean;
10
+ zoomImg?: {src: string; alt?: string};
11
+ actions?: ReactNode;
12
+ };
13
+
14
+ export const ZoomV2 = (props: Props) => {
15
+ const {zoomed, onZoomedChange, children, className, openOnClick} = props;
16
+ const onClick = useCallback(() => {
17
+ if (zoomed) {
18
+ onZoomedChange(false);
19
+ } else if (openOnClick) {
20
+ onZoomedChange(true);
21
+ }
22
+ }, [zoomed, onZoomedChange, openOnClick]);
23
+
24
+ const onDoubleClick = useCallback(() => {
25
+ if (!zoomed) {
26
+ onZoomedChange(true);
27
+ }
28
+ }, [zoomed, onZoomedChange]);
29
+
30
+ return (
31
+ <div className={className} onClick={onClick} onDoubleClick={openOnClick ? undefined : onDoubleClick}>
32
+ {children}
33
+ </div>
34
+ );
35
+ };