@fibery/ui-kit 1.37.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 (194) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/config/jest/testFrameworkSetup.js +1 -1
  3. package/package.json +20 -16
  4. package/src/abbreviate-name.ts +42 -0
  5. package/src/actions-menu/actions-menu-item.tsx +32 -11
  6. package/src/actions-menu/actions-menu-props.tsx +5 -0
  7. package/src/actions-menu/actions-menu-sub-command-menu.tsx +7 -4
  8. package/src/actions-menu/actions-menu-sub-menu.tsx +9 -3
  9. package/src/actions-menu/actions-menu.tsx +28 -9
  10. package/src/actions-menu/context-actions-menu.tsx +22 -0
  11. package/src/actions-panel.tsx +12 -2
  12. package/src/animated-height-container.tsx +24 -5
  13. package/src/antd/ant-upload.tsx +1 -1
  14. package/src/antd/input.tsx +4 -0
  15. package/src/avatar.tsx +1 -27
  16. package/src/axis-header.tsx +136 -76
  17. package/src/box.tsx +1 -1
  18. package/src/breadcrumb.tsx +78 -0
  19. package/src/button/actions-button-compact.tsx +4 -1
  20. package/src/button/base-button.tsx +7 -3
  21. package/src/button/button-group.tsx +1 -1
  22. package/src/button/button.tsx +33 -1
  23. package/src/button/icon-button.tsx +10 -1
  24. package/src/button/select-button.tsx +4 -0
  25. package/src/card-container.tsx +10 -6
  26. package/src/checkbox.tsx +17 -0
  27. package/src/collapsible-section.tsx +3 -1
  28. package/src/color-picker/swatch.tsx +1 -0
  29. package/src/command-menu/index.tsx +3 -3
  30. package/src/context-menu/index.tsx +10 -1
  31. package/src/date-picker/autocomplete.tsx +11 -5
  32. package/src/date-picker/date-range-input.tsx +5 -1
  33. package/src/date-picker/date-range-picker-popup-content.tsx +31 -13
  34. package/src/date-picker/date-range-picker.tsx +6 -3
  35. package/src/date-picker/relative-date-picker.tsx +6 -2
  36. package/src/date-picker/single-date-input.tsx +5 -1
  37. package/src/date-picker/single-date-picker.tsx +8 -5
  38. package/src/date-picker/styles.ts +52 -26
  39. package/src/date-picker/types.ts +3 -1
  40. package/src/day-select/day-select.tsx +21 -75
  41. package/src/day-select/iso-week-day-select.tsx +1 -1
  42. package/src/day-select/mount-day-select.tsx +2 -2
  43. package/src/day-select/week-day-select.tsx +1 -5
  44. package/src/design-system.ts +45 -12
  45. package/src/dropdown-menu/index.tsx +19 -2
  46. package/src/emoji-picker/app-icon-picker.tsx +6 -4
  47. package/src/emoji-picker/emoji-picker-preview-footer.tsx +5 -1
  48. package/src/emoji-picker/emoji-picker.tsx +3 -0
  49. package/src/emoji-picker/emoji.tsx +5 -3
  50. package/src/emoji-picker/icon-emoji-picker.tsx +10 -1
  51. package/src/emoji-picker/primitives/emoji.tsx +49 -51
  52. package/src/emoji-picker/primitives/footer.tsx +5 -0
  53. package/src/emoji-picker/primitives/search.tsx +8 -0
  54. package/src/emoji-picker/primitives/skin-tone.tsx +12 -1
  55. package/src/emoji-picker/use-responsive-emoji-size.ts +11 -0
  56. package/src/emoji-picker/utils/emoji-set.ts +2 -1
  57. package/src/empty-state/empty-state.tsx +6 -0
  58. package/src/fibermoji-placeholder.tsx +1 -27
  59. package/src/field-icon.tsx +4 -1
  60. package/src/file-item-2.tsx +408 -0
  61. package/src/file-item.tsx +215 -47
  62. package/src/header-layout.tsx +11 -0
  63. package/src/html-styles.ts +21 -4
  64. package/src/icons/ast/ActivityFilled.ts +8 -0
  65. package/src/icons/ast/AiAssistantFilled.ts +8 -0
  66. package/src/icons/ast/Back.ts +1 -1
  67. package/src/icons/ast/CollapseAllHorizontal.ts +8 -0
  68. package/src/icons/ast/DateRange.ts +1 -1
  69. package/src/icons/ast/EntityCreateFilled.ts +8 -0
  70. package/src/icons/ast/ExpandAllHorizontal.ts +8 -0
  71. package/src/icons/ast/ExtensionFiles.ts +1 -1
  72. package/src/icons/ast/FileUpload.ts +1 -1
  73. package/src/icons/ast/GlobeUnpublish.ts +8 -0
  74. package/src/icons/ast/HomeFilled.ts +8 -0
  75. package/src/icons/ast/KeyboardHideDown.ts +8 -0
  76. package/src/icons/ast/LayoutMultiple.ts +8 -0
  77. package/src/icons/ast/RicheditorHistoryRedo.ts +1 -1
  78. package/src/icons/ast/RicheditorHistoryUndo.ts +1 -1
  79. package/src/icons/ast/SearchFilled.ts +8 -0
  80. package/src/icons/ast/SettingsFilled.ts +8 -0
  81. package/src/icons/ast/SlideMenuFilled.ts +8 -0
  82. package/src/icons/ast/SwitchToMention.ts +8 -0
  83. package/src/icons/ast/SwitchToText.ts +8 -0
  84. package/src/icons/ast/SwitchToWidget.ts +8 -0
  85. package/src/icons/ast/TypeDate.ts +1 -1
  86. package/src/icons/ast/ViewGallery.ts +8 -0
  87. package/src/icons/ast/index.tsx +16 -0
  88. package/src/icons/react/ActivityFilled.tsx +13 -0
  89. package/src/icons/react/AiAssistantFilled.tsx +13 -0
  90. package/src/icons/react/CollapseAllHorizontal.tsx +13 -0
  91. package/src/icons/react/EntityCreateFilled.tsx +13 -0
  92. package/src/icons/react/ExpandAllHorizontal.tsx +13 -0
  93. package/src/icons/react/GlobeUnpublish.tsx +13 -0
  94. package/src/icons/react/HomeFilled.tsx +13 -0
  95. package/src/icons/react/KeyboardHideDown.tsx +13 -0
  96. package/src/icons/react/LayoutMultiple.tsx +13 -0
  97. package/src/icons/react/SearchFilled.tsx +13 -0
  98. package/src/icons/react/SettingsFilled.tsx +13 -0
  99. package/src/icons/react/SlideMenuFilled.tsx +13 -0
  100. package/src/icons/react/SwitchToMention.tsx +13 -0
  101. package/src/icons/react/SwitchToText.tsx +13 -0
  102. package/src/icons/react/SwitchToWidget.tsx +13 -0
  103. package/src/icons/react/ViewGallery.tsx +13 -0
  104. package/src/icons/react/index.tsx +16 -0
  105. package/src/icons/svg/activity-filled.svg +4 -0
  106. package/src/icons/svg/ai-assistant-filled.svg +3 -0
  107. package/src/icons/svg/back.svg +3 -1
  108. package/src/icons/svg/collapse-all-horizontal.svg +3 -0
  109. package/src/icons/svg/date-range.svg +3 -3
  110. package/src/icons/svg/entity-create-filled.svg +4 -0
  111. package/src/icons/svg/expand-all-horizontal.svg +3 -0
  112. package/src/icons/svg/extension/files.svg +3 -1
  113. package/src/icons/svg/file-upload.svg +3 -6
  114. package/src/icons/svg/globe-unpublish.svg +4 -0
  115. package/src/icons/svg/home-filled.svg +3 -0
  116. package/src/icons/svg/keyboard-hide-down.svg +1 -0
  117. package/src/icons/svg/layout-multiple.svg +3 -0
  118. package/src/icons/svg/richeditor/history/redo.svg +3 -2
  119. package/src/icons/svg/richeditor/history/undo.svg +3 -2
  120. package/src/icons/svg/search-filled.svg +3 -0
  121. package/src/icons/svg/settings-filled.svg +3 -0
  122. package/src/icons/svg/slide-menu-filled.svg +1 -0
  123. package/src/icons/svg/switch-to-mention.svg +3 -0
  124. package/src/icons/svg/switch-to-text.svg +4 -0
  125. package/src/icons/svg/switch-to-widget.svg +4 -0
  126. package/src/icons/svg/type/date.svg +3 -2
  127. package/src/icons/svg/view-gallery.svg +4 -0
  128. package/src/images-gallery/images-gallery-fall-through-provider.tsx +11 -0
  129. package/src/images-gallery/images-gallery.tsx +257 -0
  130. package/src/images-gallery/zoom.tsx +123 -0
  131. package/src/images-gallery-2/images-gallery-2.tsx +442 -0
  132. package/src/images-gallery-2/slide-buttons.tsx +230 -0
  133. package/src/images-gallery-2/zoom.tsx +35 -0
  134. package/src/is-in-popup.ts +1 -1
  135. package/src/kbd.tsx +46 -0
  136. package/src/layout-styles.ts +21 -1
  137. package/src/lists/actions-menu-row-surface.tsx +3 -1
  138. package/src/lists/list-row-surface.tsx +7 -1
  139. package/src/mobile-keyboard-aware-popup.tsx +87 -0
  140. package/src/mobile-styles.ts +8 -0
  141. package/src/modal-menu/contexts/modal-menu-context.tsx +22 -0
  142. package/src/modal-menu/contexts/modal-submenu-context.tsx +16 -0
  143. package/src/modal-menu/index.tsx +14 -0
  144. package/src/modal-menu/modal-menu-content.tsx +68 -0
  145. package/src/modal-menu/modal-menu-group.tsx +10 -0
  146. package/src/modal-menu/modal-menu-item.tsx +45 -0
  147. package/src/modal-menu/modal-menu-label.tsx +24 -0
  148. package/src/modal-menu/modal-menu-root.tsx +57 -0
  149. package/src/modal-menu/modal-menu-separator.tsx +14 -0
  150. package/src/modal-menu/modal-menu-trigger.tsx +54 -0
  151. package/src/modal-menu/modal-submenu-content.tsx +49 -0
  152. package/src/modal-menu/modal-submenu-trigger.tsx +34 -0
  153. package/src/modal-menu/modal-submenu.tsx +17 -0
  154. package/src/notification-dot.tsx +16 -7
  155. package/src/online-users.tsx +3 -3
  156. package/src/palette.ts +1 -1
  157. package/src/platform-symbols.ts +9 -0
  158. package/src/platform.ts +2 -0
  159. package/src/popover/index.tsx +710 -0
  160. package/src/popover/mobile-popup-context.tsx +29 -0
  161. package/src/{popup → popover}/modifiers.tsx +25 -25
  162. package/src/{popup → popover}/popup-modifiers-context.ts +3 -3
  163. package/src/reactions/reaction-button.tsx +342 -135
  164. package/src/reactions/reaction-picker.tsx +51 -44
  165. package/src/reactions/reactions.tsx +11 -1
  166. package/src/reactions/tootltip.tsx +55 -0
  167. package/src/rich-text-embed-error.tsx +2 -1
  168. package/src/select/components/group-heading.tsx +8 -0
  169. package/src/select/components/menu-list-virtualized.tsx +6 -2
  170. package/src/select/components/option.tsx +4 -0
  171. package/src/select/index.tsx +8 -4
  172. package/src/select/select-control-settings-context.tsx +11 -2
  173. package/src/select/select-in-popover.tsx +30 -39
  174. package/src/select/styles.ts +21 -1
  175. package/src/tab-nav/tab-nav.tsx +36 -10
  176. package/src/toast/primitives.tsx +8 -1
  177. package/src/toggle-button/round-toggle-button.tsx +23 -0
  178. package/src/toggle-button/toggle-button-group.tsx +34 -0
  179. package/src/toggle-button/toggle-button.tsx +164 -0
  180. package/src/toggle.tsx +11 -3
  181. package/src/tooltip.tsx +29 -20
  182. package/src/type-badge.tsx +5 -2
  183. package/src/unit/styles.ts +2 -2
  184. package/src/unit/unit-with-tooltip.tsx +1 -1
  185. package/src/use-is-phone.tsx +50 -4
  186. package/src/use-is-support-hover.ts +1 -1
  187. package/src/use-on-screen-keyboard-data.tsx +66 -0
  188. package/src/workflow-progress-icon.tsx +16 -22
  189. package/src/popup/index.tsx +0 -383
  190. package/src/shortcut-badge.tsx +0 -29
  191. /package/src/{popup → popover}/popup-stack-context.tsx +0 -0
  192. /package/src/{popup → popover}/reference.ts +0 -0
  193. /package/src/{popup → popover}/styles.ts +0 -0
  194. /package/src/{popup → popover}/use-click-outside.ts +0 -0
@@ -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
+ };
@@ -1,4 +1,4 @@
1
- import {basicPopupClassName} from "./popup/styles";
1
+ import {basicPopupClassName} from "./popover/styles";
2
2
  import {$TSFixMe} from "./tsfixme";
3
3
 
4
4
  const selectors = [