@fibery/ui-kit 1.38.1 → 1.39.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 (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +12 -12
  3. package/src/button/button.tsx +1 -2
  4. package/src/card-container.tsx +5 -2
  5. package/src/collapsible/index.tsx +4 -2
  6. package/src/collapsible-section.tsx +1 -0
  7. package/src/design-system.ts +2 -1
  8. package/src/fibermoji-placeholder.tsx +1 -2
  9. package/src/file-item-2.tsx +188 -30
  10. package/src/file-item.tsx +172 -189
  11. package/src/icons/ast/FileArchive.ts +8 -0
  12. package/src/icons/ast/FileBroken.ts +8 -0
  13. package/src/icons/ast/FileDocument.ts +8 -0
  14. package/src/icons/ast/FileImage.ts +8 -0
  15. package/src/icons/ast/FilePresentation.ts +8 -0
  16. package/src/icons/ast/FileTable.ts +8 -0
  17. package/src/icons/ast/FileVideo.ts +8 -0
  18. package/src/icons/ast/index.tsx +7 -0
  19. package/src/icons/react/FileArchive.tsx +13 -0
  20. package/src/icons/react/FileBroken.tsx +13 -0
  21. package/src/icons/react/FileDocument.tsx +13 -0
  22. package/src/icons/react/FileImage.tsx +13 -0
  23. package/src/icons/react/FilePresentation.tsx +13 -0
  24. package/src/icons/react/FileTable.tsx +13 -0
  25. package/src/icons/react/FileVideo.tsx +13 -0
  26. package/src/icons/react/index.tsx +7 -0
  27. package/src/icons/svg/file-archive.svg +3 -0
  28. package/src/icons/svg/file-broken.svg +3 -0
  29. package/src/icons/svg/file-document.svg +3 -0
  30. package/src/icons/svg/file-image.svg +3 -0
  31. package/src/icons/svg/file-presentation.svg +3 -0
  32. package/src/icons/svg/file-table.svg +3 -0
  33. package/src/icons/svg/file-video.svg +3 -0
  34. package/src/images-gallery-2/images-gallery-2.tsx +105 -25
  35. package/src/images-gallery-2/slide-buttons.tsx +99 -58
  36. package/src/kbd.tsx +1 -1
  37. package/src/loading-sausage.tsx +2 -2
  38. package/src/tooltip.tsx +1 -1
  39. package/src/use-is-phone.tsx +20 -12
  40. package/src/use-on-screen-keyboard-data.tsx +3 -4
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 14">
2
+ <path d="M13.75 0C14.9926 0 16 1.00736 16 2.25V11.75C16 12.9926 14.9926 14 13.75 14H2.25C1.00736 14 0 12.9926 0 11.75V2.25C0 1.00736 1.00736 0 2.25 0H13.75ZM2.25 1.5C1.83579 1.5 1.5 1.83579 1.5 2.25V11.75C1.5 12.1642 1.83579 12.5 2.25 12.5H13.75C14.1642 12.5 14.5 12.1642 14.5 11.75V2.25C14.5 1.83579 14.1642 1.5 13.75 1.5H2.25ZM12.1465 10.1465C12.4613 10.4614 12.2383 10.9999 11.793 11H4.20703C3.76165 10.9999 3.53861 10.4615 3.85352 10.1465L6 8L7 9L9 7L12.1465 10.1465ZM12 3C12.5523 3.00002 13 3.44773 13 4C13 4.55227 12.5523 4.99998 12 5C11.4477 4.99998 11 4.55227 11 4C11 3.44773 11.4477 3.00002 12 3Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
2
+ <path d="M12.7617 16.5723L12.8184 16.6348C13.0805 16.9633 13.0612 17.4486 12.7617 17.7549C12.4622 18.0611 11.9874 18.0805 11.666 17.8125L11.6045 17.7549L10.001 16.1152L8.39746 17.7549C8.07799 18.0812 7.56066 18.0812 7.24121 17.7549C6.9217 17.4282 6.92172 16.8989 7.24121 16.5723L8.77832 15H4.25C3.00736 15 2 13.9926 2 12.75V5.25C2 4.00736 3.00736 3 4.25 3H15.75C16.9926 3 18 4.00736 18 5.25V12.75C18 13.9926 16.9926 15 15.75 15H11.2236L12.7617 16.5723ZM4.25 4.5C3.83579 4.5 3.5 4.83579 3.5 5.25V12.75C3.5 13.1642 3.83579 13.5 4.25 13.5H15.75C16.1642 13.5 16.5 13.1642 16.5 12.75V5.25C16.5 4.83579 16.1642 4.5 15.75 4.5H4.25ZM10 9H12C12 9.39556 11.8828 9.78243 11.6631 10.1113C11.4434 10.4401 11.1309 10.6963 10.7656 10.8477C10.4002 10.999 9.99734 11.0391 9.60938 10.9619C9.22155 10.8847 8.86556 10.6937 8.58594 10.4141C8.30632 10.1344 8.1153 9.77845 8.03809 9.39062C7.96092 9.00266 8.00097 8.59983 8.15234 8.23438C8.30372 7.86906 8.55988 7.55662 8.88867 7.33691C9.21757 7.11715 9.60444 7 10 7V9Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" >
2
+ <path d="M11.7422 1.5C11.9405 1.5 12.131 1.5787 12.2715 1.71875L16.7764 6.21582C16.9173 6.35652 16.9971 6.54787 16.9971 6.74707V9.94141C16.9986 9.96076 17 9.98026 17 10C17 10.0194 16.9985 10.0386 16.9971 10.0576V13.9414C16.9986 13.9608 17 13.9803 17 14C17 14.0194 16.9985 14.0386 16.9971 14.0576V15.75C16.9971 17.2687 15.766 18.4999 14.248 18.5H5.74512C4.22716 18.4999 2.99708 17.2687 2.99707 15.75V4.25C2.99707 2.73127 4.22713 1.50008 5.74512 1.5H11.7422ZM8.75 17H14.248C14.938 16.9999 15.4971 16.4403 15.4971 15.75V14.75H8.75V17ZM4.49609 15.75C4.4961 16.4403 5.05519 16.9999 5.74512 17H7.25V14.75H4.49609V15.75ZM8.75 13.25H15.4971V10.75H8.75V13.25ZM4.49609 13.25H7.25V10.75H4.49609V13.25ZM5.74512 3C5.05516 3.00008 4.49609 3.55969 4.49609 4.25V9.25H15.4971V7.49707H12.5752C12.1555 7.49702 11.7528 7.33009 11.4561 7.0332C11.1593 6.73627 10.9932 6.33301 10.9932 5.91309V3H5.74512ZM12.4922 5.91309C12.4922 5.93519 12.501 5.95703 12.5166 5.97266C12.5322 5.98808 12.5533 5.99702 12.5752 5.99707H14.4346L12.4922 4.05762V5.91309Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
2
+ <path d="M15.75 4C16.9926 4 18 5.00736 18 6.25V13.75C18 14.9926 16.9926 16 15.75 16H4.25C3.00736 16 2 14.9926 2 13.75V6.25C2 5.00736 3.00736 4 4.25 4H15.75ZM4.25 5.5C3.83579 5.5 3.5 5.83579 3.5 6.25V13.75C3.5 14.1642 3.83579 14.5 4.25 14.5H15.75C16.1642 14.5 16.5 14.1642 16.5 13.75V6.25C16.5 5.83579 16.1642 5.5 15.75 5.5H4.25ZM7.5 7.80859C7.50028 7.43719 7.89134 7.19532 8.22363 7.36133L12.6055 9.55273C12.974 9.737 12.974 10.263 12.6055 10.4473L8.22363 12.6377C7.89133 12.8038 7.50034 12.5628 7.5 12.1914V7.80859Z" />
3
+ </svg>
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import PhotoSwipeLightbox, {type PhotoSwipeEventsMap} from "photoswipe/lightbox";
2
3
  import "photoswipe/style.css";
3
4
  import {
@@ -9,17 +10,20 @@ import {PropsWithChildren, useCallback, useEffect, useMemo, useReducer, useRef,
9
10
  import {css} from "@linaria/core";
10
11
  import {createRoot} from "react-dom/client";
11
12
  import {space, themeVars} from "../design-system";
13
+ import {safeAreaInsetLeftVar, safeAreaInsetRightVar, safeAreaInsetTopVar} from "../mobile-styles";
12
14
  import {useToast} from "../toast/toast-provider";
13
15
  import {ToastQueue} from "../toast/toast-queue";
14
16
  import {useThemeMode} from "../theme-provider";
15
17
  import {ThemeMode} from "../theme-settings";
16
18
  import {SlideButtons} from "./slide-buttons";
19
+ import {useIsPhone} from "../use-is-phone";
17
20
 
18
21
  type Registry = Map<
19
22
  string,
20
23
  {
21
24
  src: string;
22
25
  type: "image" | "video" | "iframe";
26
+ contentType?: string;
23
27
  actions?: React.ReactNode;
24
28
  zoomed: boolean;
25
29
  setZoomed: (zoomed: boolean) => void;
@@ -37,13 +41,13 @@ type Methods = {
37
41
  const galleryMainClass = css`
38
42
  z-index: 1000;
39
43
  & .pswp__button--arrow--prev {
40
- left: ${space.s20}px;
44
+ left: calc(${space.s20}px + var(${safeAreaInsetLeftVar}));
41
45
  height: ${space.s28}px;
42
46
  width: ${space.s28}px;
43
47
  margin-top: -${space.s14}px;
44
48
  }
45
49
  & .pswp__button--arrow--next {
46
- right: ${space.s20}px;
50
+ right: calc(${space.s20}px + var(${safeAreaInsetRightVar}));
47
51
  height: ${space.s28}px;
48
52
  width: ${space.s28}px;
49
53
  margin-top: -${space.s14}px;
@@ -73,7 +77,7 @@ const galleryMainClass = css`
73
77
  }
74
78
 
75
79
  & .pswp__iframe-container {
76
- padding: 0 ${space.s60}px 0 ${space.s60}px;
80
+ padding: ${space.s60}px;
77
81
  display: flex;
78
82
  justify-content: center;
79
83
  align-items: center;
@@ -82,13 +86,17 @@ const galleryMainClass = css`
82
86
  & .pswp__iframe-container iframe {
83
87
  width: 100%;
84
88
  height: 100%;
85
- max-width: min(822px, calc(100vw - 40px));
86
89
  object-fit: contain;
87
90
  display: block;
88
91
  color: ${themeVars.textColor};
89
92
  background-color: ${themeVars.colorBgPopup};
90
93
  border: none;
91
94
  }
95
+
96
+ & .pswp__top-bar {
97
+ top: var(${safeAreaInsetTopVar});
98
+ left: var(${safeAreaInsetLeftVar});
99
+ }
92
100
  `;
93
101
 
94
102
  export function ImagesGalleryProvider2({children}: PropsWithChildren) {
@@ -132,10 +140,11 @@ export function ImagesGalleryProvider2({children}: PropsWithChildren) {
132
140
 
133
141
  const toast = useToast();
134
142
  const themeMode = useThemeMode();
143
+ const isPhone = useIsPhone();
135
144
 
136
145
  useEffect(() => {
137
146
  if (galleryState.activated) {
138
- const value = initGallery(imagesRegistry, toast, themeMode);
147
+ const value = initGallery(imagesRegistry, {toastQueue: toast, isPhone, themeMode});
139
148
  value.on("close", () => {
140
149
  dispatchState({type: "set-activated", payload: false});
141
150
  markAllNotZoomed();
@@ -149,7 +158,7 @@ export function ImagesGalleryProvider2({children}: PropsWithChildren) {
149
158
  lightbox.current?.destroy();
150
159
  lightbox.current = null;
151
160
  };
152
- }, [galleryState.activated, imagesRegistry, markAllNotZoomed, toast, themeMode]);
161
+ }, [galleryState.activated, imagesRegistry, markAllNotZoomed, toast, themeMode, isPhone]);
153
162
 
154
163
  return (
155
164
  <ImagesGalleryRegistryProvider value={imagesRegistry}>
@@ -183,6 +192,7 @@ function imagesGalleryReducer(state: State, action: Actions) {
183
192
  type GalleryEntry = {
184
193
  type: string;
185
194
  src: string;
195
+ contentType: string | null;
186
196
  zoomed: boolean;
187
197
  setZoomed: (value: boolean) => void;
188
198
  state: "none" | "loading" | "error" | "loaded";
@@ -190,11 +200,15 @@ type GalleryEntry = {
190
200
  height?: number;
191
201
  };
192
202
 
193
- function initGallery(imagesRegistry: Registry, toastQueue: ToastQueue, themeMode: ThemeMode) {
203
+ function initGallery(
204
+ imagesRegistry: Registry,
205
+ {toastQueue, themeMode, isPhone}: {toastQueue: ToastQueue; themeMode: ThemeMode; isPhone: boolean}
206
+ ) {
194
207
  const items = [...imagesRegistry.values()];
195
208
  const dataSource: GalleryEntry[] = items.map((item) => ({
196
209
  ...item,
197
210
  state: "none",
211
+ contentType: item.contentType ?? null,
198
212
  }));
199
213
 
200
214
  const zoomedIndex = dataSource.findIndex((item) => item.zoomed);
@@ -293,6 +307,7 @@ function initGallery(imagesRegistry: Registry, toastQueue: ToastQueue, themeMode
293
307
  item,
294
308
  refreshSlide,
295
309
  getPswp: () => lightbox.pswp,
310
+ isPhone,
296
311
  };
297
312
 
298
313
  switch (item.type) {
@@ -319,10 +334,14 @@ function initGallery(imagesRegistry: Registry, toastQueue: ToastQueue, themeMode
319
334
  });
320
335
 
321
336
  const HIDE_UI_TIMEOUT_MS = 3000;
337
+ let forceShowUI = false;
322
338
  let hideTimeout: ReturnType<typeof setTimeout>;
323
339
  let isHidden = false;
324
340
 
325
341
  function hideUI() {
342
+ if (forceShowUI) {
343
+ return;
344
+ }
326
345
  lightbox.pswp?.element?.classList.remove("pswp--ui-visible");
327
346
  lightbox.pswp?.element?.classList.add("pswp--hide-cursor");
328
347
  isHidden = true;
@@ -351,6 +370,17 @@ function initGallery(imagesRegistry: Registry, toastQueue: ToastQueue, themeMode
351
370
  hideTimeout = setTimeout(hideUI, HIDE_UI_TIMEOUT_MS);
352
371
  });
353
372
 
373
+ const uiHandlers = {
374
+ showUI,
375
+ setForceShowUI: (value: boolean) => {
376
+ forceShowUI = value;
377
+ // resets timer
378
+ if (!forceShowUI) {
379
+ showUI();
380
+ }
381
+ },
382
+ };
383
+
354
384
  lightbox.on("uiRegister", () => {
355
385
  lightbox.pswp?.ui?.registerElement({
356
386
  name: "custom-nav",
@@ -358,7 +388,15 @@ function initGallery(imagesRegistry: Registry, toastQueue: ToastQueue, themeMode
358
388
  isButton: false,
359
389
  appendTo: "wrapper",
360
390
  onInit: (el, pswp) => {
361
- createRoot(el).render(<SlideButtons items={items} pswp={pswp} toastQueue={toastQueue} themeMode={themeMode} />);
391
+ createRoot(el).render(
392
+ <SlideButtons
393
+ items={items}
394
+ pswp={pswp}
395
+ toastQueue={toastQueue}
396
+ themeMode={themeMode}
397
+ uiHandlers={uiHandlers}
398
+ />
399
+ );
362
400
  },
363
401
  });
364
402
  });
@@ -379,33 +417,62 @@ type ContentLoadArg = {
379
417
  item: GalleryEntry;
380
418
  refreshSlide: (index: number) => void;
381
419
  getPswp: () => PhotoSwipeLightbox["pswp"];
420
+ isPhone: boolean;
382
421
  };
383
422
 
384
423
  function onImageContentLoad({e, item, refreshSlide}: ContentLoadArg) {
385
424
  item.state = "loading";
386
- const onLoad = () => {
425
+ // we ok if content type request fails
426
+ const contentTypePromise = item.contentType
427
+ ? Promise.resolve(item.contentType)
428
+ : fetch(item.src, {method: "HEAD"})
429
+ .then((response) => response.headers.get("content-type"))
430
+ .catch();
431
+
432
+ const contentPromise = new Promise<{width: number; height: number}>((resolve, reject) => {
433
+ const onLoad = () => {
434
+ resolve({
435
+ width: img.naturalWidth,
436
+ height: img.naturalHeight,
437
+ });
438
+ };
439
+
440
+ const img = new Image();
441
+ img.src = item.src;
442
+ if (img.complete) {
443
+ onLoad();
444
+ }
445
+ img.onload = () => onLoad();
446
+ img.onerror = () => {
447
+ reject("error");
448
+ };
449
+ });
450
+
451
+ Promise.allSettled([contentTypePromise, contentPromise]).then(([contentTypeResult, contentResult]) => {
452
+ if (contentResult.status === "rejected") {
453
+ item.state = "error";
454
+ refreshSlide(e.content.index);
455
+ e.content.onError();
456
+ return;
457
+ }
458
+
387
459
  item.state = "loaded";
388
- item.width = img.naturalWidth;
389
- item.height = img.naturalHeight;
460
+ item.width = contentResult.value.width;
461
+ item.height = contentResult.value.height;
462
+
463
+ // normalize
464
+ if (contentTypeResult.status === "fulfilled" && contentTypeResult.value?.startsWith("image/svg")) {
465
+ const scale = Math.min(3000 / item.width!, 3000 / item.height!);
466
+ item.width = Math.ceil(scale * item.width!);
467
+ item.height = Math.ceil(scale * item.height!);
468
+ }
390
469
 
391
470
  refreshSlide(e.content.index);
392
471
  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
- };
472
+ });
406
473
  }
407
474
 
408
- function onVideoContentLoad({e, item, refreshSlide, getPswp}: ContentLoadArg) {
475
+ function onVideoContentLoad({e, item, refreshSlide, getPswp, isPhone}: ContentLoadArg) {
409
476
  item.state = "loaded";
410
477
 
411
478
  e.content.element = document.createElement("div");
@@ -422,6 +489,19 @@ function onVideoContentLoad({e, item, refreshSlide, getPswp}: ContentLoadArg) {
422
489
  refreshSlide(e.content.index);
423
490
  e.content.onError();
424
491
  };
492
+ video.onclick = (e) => {
493
+ e.preventDefault();
494
+ e.stopPropagation();
495
+ if (isPhone) {
496
+ return;
497
+ }
498
+ if (video.paused) {
499
+ video.play().catch();
500
+ } else {
501
+ video.pause();
502
+ }
503
+ };
504
+
425
505
  e.content.element.appendChild(video);
426
506
  }
427
507
 
@@ -10,6 +10,7 @@ import RicheditorOpenLink from "../icons/react/RicheditorOpenLink";
10
10
  import Link from "../icons/react/Link";
11
11
  import Close from "../icons/react/Close";
12
12
  import {colors, createInlineTheme, getThemeColors, space} from "../design-system";
13
+ import {safeAreaInsetRightVar, safeAreaInsetTopVar} from "../mobile-styles";
13
14
  import {Button} from "../button/button";
14
15
  import {copyToClipboard} from "../copy-to-clipboard";
15
16
  import {ToastProvider, useToast} from "../toast/toast-provider";
@@ -21,8 +22,8 @@ import {ThemeMode} from "../theme-settings";
21
22
 
22
23
  const barClassName = css`
23
24
  position: absolute;
24
- top: ${space.s20}px;
25
- right: ${space.s20}px;
25
+ top: calc(${space.s20}px + var(${safeAreaInsetTopVar}));
26
+ right: calc(${space.s20}px + var(${safeAreaInsetRightVar}));
26
27
 
27
28
  display: flex;
28
29
  flex-direction: row;
@@ -31,6 +32,11 @@ const barClassName = css`
31
32
  gap: ${space.s8}px; /* space between children */
32
33
  `;
33
34
 
35
+ const wrapper = css`
36
+ width: 100%;
37
+ height: 100%;
38
+ `;
39
+
34
40
  type SlideState = {
35
41
  currIndex: number;
36
42
  numItems: number;
@@ -41,8 +47,8 @@ type SlideState = {
41
47
  export function SlideButtons({
42
48
  pswp,
43
49
  toastQueue,
44
- themeMode,
45
50
  items,
51
+ uiHandlers,
46
52
  }: {
47
53
  pswp: PhotoSwipe;
48
54
  toastQueue: ToastQueue;
@@ -51,6 +57,10 @@ export function SlideButtons({
51
57
  src: string;
52
58
  actions?: React.ReactNode;
53
59
  }[];
60
+ uiHandlers: {
61
+ showUI: () => void;
62
+ setForceShowUI: (value: boolean) => void;
63
+ };
54
64
  }) {
55
65
  const [slideState, setSliceState] = useState<SlideState>(getSlideData(pswp, items));
56
66
  useEffect(() => {
@@ -64,80 +74,109 @@ export function SlideButtons({
64
74
  };
65
75
  }, [items, pswp]);
66
76
 
77
+ const {theme, styles} = useMemo(() => {
78
+ const theme = getThemeColors(colors.brandColors.blue, "dark");
79
+ return {theme, styles: createInlineTheme(theme, emptyArray)};
80
+ }, []);
81
+
67
82
  return (
68
83
  <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>
84
+ <ThemeProvider theme={theme}>
85
+ <div style={styles} className={wrapper}>
86
+ <div className={barClassName}>
87
+ <CopyURLButton pswp={pswp} showUI={uiHandlers.showUI} />
88
+ <ZoomButton pswp={pswp} showUI={uiHandlers.showUI} />
89
+ {slideState.actions ? (
90
+ <SlideActionsMenu actions={slideState.actions} uiHandlers={uiHandlers} />
91
+ ) : (
92
+ <IconButton
93
+ size="medium"
94
+ color="neutral"
95
+ variant="soft"
96
+ onClick={() => {
97
+ window.open(slideState.src, "_blank");
98
+ uiHandlers.showUI();
99
+ }}
100
+ >
101
+ <RicheditorOpenLink />
102
+ </IconButton>
103
+ )}
104
+
105
+ <IconButton
106
+ onClick={() => {
107
+ pswp.close();
108
+ uiHandlers.showUI();
109
+ }}
110
+ size="medium"
111
+ color="neutral"
112
+ variant="soft"
113
+ >
114
+ <Close />
115
+ </IconButton>
116
+ </div>
117
+ <div className="pswp__button--arrow--prev pswp__button--arrow">
118
+ <IconButton
119
+ disabled={slideState.currIndex === 0}
120
+ onClick={() => {
121
+ pswp.prev();
122
+ uiHandlers.showUI();
123
+ }}
124
+ size="medium"
125
+ color="neutral"
126
+ variant="soft"
127
+ >
128
+ <Back />
129
+ </IconButton>
130
+ </div>
131
+ <div className="pswp__button--arrow--next pswp__button--arrow">
132
+ <IconButton
133
+ disabled={slideState.currIndex === slideState.numItems - 1}
134
+ onClick={() => {
135
+ pswp.next();
136
+ uiHandlers.showUI();
137
+ }}
138
+ size="medium"
139
+ color="neutral"
140
+ variant="soft"
141
+ >
142
+ <ArrowForward />
143
+ </IconButton>
144
+ </div>
145
+ </div>
146
+ </ThemeProvider>
111
147
  </ToastProvider>
112
148
  );
113
149
  }
114
150
 
115
- function SlideActionsMenu({themeMode, actions}: {themeMode: ThemeMode; actions?: React.ReactNode}) {
151
+ function SlideActionsMenu({
152
+ actions,
153
+ uiHandlers,
154
+ }: {
155
+ actions?: React.ReactNode;
156
+ uiHandlers: {showUI: () => void; setForceShowUI: (value: boolean) => void};
157
+ }) {
116
158
  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
159
 
122
160
  return (
123
161
  <div>
124
162
  <ActionsMenu
163
+ onOpenChange={(value) => {
164
+ uiHandlers.setForceShowUI(value);
165
+ }}
125
166
  contentStyles={css`
126
167
  margin-top: ${space.s8}px;
127
168
  `}
128
169
  container={container}
129
- trigger={<ActionsButtonCompact size="medium" color="neutral" variant="solid" />}
170
+ trigger={<ActionsButtonCompact size="medium" color="neutral" variant="soft" onClick={uiHandlers.showUI} />}
130
171
  >
131
172
  {actions}
132
173
  </ActionsMenu>
133
- <ThemeProvider theme={theme}>
134
- <div style={styles} ref={setContainer}></div>
135
- </ThemeProvider>
174
+ <div ref={setContainer} />
136
175
  </div>
137
176
  );
138
177
  }
139
178
 
140
- function ZoomButton({pswp}: {pswp: PhotoSwipe}) {
179
+ function ZoomButton({pswp, showUI}: {pswp: PhotoSwipe; showUI: () => void}) {
141
180
  const [zoomed, setZoomed] = useState(false);
142
181
  const [isVisible, setIsVisible] = useState(false);
143
182
 
@@ -176,21 +215,23 @@ function ZoomButton({pswp}: {pswp: PhotoSwipe}) {
176
215
  onClick={() => {
177
216
  setZoomed(!zoomed);
178
217
  pswp.toggleZoom();
218
+ showUI();
179
219
  }}
180
220
  size="medium"
181
221
  color="neutral"
182
- variant="solid"
222
+ variant="soft"
183
223
  >
184
224
  {zoomed ? <LoupeZoomOut /> : <LoupeZoomIn />}
185
225
  </IconButton>
186
226
  );
187
227
  }
188
228
 
189
- function CopyURLButton({pswp}: {pswp: PhotoSwipe}) {
229
+ function CopyURLButton({pswp, showUI}: {pswp: PhotoSwipe; showUI: () => void}) {
190
230
  const toast = useToast();
191
231
  return (
192
232
  <Button
193
233
  onClick={() => {
234
+ showUI();
194
235
  if (!pswp?.currSlide?.data.src) {
195
236
  toast.error({title: "Unable to copy."});
196
237
  return;
@@ -208,7 +249,7 @@ function CopyURLButton({pswp}: {pswp: PhotoSwipe}) {
208
249
  }
209
250
  }}
210
251
  size="medium"
211
- variant="solid"
252
+ variant="soft"
212
253
  color="neutral"
213
254
  iconStart={<Link />}
214
255
  type="button"
package/src/kbd.tsx CHANGED
@@ -14,7 +14,7 @@ export const kbdCss = css`
14
14
  height: ${space.s18}px;
15
15
  padding: ${space.s2}px;
16
16
  border-radius: ${border.radius4}px;
17
- border: ${themeVars.shortcutBorderColor};
17
+ border: ${themeVars.shortcutBorder};
18
18
  color: ${themeVars.shortcutTextColor};
19
19
  line-height: ${lineHeight.nowrap};
20
20
  white-space: nowrap;
@@ -2,7 +2,7 @@ import {css} from "@linaria/core";
2
2
  import {styled} from "@linaria/react";
3
3
  import cn from "classnames";
4
4
  import _ from "lodash";
5
- import {memo, useRef} from "react";
5
+ import {ComponentType, memo, ReactNode, useRef} from "react";
6
6
  import {space, themeVars} from "./design-system";
7
7
 
8
8
  const getRandomWithSeed = _.memoize((seed: number) => {
@@ -141,7 +141,7 @@ type SausageListProps = {
141
141
  amount?: number;
142
142
  minAmount?: number;
143
143
  maxAmount?: number;
144
- WrapperComponent?: ({children}: {children: JSX.Element}) => JSX.Element | null;
144
+ WrapperComponent?: ComponentType<{children: ReactNode}> | null;
145
145
  } & Omit<LoadingSausageProps, "index">;
146
146
  export const LoadingSausageList = memo<SausageListProps>(function LoadingSausageList({
147
147
  amount,
package/src/tooltip.tsx CHANGED
@@ -111,7 +111,7 @@ export const TooltipProvider: React.FC<TooltipPrimitive.TooltipProviderProps> =
111
111
  <TooltipPrimitive.Provider disableHoverableContent skipDelayDuration={150} {...props} />
112
112
  );
113
113
 
114
- export type TooltipProps = Omit<TooltipPrimitive.TooltipTriggerProps, "title"> & {
114
+ export type TooltipProps = Omit<TooltipPrimitive.TooltipTriggerProps, "title" | "content"> & {
115
115
  title?: ReactNode;
116
116
  description?: ReactNode;
117
117
  tooltipContentStyle?: string;
@@ -2,6 +2,7 @@ import {ReactNode, useLayoutEffect} from "react";
2
2
  import {createContext} from "@fibery/react/src/create-context";
3
3
  import {useIsMediaQueryMatched} from "./use-is-media-query-matched";
4
4
  import {getMediaQueryList} from "./media-query-utils";
5
+ import {mobileRootClassName} from "./mobile-styles";
5
6
 
6
7
  const [Provider, useIsPhone] = createContext<boolean>("PhoneContext", false);
7
8
 
@@ -23,36 +24,43 @@ function isVerticalMobileBreakpointDisabled() {
23
24
  return urlParams.has("no-vertical-mobile-breakpoint");
24
25
  }
25
26
 
26
- export const isPhoneApp = Boolean(
27
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
28
- // @ts-ignore - injected by consumer app
29
- window.__FIBERY_MOBILE_APP_API
30
- );
27
+ export const isPhoneApp = () =>
28
+ Boolean(
29
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
30
+ // @ts-ignore - injected by consumer app
31
+ window.__FIBERY_MOBILE_APP_API
32
+ );
31
33
 
32
34
  export const phoneMediaQuery = isVerticalMobileBreakpointDisabled()
33
35
  ? "(max-width: 450px)"
34
36
  : "(max-width: 450px), (max-height: 450px)";
35
37
 
36
38
  export function isPhoneMode() {
37
- return isPhoneApp || getMediaQueryList(phoneMediaQuery).matches;
39
+ return isPhoneApp() || getMediaQueryList(phoneMediaQuery).matches;
38
40
  }
39
41
 
40
42
  export function IsPhoneProvider({children}: {children: ReactNode}) {
41
43
  const isPhone = useIsMediaQueryMatched(phoneMediaQuery);
42
44
  useLayoutEffect(() => {
43
45
  const htmlElement = document.querySelector("html");
44
- if (!htmlElement || isPhoneApp) {
46
+ if (!htmlElement || isPhoneApp()) {
47
+ if (htmlElement) {
48
+ htmlElement.classList.add(mobileRootClassName);
49
+ return () => {
50
+ htmlElement.classList.remove(mobileRootClassName);
51
+ };
52
+ }
53
+ // eslint-disable-next-line consistent-return
45
54
  return;
46
55
  }
47
56
  if (isPhone) {
48
- htmlElement.classList.add("fibery-mobile");
57
+ htmlElement.classList.add(mobileRootClassName);
49
58
  } else {
50
- htmlElement.classList.remove("fibery-mobile");
59
+ htmlElement.classList.remove(mobileRootClassName);
51
60
  }
52
- // eslint-disable-next-line consistent-return
53
61
  return () => {
54
- htmlElement.classList.remove("fibery-mobile");
62
+ htmlElement.classList.remove(mobileRootClassName);
55
63
  };
56
64
  }, [isPhone]);
57
- return <Provider value={isPhoneApp || isPhone}>{children}</Provider>;
65
+ return <Provider value={isPhoneApp() || isPhone}>{children}</Provider>;
58
66
  }
@@ -1,7 +1,7 @@
1
1
  import {ReactNode, useEffect, useState} from "react";
2
2
  import {createContext} from "@fibery/react/src/create-context";
3
3
  import _ from "lodash";
4
- import {useIsPhone} from "./use-is-phone";
4
+ import {isPhoneApp} from "./use-is-phone";
5
5
  import {keyboardHeightVar, mobileKeyboardOpenClassName} from "./mobile-styles";
6
6
 
7
7
  const defaultKeyboardHeight = 350;
@@ -17,13 +17,12 @@ const [IsOnScreenKeyboardOpenedValueProvider, useIsOnScreenKeyboardOpened] = cre
17
17
  );
18
18
 
19
19
  export function OnScreenKeyboardContextProvider({children}: {children: ReactNode}) {
20
- const isPhone = useIsPhone();
21
20
  const [keyboardHeight, setKeyboardHeight] = useState<number>(defaultKeyboardHeight);
22
21
  const [keyboardVisible, setKeyboardVisible] = useState<boolean>(false);
23
22
  const [initialViewPortHeight] = useState(() => window.visualViewport?.height || 0);
24
23
  useEffect(() => {
25
24
  const viewport = window.visualViewport;
26
- if (!viewport || !isPhone) {
25
+ if (!viewport || !isPhoneApp()) {
27
26
  return _.noop;
28
27
  }
29
28
 
@@ -55,7 +54,7 @@ export function OnScreenKeyboardContextProvider({children}: {children: ReactNode
55
54
  return () => {
56
55
  viewport.removeEventListener("resize", viewportHandler);
57
56
  };
58
- }, [initialViewPortHeight, isPhone, keyboardHeight, keyboardVisible]);
57
+ }, [initialViewPortHeight, keyboardHeight, keyboardVisible]);
59
58
  return (
60
59
  <OnScreenKeyboardHeightValueProvider value={keyboardHeight}>
61
60
  <IsOnScreenKeyboardOpenedValueProvider value={keyboardVisible}>{children}</IsOnScreenKeyboardOpenedValueProvider>