@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
package/src/kbd.tsx ADDED
@@ -0,0 +1,46 @@
1
+ import {css, cx} from "@linaria/core";
2
+ import {border, lineHeight, space, textStyles, themeVars} from "./design-system";
3
+ import {useIsSupportHardwareKeyboard} from "./use-is-support-hardware-keyboard";
4
+
5
+ export const kbdCss = css`
6
+ ${{
7
+ ...textStyles.small,
8
+ ...textStyles.numeric.tabularOn,
9
+ }}
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ min-width: ${space.s18}px;
14
+ height: ${space.s18}px;
15
+ padding: ${space.s2}px;
16
+ border-radius: ${border.radius4}px;
17
+ border: ${themeVars.shortcutBorderColor};
18
+ color: ${themeVars.shortcutTextColor};
19
+ line-height: ${lineHeight.nowrap};
20
+ white-space: nowrap;
21
+ user-select: none;
22
+ `;
23
+
24
+ export const Kbd = ({className, ...props}: React.ComponentProps<"kbd">): JSX.Element | null => {
25
+ const isSupportHardwareKeyboard = useIsSupportHardwareKeyboard();
26
+ if (!isSupportHardwareKeyboard) {
27
+ return null;
28
+ }
29
+ return <kbd className={cx(kbdCss, className)} {...props} />;
30
+ };
31
+
32
+ export const KbdGroup = ({className, ...props}: React.ComponentProps<"kbd">) => {
33
+ return (
34
+ <kbd
35
+ className={cx(
36
+ className,
37
+ css`
38
+ display: flex;
39
+ align-items: center;
40
+ gap: ${space.s2}px;
41
+ `
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+ };
@@ -5,18 +5,38 @@ import {
5
5
  sidebarMenuItemHeightVar,
6
6
  sidebarMenuIconSizeVar,
7
7
  textStyles,
8
+ menuItemHeightVar,
8
9
  } from "./design-system";
9
- import {mobileRootClassName} from "./mobile-styles";
10
+ import {
11
+ keyboardHeightVar,
12
+ keyboardInsetBottomVar,
13
+ mobileRootClassName,
14
+ safeAreaInsetLeftVar,
15
+ safeAreaInsetRightVar,
16
+ safeAreaInsetTopVar,
17
+ safeAreaInsetBottomVar,
18
+ } from "./mobile-styles";
10
19
 
11
20
  export const layoutStyles = css`
12
21
  :global() {
13
22
  html:root {
23
+ ${safeAreaInsetLeftVar}: var(--app-safe-area-inset-left, env(safe-area-inset-left));
24
+ ${safeAreaInsetRightVar}: var(--app-safe-area-inset-right, env(safe-area-inset-right));
25
+ ${safeAreaInsetTopVar}: var(--app-safe-area-inset-top, env(safe-area-inset-top));
26
+ ${safeAreaInsetBottomVar}: var(--app-safe-area-inset-bottom, env(safe-area-inset-bottom));
27
+ ${keyboardInsetBottomVar}: var(--app-current-keyboard-height, 0px);
28
+ ${keyboardHeightVar}: 350px;
29
+
30
+ ${menuItemHeightVar}: ${layout.menuItemHeight}px;
14
31
  ${sidebarMenuItemHeightVar}: ${layout.newMenuItemHeight}px;
15
32
  ${sidebarMenuIconSizeVar}: ${layout.newMenuIconSize}px;
16
33
  ${listRowSurfaceFontSizeVar}: ${textStyles.regular.fontSize}px;
34
+
17
35
  &.${mobileRootClassName} {
36
+ ${menuItemHeightVar}: ${layout.mobileMenuItemHeight}px;
18
37
  ${sidebarMenuItemHeightVar}: ${layout.mobileSidebarMenuItemHeight}px;
19
38
  ${sidebarMenuIconSizeVar}: ${layout.mobileSidebarIconSize}px;
39
+ ${listRowSurfaceFontSizeVar}: ${textStyles.big.fontSize}px;
20
40
  }
21
41
  }
22
42
  }
@@ -7,6 +7,7 @@ import {useActionsMenuContext} from "../actions-menu";
7
7
  import {ActionsMenuItemProps} from "../actions-menu/actions-menu-item";
8
8
  import {useActiveDangerousRow, useSetActiveDangerousRow} from "../actions-menu/contexts/actions-menu-dangerous-rows";
9
9
  import {colors, layout, lineHeight, space, textStyles, themeVars} from "../design-system";
10
+ import {useIsSupportHover} from "../use-is-support-hover";
10
11
  import {
11
12
  ListRowContent,
12
13
  ListRowContentProps,
@@ -66,6 +67,7 @@ export const actionMenuItemSurface = css`
66
67
  export const ActionMenuItemSurface = forwardRef<HTMLDivElement, ActionMenuItemSurfaceProps>(
67
68
  ({testId, className, loading, selected, hovered, focused, title, disabled, children, dangerous, ...props}, ref) => {
68
69
  const id = useId();
70
+ const platformSupportsHover = useIsSupportHover();
69
71
  const activeDangerousRow = useActiveDangerousRow();
70
72
  const showWarning = activeDangerousRow === id;
71
73
  const onSelect = useCallbackRef(props.onSelect || _.noop);
@@ -100,7 +102,7 @@ export const ActionMenuItemSurface = forwardRef<HTMLDivElement, ActionMenuItemSu
100
102
  listRowSurfaceStylesBase,
101
103
  actionMenuItemSurface,
102
104
  className,
103
- hovered === undefined && NATURAL_HOVER_CLASS,
105
+ platformSupportsHover && hovered === undefined && NATURAL_HOVER_CLASS,
104
106
  focused === undefined && NATURAL_FOCUS_CLASS,
105
107
  focused && "focus",
106
108
  disabled && "done",
@@ -1,6 +1,8 @@
1
1
  import {css, cx} from "@linaria/core";
2
2
  import {ForwardedRef, forwardRef, HTMLAttributes, PropsWithChildren, ReactNode} from "react";
3
3
  import {border, layoutVars, space, textStyles, themeVars, transition} from "../design-system";
4
+ import {useIsSupportHover} from "../use-is-support-hover";
5
+ import {mobileRootSelector} from "../mobile-styles";
4
6
 
5
7
  interface ListRowSurfaceBase extends HTMLAttributes<HTMLDivElement> {
6
8
  testId?: string;
@@ -152,6 +154,7 @@ export const listRowSurfaceStatesDark = css`
152
154
 
153
155
  export const ListRowSurface = forwardRef<HTMLDivElement, ListRowSurfaceProps>(
154
156
  ({selected, focused, darker, hovered, done, className, children, ...rest}, ref) => {
157
+ const platformSupportsHover = useIsSupportHover();
155
158
  return (
156
159
  <div
157
160
  {...rest}
@@ -159,7 +162,7 @@ export const ListRowSurface = forwardRef<HTMLDivElement, ListRowSurfaceProps>(
159
162
  listRowSurfaceStylesBase,
160
163
  darker ? listRowSurfaceStatesDark : listRowSurfaceStatesDefault,
161
164
  className,
162
- hovered === undefined && NATURAL_HOVER_CLASS,
165
+ platformSupportsHover && hovered === undefined && NATURAL_HOVER_CLASS,
163
166
  focused === undefined && NATURAL_FOCUS_CLASS,
164
167
  focused && "focus",
165
168
  hovered && "hover",
@@ -204,6 +207,9 @@ const ListRowContentSegment = css`
204
207
  min-width: 0; // allows ellipsis in flex contexts :)
205
208
  max-width: 100%;
206
209
  width: 100%;
210
+ ${mobileRootSelector} & {
211
+ ${textStyles.big}
212
+ }
207
213
  `;
208
214
  export const ListRowContent = forwardRef<HTMLDivElement, ListRowContentProps>(
209
215
  (
@@ -0,0 +1,87 @@
1
+ import {ReactNode, useEffect, useRef} from "react";
2
+ import {createPortal} from "react-dom";
3
+ import _ from "lodash";
4
+ import {css, cx} from "@linaria/core";
5
+ import {border, space, themeVars} from "./design-system";
6
+ import {keyboardInsetBottomVar, safeAreaInsetBottomVar} from "./mobile-styles";
7
+
8
+ export const mobileKeyboardAwarePopupZIndex = 1001;
9
+
10
+ export function MobileKeyboardAwarePopup({children, className}: {children: ReactNode; className?: string}) {
11
+ const ref = useRef<HTMLDivElement>(null);
12
+ useEffect(() => {
13
+ const viewport = window.visualViewport;
14
+ if (!viewport) {
15
+ return _.noop;
16
+ }
17
+
18
+ const manualKeyboardModeHandler = document.documentElement.classList.contains("fibery-manual-keyboard-mode");
19
+
20
+ let pendingUpdate = false;
21
+ const layoutViewport = document.getElementById("root") as HTMLDivElement;
22
+ const timeoutId = setTimeout(() => {
23
+ if (viewport.offsetTop >= 0 && ref.current) {
24
+ ref.current.style.transform = `translateY(${
25
+ manualKeyboardModeHandler
26
+ ? viewport.offsetTop
27
+ : viewport.height - layoutViewport.getBoundingClientRect().height + viewport.offsetTop
28
+ }px)`;
29
+ }
30
+ }, 0);
31
+
32
+ function viewportHandler() {
33
+ clearTimeout(timeoutId);
34
+ if (pendingUpdate) {
35
+ return;
36
+ }
37
+ pendingUpdate = true;
38
+
39
+ requestAnimationFrame(() => {
40
+ pendingUpdate = false;
41
+
42
+ if (viewport && viewport.offsetTop >= 0 && ref.current) {
43
+ ref.current.style.transform = `translateY(${
44
+ manualKeyboardModeHandler
45
+ ? viewport.offsetTop
46
+ : viewport.height - layoutViewport.getBoundingClientRect().height + viewport.offsetTop
47
+ }px)`;
48
+ }
49
+ });
50
+ }
51
+
52
+ viewport.addEventListener("scroll", viewportHandler);
53
+ viewport.addEventListener("resize", viewportHandler);
54
+ return () => {
55
+ clearTimeout(timeoutId);
56
+ viewport.removeEventListener("scroll", viewportHandler);
57
+ viewport.removeEventListener("resize", viewportHandler);
58
+ };
59
+ }, []);
60
+ return createPortal(
61
+ <div
62
+ ref={ref}
63
+ className={cx(
64
+ css`
65
+ box-shadow: ${themeVars.shadow200};
66
+ background-color: ${themeVars.actionMenuBg};
67
+ position: fixed;
68
+ bottom: max(max(var(${keyboardInsetBottomVar}), var(${safeAreaInsetBottomVar})), 8px);
69
+ left: 0;
70
+ right: 0;
71
+ z-index: ${mobileKeyboardAwarePopupZIndex};
72
+ transform: translateY(0px);
73
+ border-radius: ${border.radius16}px;
74
+ border: 1px solid ${themeVars.separatorColor};
75
+ margin: 0 ${space.s8}px;
76
+ .fibery-manual-keyboard-mode & {
77
+ margin-bottom: 8px;
78
+ }
79
+ `,
80
+ className
81
+ )}
82
+ >
83
+ {children}
84
+ </div>,
85
+ document.body
86
+ );
87
+ }
@@ -1,2 +1,10 @@
1
1
  export const mobileRootClassName = "fibery-mobile";
2
2
  export const mobileRootSelector = `.${mobileRootClassName}`;
3
+ export const mobileKeyboardOpenClassName = "fibery-mobile-keyboard-open";
4
+ export const mobileKeyboardOpenSelector = `.${mobileKeyboardOpenClassName}`;
5
+ export const keyboardHeightVar = "--fibery-on-screen-keyboard-height";
6
+ export const safeAreaInsetLeftVar = "--safe-area-inset-left";
7
+ export const safeAreaInsetRightVar = "--safe-area-inset-right";
8
+ export const safeAreaInsetTopVar = "--safe-area-inset-top";
9
+ export const safeAreaInsetBottomVar = "--safe-area-inset-bottom";
10
+ export const keyboardInsetBottomVar = "--keyboard-inset-bottom";
@@ -0,0 +1,22 @@
1
+ import {createContext} from "@fibery/react/src/create-context";
2
+
3
+ export type SubmenuItem = {
4
+ id: string;
5
+ title: string;
6
+ };
7
+
8
+ export type ModalMenuContext = {
9
+ open: boolean;
10
+ title: string;
11
+ submenuStack: SubmenuItem[];
12
+ contentContainer: HTMLDivElement | null;
13
+ setContentContainer: (el: HTMLDivElement | null) => void;
14
+ onOpenSubmenu: (item: SubmenuItem) => void;
15
+ onNavigateBack: () => void;
16
+ onOpenChange: (open: boolean) => void;
17
+ height?: string;
18
+ };
19
+
20
+ const [ModalMenuContextProvider, useModalMenu] = createContext<ModalMenuContext>("ModalMenuContext");
21
+
22
+ export {ModalMenuContextProvider, useModalMenu};
@@ -0,0 +1,16 @@
1
+ import {createContext} from "@fibery/react/src/create-context";
2
+
3
+ export type ModalSubmenuContext = {
4
+ id: string;
5
+ title: string;
6
+ depth: number;
7
+ };
8
+
9
+ // The default value below is only used to provide initial depth of 0
10
+ const [ModalSubmenuContextPovider, useModalSubmenu] = createContext<ModalSubmenuContext>("ModalSubmenuContext", {
11
+ id: "root",
12
+ title: "Root",
13
+ depth: 0,
14
+ });
15
+
16
+ export {ModalSubmenuContextPovider, useModalSubmenu};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * This is designed to be drop-in compatible with dropdown-menu or context-menu
3
+ */
4
+
5
+ export {ModalMenuRoot as Root} from "./modal-menu-root";
6
+ export {ModalMenuTrigger as Trigger} from "./modal-menu-trigger";
7
+ export {ModalMenuContent as Content} from "./modal-menu-content";
8
+ export {ModalMenuItem as Item} from "./modal-menu-item";
9
+ export {ModalMenuSeparator as Separator} from "./modal-menu-separator";
10
+ export {ModalMenuLabel as Label} from "./modal-menu-label";
11
+ export {ModalMenuGroup as Group} from "./modal-menu-group";
12
+ export {ModalSubmenu as Sub} from "./modal-submenu";
13
+ export {ModalSubmenuTrigger as SubTrigger} from "./modal-submenu-trigger";
14
+ export {ModalSubmenuContent as SubContent} from "./modal-submenu-content";
@@ -0,0 +1,68 @@
1
+ import {ReactNode} from "react";
2
+ import {createPortal} from "react-dom";
3
+ import {css} from "@linaria/core";
4
+ import {MobilePopup} from "../popover";
5
+ import {space, themeVars} from "../design-system";
6
+ import {getPopupContainerElement} from "../utils-dom";
7
+ import {CommandMenuList, CommandMenuRoot} from "../command-menu";
8
+ import {useModalMenu} from "./contexts/modal-menu-context";
9
+
10
+ interface ModalMenuContentProps {
11
+ children?: ReactNode;
12
+ }
13
+
14
+ export const ModalMenuContent = ({children}: ModalMenuContentProps) => {
15
+ const {open, title, submenuStack, setContentContainer, onOpenChange, onNavigateBack, height} = useModalMenu();
16
+
17
+ if (!open) {
18
+ return null;
19
+ }
20
+
21
+ const content = (
22
+ <div
23
+ ref={setContentContainer}
24
+ className={css`
25
+ width: 100%;
26
+ height: 100%;
27
+ position: relative;
28
+
29
+ & ${CommandMenuRoot}:is(${CommandMenuRoot}) {
30
+ padding: 0 ${space.s4}px;
31
+ width: 100%;
32
+ }
33
+ & ${CommandMenuList}:is(${CommandMenuList}) {
34
+ max-height: unset;
35
+ }
36
+ `}
37
+ >
38
+ <div
39
+ className={css`
40
+ position: absolute;
41
+ background: ${themeVars.actionMenuBg};
42
+ inset: 0;
43
+ overflow-y: auto;
44
+ `}
45
+ >
46
+ {children}
47
+ </div>
48
+ {/* Here the submenus will render via portals */}
49
+ </div>
50
+ );
51
+
52
+ return createPortal(
53
+ <MobilePopup
54
+ height={height}
55
+ title={submenuStack.at(-1)?.title ?? title}
56
+ content={content}
57
+ onClose={() => onOpenChange(false)}
58
+ onNavigateBack={
59
+ submenuStack.length === 0
60
+ ? undefined
61
+ : () => {
62
+ onNavigateBack();
63
+ }
64
+ }
65
+ />,
66
+ getPopupContainerElement()
67
+ );
68
+ };
@@ -0,0 +1,10 @@
1
+ import {css} from "@linaria/core";
2
+ import {ReactNode} from "react";
3
+
4
+ interface ModalMenuGroupProps {
5
+ children?: ReactNode;
6
+ }
7
+
8
+ export const ModalMenuGroup = ({children}: ModalMenuGroupProps) => {
9
+ return <div className={css``}>{children}</div>;
10
+ };
@@ -0,0 +1,45 @@
1
+ import {css, cx} from "@linaria/core";
2
+ import {ComponentProps, ReactNode} from "react";
3
+ import {space, textStyles} from "../design-system";
4
+ import {useModalMenu} from "./contexts/modal-menu-context";
5
+
6
+ interface ModalMenuItemProps extends Omit<ComponentProps<"div">, "onSelect"> {
7
+ disabled?: boolean;
8
+ onSelect?: (event: Event) => void;
9
+ children?: ReactNode;
10
+ }
11
+
12
+ export const ModalMenuItem = ({children, disabled, onSelect, ...otherProps}: ModalMenuItemProps) => {
13
+ const {onOpenChange} = useModalMenu();
14
+
15
+ return (
16
+ <div
17
+ role="button"
18
+ {...otherProps}
19
+ className={cx(
20
+ otherProps.className,
21
+ css`
22
+ ${textStyles.big}
23
+ padding: ${space.s10}px ${space.s16}px ${space.s10}px ${space.s16}px;
24
+ `,
25
+ disabled &&
26
+ css`
27
+ opacity: 0.4;
28
+ `
29
+ )}
30
+ onClick={(event) => {
31
+ otherProps.onClick?.(event);
32
+ if (disabled) {
33
+ return;
34
+ }
35
+ const selectionEvent = new CustomEvent("modal-menu-select", {bubbles: true, cancelable: true});
36
+ onSelect?.(selectionEvent);
37
+ if (!selectionEvent.defaultPrevented) {
38
+ onOpenChange(false);
39
+ }
40
+ }}
41
+ >
42
+ {children}
43
+ </div>
44
+ );
45
+ };
@@ -0,0 +1,24 @@
1
+ import {css} from "@linaria/core";
2
+ import {ReactNode} from "react";
3
+ import {layoutVars, space, textStyles, themeVars} from "../design-system";
4
+
5
+ interface ModalMenuLabelProps {
6
+ children?: ReactNode;
7
+ }
8
+
9
+ export const ModalMenuLabel = ({children}: ModalMenuLabelProps) => {
10
+ return (
11
+ <div
12
+ className={css`
13
+ ${textStyles.big}
14
+ display: flex;
15
+ align-items: center;
16
+ padding: ${space.s10}px ${space.s16}px ${space.s10}px ${space.s16}px;
17
+ min-height: ${layoutVars.sidebarMenuItemHeight};
18
+ color: ${themeVars.accentTextColor} !important;
19
+ `}
20
+ >
21
+ {children}
22
+ </div>
23
+ );
24
+ };
@@ -0,0 +1,57 @@
1
+ import {ReactNode, RefObject, useCallback, useMemo, useState} from "react";
2
+ import {ModalMenuContextProvider, SubmenuItem} from "./contexts/modal-menu-context";
3
+
4
+ interface ModalMenuRootProps {
5
+ open?: boolean;
6
+ title?: string;
7
+ height?: string;
8
+ contentRef?: RefObject<HTMLDivElement>;
9
+ onOpenChange?: (open: boolean) => void;
10
+ children?: ReactNode;
11
+ }
12
+
13
+ export const ModalMenuRoot = ({
14
+ open,
15
+ title = "Actions",
16
+ children,
17
+ onOpenChange: _onOpenChange,
18
+ height,
19
+ }: ModalMenuRootProps) => {
20
+ const [submenuStack, setSubmenuStack] = useState<SubmenuItem[]>([]);
21
+ const [contentContainer, setContentContainer] = useState<HTMLDivElement | null>(null);
22
+
23
+ const onOpenChange = useCallback(
24
+ (open: boolean) => {
25
+ _onOpenChange?.(open);
26
+ if (!open) {
27
+ setSubmenuStack([]);
28
+ }
29
+ },
30
+ [_onOpenChange]
31
+ );
32
+
33
+ const onOpenSubmenu = useCallback((item: SubmenuItem) => {
34
+ setSubmenuStack((stack) => [...stack, item]);
35
+ }, []);
36
+
37
+ const onNavigateBack = useCallback(() => {
38
+ setSubmenuStack((stack) => stack.slice(0, -1));
39
+ }, []);
40
+
41
+ const contextValue = useMemo(
42
+ () => ({
43
+ title,
44
+ open: open ?? false,
45
+ submenuStack,
46
+ contentContainer,
47
+ setContentContainer,
48
+ onOpenChange,
49
+ onOpenSubmenu,
50
+ onNavigateBack,
51
+ height,
52
+ }),
53
+ [open, title, submenuStack, contentContainer, onOpenChange, onOpenSubmenu, onNavigateBack, height]
54
+ );
55
+
56
+ return <ModalMenuContextProvider value={contextValue}>{children}</ModalMenuContextProvider>;
57
+ };
@@ -0,0 +1,14 @@
1
+ import {css} from "@linaria/core";
2
+ import {space, themeVars} from "../design-system";
3
+
4
+ export const ModalMenuSeparator = () => {
5
+ return (
6
+ <hr
7
+ className={css`
8
+ border: none;
9
+ border-bottom: 1px solid ${themeVars.separatorColor};
10
+ margin: ${space.s4}px 0;
11
+ `}
12
+ />
13
+ );
14
+ };
@@ -0,0 +1,54 @@
1
+ import {cx} from "@linaria/core";
2
+ import {cloneElement, ComponentProps, ReactHTMLElement, ReactNode} from "react";
3
+ import {useModalMenu} from "./contexts/modal-menu-context";
4
+
5
+ interface ModalMenuTriggerProps extends ComponentProps<"div"> {
6
+ asChild?: boolean;
7
+ triggerEvent?: "click" | "context-menu";
8
+ children: ReactHTMLElement<HTMLDivElement> | ReactNode;
9
+ }
10
+
11
+ export const ModalMenuTrigger = ({
12
+ asChild,
13
+ triggerEvent,
14
+ children,
15
+ onClick,
16
+ onContextMenu,
17
+ ...otherProps
18
+ }: ModalMenuTriggerProps) => {
19
+ const {open, onOpenChange} = useModalMenu();
20
+
21
+ if (asChild) {
22
+ const className = (children as ReactHTMLElement<HTMLDivElement>)?.props?.className;
23
+ return cloneElement(children as ReactHTMLElement<HTMLDivElement>, {
24
+ ...otherProps,
25
+ className: cx(className, otherProps.className),
26
+ onClick: (event) => {
27
+ onClick?.(event);
28
+ if (triggerEvent === "click") {
29
+ onOpenChange(!open);
30
+ }
31
+ },
32
+ onContextMenu: (event) => {
33
+ onContextMenu?.(event);
34
+ if (triggerEvent === "context-menu") {
35
+ onOpenChange(!open);
36
+ }
37
+ },
38
+ });
39
+ }
40
+
41
+ return (
42
+ <div
43
+ role="button"
44
+ {...otherProps}
45
+ className={otherProps.className}
46
+ onClick={(event) => {
47
+ onClick?.(event);
48
+ onOpenChange(!open);
49
+ }}
50
+ >
51
+ {children}
52
+ </div>
53
+ );
54
+ };
@@ -0,0 +1,49 @@
1
+ import {ReactNode} from "react";
2
+ import {createPortal} from "react-dom";
3
+ import {css} from "@linaria/core";
4
+ import {themeVars, transition} from "../design-system";
5
+ import {useModalMenu} from "./contexts/modal-menu-context";
6
+ import {useModalSubmenu} from "./contexts/modal-submenu-context";
7
+
8
+ interface ModalSubmenuContentProps {
9
+ children?: ReactNode;
10
+ }
11
+
12
+ export const ModalSubmenuContent = ({children}: ModalSubmenuContentProps) => {
13
+ const {contentContainer, submenuStack} = useModalMenu();
14
+ const {id, depth} = useModalSubmenu();
15
+
16
+ const open = submenuStack.some((item) => item.id === id);
17
+
18
+ if (!contentContainer || !open) {
19
+ return null;
20
+ }
21
+
22
+ return createPortal(
23
+ <div
24
+ className={css`
25
+ position: absolute;
26
+ background: ${themeVars.actionMenuBg};
27
+ inset: 0;
28
+ overflow-y: auto;
29
+
30
+ transition: transform ${transition}, opacity ${transition};
31
+ transform: translateX(0);
32
+ opacity: 1;
33
+
34
+ @media (prefers-reduced-motion: no-preference) {
35
+ @starting-style {
36
+ transform: translateX(20%);
37
+ opacity: 0;
38
+ }
39
+ }
40
+ `}
41
+ style={{
42
+ zIndex: depth,
43
+ }}
44
+ >
45
+ {children}
46
+ </div>,
47
+ contentContainer
48
+ );
49
+ };
@@ -0,0 +1,34 @@
1
+ import {css, cx} from "@linaria/core";
2
+ import {ComponentProps, ReactHTMLElement} from "react";
3
+ import {space, textStyles} from "../design-system";
4
+ import {useModalMenu} from "./contexts/modal-menu-context";
5
+ import {useModalSubmenu} from "./contexts/modal-submenu-context";
6
+
7
+ interface ModalSubmenuTriggerProps extends ComponentProps<"div"> {
8
+ children: ReactHTMLElement<HTMLButtonElement>;
9
+ }
10
+
11
+ export const ModalSubmenuTrigger = ({children, ...otherProps}: ModalSubmenuTriggerProps) => {
12
+ const {onOpenSubmenu} = useModalMenu();
13
+ const {id, title} = useModalSubmenu();
14
+
15
+ return (
16
+ <div
17
+ role="button"
18
+ {...otherProps}
19
+ className={cx(
20
+ otherProps.className,
21
+ css`
22
+ ${textStyles.big}
23
+ padding: ${space.s10}px ${space.s16}px ${space.s10}px ${space.s16}px;
24
+ `
25
+ )}
26
+ onClick={(event) => {
27
+ otherProps.onClick?.(event);
28
+ onOpenSubmenu({id, title});
29
+ }}
30
+ >
31
+ {children}
32
+ </div>
33
+ );
34
+ };
@@ -0,0 +1,17 @@
1
+ import {ReactNode, useId, useMemo} from "react";
2
+ import {ModalSubmenuContextPovider, useModalSubmenu} from "./contexts/modal-submenu-context";
3
+
4
+ interface ModalSubmenuProps {
5
+ open?: boolean;
6
+ onOpenChange?: (open: boolean) => void;
7
+ title?: string;
8
+ children?: ReactNode;
9
+ }
10
+
11
+ export const ModalSubmenu = ({title = "Submenu", children}: ModalSubmenuProps) => {
12
+ const id = useId();
13
+ const {depth} = useModalSubmenu();
14
+ const contextValue = useMemo(() => ({id, title, depth: depth + 1}), [id, title, depth]);
15
+
16
+ return <ModalSubmenuContextPovider value={contextValue}>{children}</ModalSubmenuContextPovider>;
17
+ };