@cloud-ru/ds-uikit-product-header 2.0.2 → 2.0.3-preview-1207a902.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 (33) hide show
  1. package/dist/cjs/components/MainMenu/helperComponents/MenuBottom/MenuBottom.js +1 -1
  2. package/dist/cjs/components/MainMenu/helperComponents/MenuMobile/MenuMobile.js +2 -1
  3. package/dist/cjs/components/MainMenu/helperComponents/ScrollWithAnimatedStickyPanel/ScrollWithAnimatedStickyPanel.js +18 -6
  4. package/dist/cjs/components/UserMenu/UserMenu.js +5 -4
  5. package/dist/cjs/components/UserMenu/hooks/useLogoutItem.d.ts +2 -2
  6. package/dist/cjs/components/UserMenu/hooks/useLogoutItem.js +12 -9
  7. package/dist/cjs/components/UserMenu/hooks/useUserMenuItems.js +3 -3
  8. package/dist/cjs/components/UserMenu/styles.module.css +9 -0
  9. package/dist/cjs/constants.d.ts +0 -1
  10. package/dist/cjs/constants.js +1 -3
  11. package/dist/cjs/style.css +9 -0
  12. package/dist/esm/components/MainMenu/helperComponents/MenuBottom/MenuBottom.js +2 -2
  13. package/dist/esm/components/MainMenu/helperComponents/MenuMobile/MenuMobile.js +3 -2
  14. package/dist/esm/components/MainMenu/helperComponents/ScrollWithAnimatedStickyPanel/ScrollWithAnimatedStickyPanel.js +18 -6
  15. package/dist/esm/components/UserMenu/UserMenu.js +8 -7
  16. package/dist/esm/components/UserMenu/hooks/useLogoutItem.d.ts +2 -2
  17. package/dist/esm/components/UserMenu/hooks/useLogoutItem.js +9 -9
  18. package/dist/esm/components/UserMenu/hooks/useUserMenuItems.js +3 -3
  19. package/dist/esm/components/UserMenu/styles.module.css +9 -0
  20. package/dist/esm/constants.d.ts +0 -1
  21. package/dist/esm/constants.js +0 -2
  22. package/dist/esm/style.css +9 -0
  23. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  24. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  25. package/package.json +28 -28
  26. package/src/components/MainMenu/helperComponents/MenuBottom/MenuBottom.tsx +2 -2
  27. package/src/components/MainMenu/helperComponents/MenuMobile/MenuMobile.tsx +3 -2
  28. package/src/components/MainMenu/helperComponents/ScrollWithAnimatedStickyPanel/ScrollWithAnimatedStickyPanel.tsx +21 -10
  29. package/src/components/UserMenu/UserMenu.tsx +12 -7
  30. package/src/components/UserMenu/hooks/useLogoutItem.tsx +12 -12
  31. package/src/components/UserMenu/hooks/useUserMenuItems.ts +4 -4
  32. package/src/components/UserMenu/styles.module.scss +11 -0
  33. package/src/constants.ts +0 -3
@@ -12,5 +12,5 @@ const constants_1 = require("../../../../constants");
12
12
  const utils_1 = require("../../utils");
13
13
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
14
14
  function MenuBottom({ settingItems, leftBottom }) {
15
- return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.bottom, "data-test-id": constants_1.TEST_IDS.mainMenu.leftBottom, children: [Boolean(settingItems?.length) && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.bottomItems, "data-test-id": constants_1.TEST_IDS.mainMenu.leftBottomItems, children: settingItems?.map(item => ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [item.divider === 'before' && (0, jsx_runtime_1.jsx)(divider_1.Divider, { orientation: 'horizontal', className: styles_module_scss_1.default.bottomDivider }), (0, jsx_runtime_1.jsx)(uikit_product_card_predefined_1.CardServiceLight, { ...(item.href ? { href: item.href, as: 'a' } : { as: 'button', type: 'button' }), title: item.label, icon: (0, utils_1.getLinkEmblem)(item), onClick: item.onClick, "data-test-id": `header__drawer-menu__setting-${item.id}` }), item.divider === 'after' && (0, jsx_runtime_1.jsx)(divider_1.Divider, { orientation: 'horizontal', className: styles_module_scss_1.default.bottomDivider })] }, item.id))) })), leftBottom] }));
15
+ return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.bottom, "data-test-id": constants_1.TEST_IDS.mainMenu.leftBottom, children: [Boolean(settingItems?.length) && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.bottomItems, "data-test-id": constants_1.TEST_IDS.mainMenu.leftBottomItems, children: settingItems?.map(item => ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [item.divider === 'before' && (0, jsx_runtime_1.jsx)(divider_1.Divider, { orientation: 'horizontal', className: styles_module_scss_1.default.bottomDivider }), (0, jsx_runtime_1.jsx)(uikit_product_card_predefined_1.CardNavigation, { ...(item.href ? { href: item.href, as: 'a' } : { as: 'button', type: 'button' }), title: item.label, icon: (0, utils_1.getLinkEmblem)(item), onClick: item.onClick, "data-test-id": `header__drawer-menu__setting-${item.id}` }), item.divider === 'after' && (0, jsx_runtime_1.jsx)(divider_1.Divider, { orientation: 'horizontal', className: styles_module_scss_1.default.bottomDivider })] }, item.id))) })), leftBottom] }));
16
16
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MenuMobile = MenuMobile;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const divider_1 = require("@cloud-ru/ds-divider");
9
+ const list_1 = require("@cloud-ru/ds-list");
9
10
  const react_1 = require("react");
10
11
  const constants_1 = require("../../../../constants");
11
12
  const mobileOverlays_1 = require("../../../../mobileOverlays");
@@ -38,5 +39,5 @@ function MenuMobile({ open = false, setOpen, settingItems, platformGroups, segme
38
39
  const handleClose = (0, react_1.useCallback)(() => {
39
40
  setOpen?.(false);
40
41
  }, [setOpen]);
41
- return ((0, jsx_runtime_1.jsx)(mobileOverlays_1.MobileDrawerCustom, { open: open, onClose: handleClose, position: 'bottom', className: styles_module_scss_1.default.drawerMobile, swipeEnabled: false, "data-test-id": constants_1.TEST_IDS.mainMenu.drawerMobile, closeOnPopstate: true, snapPoints: constants_1.BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS, disableMotions: true, children: (0, jsx_runtime_1.jsx)(ScrollWithAnimatedStickyPanel_1.ScrollWithAnimatedStickyPanel, { panel: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(MenuHeaderBrand_1.MenuHeaderBrand, { logo: logo, onClose: handleClose, className: styles_module_scss_1.default.menuHeader, isMobile: true }), (0, jsx_runtime_1.jsx)(divider_1.Divider, { orientation: 'horizontal' })] }), children: (0, jsx_runtime_1.jsxs)(MountAnimation_1.MountAnimation, { className: styles_module_scss_1.default.scrollMobile, type: 'fade-slide-up', children: [leftTop, search && (0, jsx_runtime_1.jsx)(Search_1.Search, { ...search, ref: searchRef, isMobile: true }), !isSearching && favorite && (0, jsx_runtime_1.jsx)(Favorites_1.Favorites, { favorite: favorite, servicesById: servicesById, isMobile: true }), hasSegments && ((0, jsx_runtime_1.jsx)(useMainMenuDnd_1.MainMenuDndContext, { children: (0, jsx_runtime_1.jsx)(Content_1.Content, { isMobile: true, searchValue: search && search.value, rightTop: rightTop, favorite: favorite, segments: segments, searchGroups: resultItems, segmentPrefs: segmentPrefs, activeSegmentId: activeSegmentId, onActiveSegmentChange: onActiveSegmentChange, onSegmentOrderChange: onSegmentOrderChange, onSegmentExpandedChange: onSegmentExpandedChange, onSegmentServiceClick: onSegmentServiceClick, loading: loading }) })), !isSearching && hasBottomItems && (0, jsx_runtime_1.jsx)(MenuBottom_1.MenuBottom, { settingItems: settingItems, leftBottom: leftBottom })] }) }) }));
42
+ return ((0, jsx_runtime_1.jsx)(mobileOverlays_1.MobileDrawerCustom, { open: open, onClose: handleClose, position: 'bottom', className: styles_module_scss_1.default.drawerMobile, swipeEnabled: false, "data-test-id": constants_1.TEST_IDS.mainMenu.drawerMobile, closeOnPopstate: true, snapPoints: list_1.SNAP_POINTS_PRESET.full, disableMotions: true, children: (0, jsx_runtime_1.jsx)(ScrollWithAnimatedStickyPanel_1.ScrollWithAnimatedStickyPanel, { panel: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(MenuHeaderBrand_1.MenuHeaderBrand, { logo: logo, onClose: handleClose, className: styles_module_scss_1.default.menuHeader, isMobile: true }), (0, jsx_runtime_1.jsx)(divider_1.Divider, { orientation: 'horizontal' })] }), children: (0, jsx_runtime_1.jsxs)(MountAnimation_1.MountAnimation, { className: styles_module_scss_1.default.scrollMobile, type: 'fade-slide-up', children: [leftTop, search && (0, jsx_runtime_1.jsx)(Search_1.Search, { ...search, ref: searchRef, isMobile: true }), !isSearching && favorite && (0, jsx_runtime_1.jsx)(Favorites_1.Favorites, { favorite: favorite, servicesById: servicesById, isMobile: true }), hasSegments && ((0, jsx_runtime_1.jsx)(useMainMenuDnd_1.MainMenuDndContext, { children: (0, jsx_runtime_1.jsx)(Content_1.Content, { isMobile: true, searchValue: search && search.value, rightTop: rightTop, favorite: favorite, segments: segments, searchGroups: resultItems, segmentPrefs: segmentPrefs, activeSegmentId: activeSegmentId, onActiveSegmentChange: onActiveSegmentChange, onSegmentOrderChange: onSegmentOrderChange, onSegmentExpandedChange: onSegmentExpandedChange, onSegmentServiceClick: onSegmentServiceClick, loading: loading }) })), !isSearching && hasBottomItems && (0, jsx_runtime_1.jsx)(MenuBottom_1.MenuBottom, { settingItems: settingItems, leftBottom: leftBottom })] }) }) }));
42
43
  }
@@ -8,6 +8,10 @@ const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const scroll_1 = require("@cloud-ru/ds-scroll");
9
9
  const react_1 = require("react");
10
10
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
11
+ // Упругая прокрутка у верхней границы (touch-скролл на мобильных не "упирается" жёстко в 0 —
12
+ // контент можно немного "оттянуть" дальше) даёт scrollTop, не совпадающий с реальным положением
13
+ // контента в этот момент. Диапазон запаса — единицы пикселей; двух хватает с большим запасом.
14
+ const TOP_OVERSCROLL_EPSILON_PX = 2;
11
15
  function ScrollWithAnimatedStickyPanel({ panel, children }) {
12
16
  const [positions, setPositions] = (0, react_1.useState)({ panelTopShift: 0, containerScroll: 0 });
13
17
  const [panelHeight, setPanelHeight] = (0, react_1.useState)(0);
@@ -20,6 +24,13 @@ function ScrollWithAnimatedStickyPanel({ panel, children }) {
20
24
  return;
21
25
  const panelHeight = panelRef.current.offsetHeight;
22
26
  setPanelHeight(panelHeight);
27
+ // У верхней границы панель всегда полностью видна — не доверяем накопленному diff. Иначе
28
+ // "лишний" ход упругой прокрутки у самого верха читается как скролл вниз и на возврате к
29
+ // началу списка панель может остаться скрытой, хотя пользователь как раз скроллит наверх.
30
+ if (target.scrollTop <= TOP_OVERSCROLL_EPSILON_PX) {
31
+ setPositions({ panelTopShift: 0, containerScroll: target.scrollTop });
32
+ return;
33
+ }
23
34
  setPositions(prev => {
24
35
  const diff = prev.containerScroll - target.scrollTop;
25
36
  return {
@@ -28,10 +39,11 @@ function ScrollWithAnimatedStickyPanel({ panel, children }) {
28
39
  };
29
40
  });
30
41
  }, []);
31
- return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.container, style: { '--snack-autohide-panel-height': `${panelHeight}px` }, children: [(0, jsx_runtime_1.jsx)(scroll_1.Scroll, { barHideStrategy: 'never', overflow: { x: 'hidden' }, onScroll: handleScroll, children: children }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.panel, ref: element => {
32
- if (element) {
33
- setPanelHeight(element.offsetHeight);
34
- panelRef.current = element;
35
- }
36
- }, style: { top: positions.panelTopShift }, children: panel })] }));
42
+ const setPanelRef = (0, react_1.useCallback)((element) => {
43
+ if (element) {
44
+ setPanelHeight(element.offsetHeight);
45
+ panelRef.current = element;
46
+ }
47
+ }, []);
48
+ return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.container, style: { '--snack-autohide-panel-height': `${panelHeight}px` }, children: [(0, jsx_runtime_1.jsx)(scroll_1.Scroll, { barHideStrategy: 'never', overflow: { x: 'hidden' }, onScroll: handleScroll, children: children }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.panel, ref: setPanelRef, style: { top: positions.panelTopShift }, children: panel })] }));
37
49
  }
@@ -28,6 +28,9 @@ function UserMenu({ profile = {}, open: openProp, setOpen: setOpenProp, onLogout
28
28
  const comfortClassName = (0, useDesktopComfortClassName_1.useDesktopComfortClassName)();
29
29
  const [open = false, setOpen] = (0, utils_1.useValueControl)({ value: openProp, onChange: setOpenProp });
30
30
  const { fullName = '', inviteCount } = profile;
31
+ const handleClose = (0, react_1.useCallback)(() => {
32
+ setOpen(false);
33
+ }, [setOpen]);
31
34
  const { pinTop, items: menuItems, pinBottom, } = (0, useUserMenuItems_1.useUserMenuItems)({
32
35
  isMobile,
33
36
  profile,
@@ -36,9 +39,7 @@ function UserMenu({ profile = {}, open: openProp, setOpen: setOpenProp, onLogout
36
39
  organizationItems,
37
40
  bottomItems,
38
41
  settingItems,
39
- onClose: () => {
40
- setOpen(false);
41
- },
42
+ onClose: handleClose,
42
43
  onLogout,
43
44
  });
44
45
  const trigger = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsx)(HeaderButton_1.HeaderButton, { tooltip: { tip: triggerTooltip }, isMobile: isMobile, onClick: () => {
@@ -50,5 +51,5 @@ function UserMenu({ profile = {}, open: openProp, setOpen: setOpenProp, onLogout
50
51
  appearance: 'primary',
51
52
  }
52
53
  : undefined, "data-test-id": constants_1.TEST_IDS.userMenu.button, icon: (0, jsx_runtime_1.jsx)(avatar_1.Avatar, { appearance: 'red', size: 's', name: fullName, showTwoSymbols: true }), "data-pressed": open })), [fullName, inviteCount, onClick, open, setOpen, isMobile, triggerTooltip]);
53
- return ((0, jsx_runtime_1.jsx)(list_1.Droplist, { open: open, onOpenChange: setOpen, size: 'm', selection: SELECTION_STUB, pinTop: pinTop, items: menuItems, pinBottom: pinBottom, scroll: !isMobile, trigger: 'click', placement: 'bottom-end', className: (0, classnames_1.default)(styles_module_scss_1.default.userMenuDroplist, comfortClassName), closeOnPopstate: true, "data-test-id": constants_1.TEST_IDS.userMenu.root, label: t('user'), snapPoints: constants_1.BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS, children: trigger }));
54
+ return ((0, jsx_runtime_1.jsx)(list_1.Droplist, { open: open, onOpenChange: setOpen, size: 'm', selection: SELECTION_STUB, pinTop: pinTop, items: menuItems, pinBottom: pinBottom, scroll: !isMobile, trigger: 'click', placement: 'bottom-end', className: (0, classnames_1.default)(styles_module_scss_1.default.userMenuDroplist, comfortClassName), closeOnPopstate: true, "data-test-id": constants_1.TEST_IDS.userMenu.root, label: t('user'), snapPoints: list_1.SNAP_POINTS_PRESET.full, safeArea: true, withDividers: false, triggerClassName: styles_module_scss_1.default.userMenuDroplistTrigger, children: trigger }));
54
55
  }
@@ -1,4 +1,4 @@
1
- import { ListProps } from '@cloud-ru/ds-list';
1
+ import { BaseItem } from '@cloud-ru/ds-list';
2
2
  export declare function useLogoutItem({ onLogout }: {
3
3
  onLogout?(): void;
4
- }): ListProps['items'];
4
+ }): BaseItem;
@@ -1,20 +1,23 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.useLogoutItem = useLogoutItem;
4
7
  const jsx_runtime_1 = require("react/jsx-runtime");
5
8
  const product_1 = require("@cloud-ru/ds-icons/interface/product");
6
9
  const react_1 = require("react");
7
10
  const locale_1 = require("../../../locale");
11
+ const styles_module_scss_1 = __importDefault(require('../styles.module.css'));
8
12
  function useLogoutItem({ onLogout }) {
9
13
  const { t } = locale_1.headerLocale.useTranslations();
10
- return (0, react_1.useMemo)(() => [
11
- {
12
- beforeContent: (0, jsx_runtime_1.jsx)(product_1.ExitSVG, {}),
13
- content: {
14
- label: t('logout'),
15
- },
16
- onClick: onLogout,
17
- 'data-test-id': 'header__user-menu__logout',
14
+ return (0, react_1.useMemo)(() => ({
15
+ beforeContent: (0, jsx_runtime_1.jsx)(product_1.ExitSVG, {}),
16
+ content: {
17
+ label: t('logout'),
18
18
  },
19
- ], [onLogout, t]);
19
+ className: styles_module_scss_1.default.logoutItem,
20
+ onClick: onLogout,
21
+ 'data-test-id': 'header__user-menu__logout',
22
+ }), [onLogout, t]);
20
23
  }
@@ -38,11 +38,11 @@ function useUserMenuItems({ profile, theme, onLogout, topItems = emptyArray_1.EM
38
38
  }
39
39
  pinTop = pinTop.concat(topItems);
40
40
  let pinBottom = bottomItems.concat(logoutItem);
41
- if (isMobile && Boolean(settingItems?.length)) {
42
- pinBottom = pinBottom.concat([DIVIDER_ITEM, ...settingItems]);
43
- }
44
41
  // В шторке прокручивается всё меню, закреплять части незачем; разделитель над организациями добавляем сами
45
42
  if (isMobile) {
43
+ if (settingItems?.length) {
44
+ pinBottom = [...bottomItems, ...settingItems, DIVIDER_ITEM, logoutItem];
45
+ }
46
46
  return {
47
47
  pinTop: [],
48
48
  items: withClose([...pinTop, DIVIDER_ITEM, ...organizationItems, ...pinBottom]),
@@ -1,7 +1,16 @@
1
+ .userMenuDroplistTrigger{
2
+ display:inline-flex;
3
+ }
4
+
1
5
  .userMenuDroplist{
2
6
  width:256px;
3
7
  }
4
8
 
5
9
  .profileSettingsIcon{
6
10
  color:var(--sn-theme-color-neutral-text, var(--sn-brand-color-neutral-40, var(--sn-brand-palette-gray-40, #81869c)));
11
+ }
12
+
13
+ .logoutItem span,
14
+ .logoutItem svg{
15
+ color:var(--sn-theme-color-red-text, var(--sn-brand-palette-red-40, #a93533));
7
16
  }
@@ -26,4 +26,3 @@ export declare const TEST_IDS: {
26
26
  readonly root: "header__breadcrumbs";
27
27
  };
28
28
  };
29
- export declare const BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS: number[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS = exports.TEST_IDS = void 0;
3
+ exports.TEST_IDS = void 0;
4
4
  exports.TEST_IDS = {
5
5
  headerLayout: {
6
6
  root: 'header-layout',
@@ -29,5 +29,3 @@ exports.TEST_IDS = {
29
29
  root: 'header__breadcrumbs',
30
30
  },
31
31
  };
32
- // Только mobile: в мастере bottomSheet панель открывается на всю высоту экрана.
33
- exports.BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS = [1];
@@ -951,10 +951,19 @@
951
951
  color:var(--sn-theme-color-neutral-onAccent, var(--sn-brand-color-neutral-99, var(--sn-brand-palette-gray-99, #ffffff)));
952
952
  background:var(--sn-theme-color-neutral-accent, var(--sn-brand-color-neutral-50, var(--sn-brand-palette-gray-50, #8b8fa7)));
953
953
  }
954
+ .userMenuDroplistTrigger{
955
+ display:inline-flex;
956
+ }
957
+
954
958
  .userMenuDroplist{
955
959
  width:256px;
956
960
  }
957
961
 
958
962
  .profileSettingsIcon{
959
963
  color:var(--sn-theme-color-neutral-text, var(--sn-brand-color-neutral-40, var(--sn-brand-palette-gray-40, #81869c)));
964
+ }
965
+
966
+ .logoutItem span,
967
+ .logoutItem svg{
968
+ color:var(--sn-theme-color-red-text, var(--sn-brand-palette-red-40, #a93533));
960
969
  }
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Divider } from '@cloud-ru/ds-divider';
3
- import { CardServiceLight } from '@cloud-ru/ds-uikit-product-card-predefined';
3
+ import { CardNavigation } from '@cloud-ru/ds-uikit-product-card-predefined';
4
4
  import { Fragment } from 'react';
5
5
  import { TEST_IDS } from '../../../../constants.js';
6
6
  import { getLinkEmblem } from '../../utils/index.js';
7
7
  import styles from './styles.module.css';
8
8
  export function MenuBottom({ settingItems, leftBottom }) {
9
- return (_jsxs("div", { className: styles.bottom, "data-test-id": TEST_IDS.mainMenu.leftBottom, children: [Boolean(settingItems?.length) && (_jsx("div", { className: styles.bottomItems, "data-test-id": TEST_IDS.mainMenu.leftBottomItems, children: settingItems?.map(item => (_jsxs(Fragment, { children: [item.divider === 'before' && _jsx(Divider, { orientation: 'horizontal', className: styles.bottomDivider }), _jsx(CardServiceLight, { ...(item.href ? { href: item.href, as: 'a' } : { as: 'button', type: 'button' }), title: item.label, icon: getLinkEmblem(item), onClick: item.onClick, "data-test-id": `header__drawer-menu__setting-${item.id}` }), item.divider === 'after' && _jsx(Divider, { orientation: 'horizontal', className: styles.bottomDivider })] }, item.id))) })), leftBottom] }));
9
+ return (_jsxs("div", { className: styles.bottom, "data-test-id": TEST_IDS.mainMenu.leftBottom, children: [Boolean(settingItems?.length) && (_jsx("div", { className: styles.bottomItems, "data-test-id": TEST_IDS.mainMenu.leftBottomItems, children: settingItems?.map(item => (_jsxs(Fragment, { children: [item.divider === 'before' && _jsx(Divider, { orientation: 'horizontal', className: styles.bottomDivider }), _jsx(CardNavigation, { ...(item.href ? { href: item.href, as: 'a' } : { as: 'button', type: 'button' }), title: item.label, icon: getLinkEmblem(item), onClick: item.onClick, "data-test-id": `header__drawer-menu__setting-${item.id}` }), item.divider === 'after' && _jsx(Divider, { orientation: 'horizontal', className: styles.bottomDivider })] }, item.id))) })), leftBottom] }));
10
10
  }
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Divider } from '@cloud-ru/ds-divider';
3
+ import { SNAP_POINTS_PRESET } from '@cloud-ru/ds-list';
3
4
  import { useCallback, useMemo } from 'react';
4
- import { BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS, TEST_IDS } from '../../../../constants.js';
5
+ import { TEST_IDS } from '../../../../constants.js';
5
6
  import { MobileDrawerCustom } from '../../../../mobileOverlays.js';
6
7
  import { MainMenuDndContext } from '../../hooks/useMainMenuDnd.js';
7
8
  import { useMenuItems } from '../../hooks/useMenuItems.js';
@@ -32,5 +33,5 @@ export function MenuMobile({ open = false, setOpen, settingItems, platformGroups
32
33
  const handleClose = useCallback(() => {
33
34
  setOpen?.(false);
34
35
  }, [setOpen]);
35
- return (_jsx(MobileDrawerCustom, { open: open, onClose: handleClose, position: 'bottom', className: styles.drawerMobile, swipeEnabled: false, "data-test-id": TEST_IDS.mainMenu.drawerMobile, closeOnPopstate: true, snapPoints: BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS, disableMotions: true, children: _jsx(ScrollWithAnimatedStickyPanel, { panel: _jsxs(_Fragment, { children: [_jsx(MenuHeaderBrand, { logo: logo, onClose: handleClose, className: styles.menuHeader, isMobile: true }), _jsx(Divider, { orientation: 'horizontal' })] }), children: _jsxs(MountAnimation, { className: styles.scrollMobile, type: 'fade-slide-up', children: [leftTop, search && _jsx(Search, { ...search, ref: searchRef, isMobile: true }), !isSearching && favorite && _jsx(Favorites, { favorite: favorite, servicesById: servicesById, isMobile: true }), hasSegments && (_jsx(MainMenuDndContext, { children: _jsx(Content, { isMobile: true, searchValue: search && search.value, rightTop: rightTop, favorite: favorite, segments: segments, searchGroups: resultItems, segmentPrefs: segmentPrefs, activeSegmentId: activeSegmentId, onActiveSegmentChange: onActiveSegmentChange, onSegmentOrderChange: onSegmentOrderChange, onSegmentExpandedChange: onSegmentExpandedChange, onSegmentServiceClick: onSegmentServiceClick, loading: loading }) })), !isSearching && hasBottomItems && _jsx(MenuBottom, { settingItems: settingItems, leftBottom: leftBottom })] }) }) }));
36
+ return (_jsx(MobileDrawerCustom, { open: open, onClose: handleClose, position: 'bottom', className: styles.drawerMobile, swipeEnabled: false, "data-test-id": TEST_IDS.mainMenu.drawerMobile, closeOnPopstate: true, snapPoints: SNAP_POINTS_PRESET.full, disableMotions: true, children: _jsx(ScrollWithAnimatedStickyPanel, { panel: _jsxs(_Fragment, { children: [_jsx(MenuHeaderBrand, { logo: logo, onClose: handleClose, className: styles.menuHeader, isMobile: true }), _jsx(Divider, { orientation: 'horizontal' })] }), children: _jsxs(MountAnimation, { className: styles.scrollMobile, type: 'fade-slide-up', children: [leftTop, search && _jsx(Search, { ...search, ref: searchRef, isMobile: true }), !isSearching && favorite && _jsx(Favorites, { favorite: favorite, servicesById: servicesById, isMobile: true }), hasSegments && (_jsx(MainMenuDndContext, { children: _jsx(Content, { isMobile: true, searchValue: search && search.value, rightTop: rightTop, favorite: favorite, segments: segments, searchGroups: resultItems, segmentPrefs: segmentPrefs, activeSegmentId: activeSegmentId, onActiveSegmentChange: onActiveSegmentChange, onSegmentOrderChange: onSegmentOrderChange, onSegmentExpandedChange: onSegmentExpandedChange, onSegmentServiceClick: onSegmentServiceClick, loading: loading }) })), !isSearching && hasBottomItems && _jsx(MenuBottom, { settingItems: settingItems, leftBottom: leftBottom })] }) }) }));
36
37
  }
@@ -2,6 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Scroll } from '@cloud-ru/ds-scroll';
3
3
  import { useCallback, useRef, useState } from 'react';
4
4
  import styles from './styles.module.css';
5
+ // Упругая прокрутка у верхней границы (touch-скролл на мобильных не "упирается" жёстко в 0 —
6
+ // контент можно немного "оттянуть" дальше) даёт scrollTop, не совпадающий с реальным положением
7
+ // контента в этот момент. Диапазон запаса — единицы пикселей; двух хватает с большим запасом.
8
+ const TOP_OVERSCROLL_EPSILON_PX = 2;
5
9
  export function ScrollWithAnimatedStickyPanel({ panel, children }) {
6
10
  const [positions, setPositions] = useState({ panelTopShift: 0, containerScroll: 0 });
7
11
  const [panelHeight, setPanelHeight] = useState(0);
@@ -14,6 +18,13 @@ export function ScrollWithAnimatedStickyPanel({ panel, children }) {
14
18
  return;
15
19
  const panelHeight = panelRef.current.offsetHeight;
16
20
  setPanelHeight(panelHeight);
21
+ // У верхней границы панель всегда полностью видна — не доверяем накопленному diff. Иначе
22
+ // "лишний" ход упругой прокрутки у самого верха читается как скролл вниз и на возврате к
23
+ // началу списка панель может остаться скрытой, хотя пользователь как раз скроллит наверх.
24
+ if (target.scrollTop <= TOP_OVERSCROLL_EPSILON_PX) {
25
+ setPositions({ panelTopShift: 0, containerScroll: target.scrollTop });
26
+ return;
27
+ }
17
28
  setPositions(prev => {
18
29
  const diff = prev.containerScroll - target.scrollTop;
19
30
  return {
@@ -22,10 +33,11 @@ export function ScrollWithAnimatedStickyPanel({ panel, children }) {
22
33
  };
23
34
  });
24
35
  }, []);
25
- return (_jsxs("div", { className: styles.container, style: { '--snack-autohide-panel-height': `${panelHeight}px` }, children: [_jsx(Scroll, { barHideStrategy: 'never', overflow: { x: 'hidden' }, onScroll: handleScroll, children: children }), _jsx("div", { className: styles.panel, ref: element => {
26
- if (element) {
27
- setPanelHeight(element.offsetHeight);
28
- panelRef.current = element;
29
- }
30
- }, style: { top: positions.panelTopShift }, children: panel })] }));
36
+ const setPanelRef = useCallback((element) => {
37
+ if (element) {
38
+ setPanelHeight(element.offsetHeight);
39
+ panelRef.current = element;
40
+ }
41
+ }, []);
42
+ return (_jsxs("div", { className: styles.container, style: { '--snack-autohide-panel-height': `${panelHeight}px` }, children: [_jsx(Scroll, { barHideStrategy: 'never', overflow: { x: 'hidden' }, onScroll: handleScroll, children: children }), _jsx("div", { className: styles.panel, ref: setPanelRef, style: { top: positions.panelTopShift }, children: panel })] }));
31
43
  }
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Avatar } from '@cloud-ru/ds-avatar';
3
- import { Droplist } from '@cloud-ru/ds-list';
3
+ import { Droplist, SNAP_POINTS_PRESET } from '@cloud-ru/ds-list';
4
4
  import { useValueControl } from '@cloud-ru/ds-utils';
5
5
  import cn from 'classnames';
6
- import { useMemo } from 'react';
7
- import { BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS, TEST_IDS } from '../../constants.js';
6
+ import { useCallback, useMemo } from 'react';
7
+ import { TEST_IDS } from '../../constants.js';
8
8
  import { useDesktopComfortClassName } from '../../hooks/useDesktopComfortClassName.js';
9
9
  import { useMobileLayout } from '../../hooks/useMobileLayout.js';
10
10
  import { headerLocale } from '../../locale/index.js';
@@ -22,6 +22,9 @@ export function UserMenu({ profile = {}, open: openProp, setOpen: setOpenProp, o
22
22
  const comfortClassName = useDesktopComfortClassName();
23
23
  const [open = false, setOpen] = useValueControl({ value: openProp, onChange: setOpenProp });
24
24
  const { fullName = '', inviteCount } = profile;
25
+ const handleClose = useCallback(() => {
26
+ setOpen(false);
27
+ }, [setOpen]);
25
28
  const { pinTop, items: menuItems, pinBottom, } = useUserMenuItems({
26
29
  isMobile,
27
30
  profile,
@@ -30,9 +33,7 @@ export function UserMenu({ profile = {}, open: openProp, setOpen: setOpenProp, o
30
33
  organizationItems,
31
34
  bottomItems,
32
35
  settingItems,
33
- onClose: () => {
34
- setOpen(false);
35
- },
36
+ onClose: handleClose,
36
37
  onLogout,
37
38
  });
38
39
  const trigger = useMemo(() => (_jsx(HeaderButton, { tooltip: { tip: triggerTooltip }, isMobile: isMobile, onClick: () => {
@@ -44,5 +45,5 @@ export function UserMenu({ profile = {}, open: openProp, setOpen: setOpenProp, o
44
45
  appearance: 'primary',
45
46
  }
46
47
  : undefined, "data-test-id": TEST_IDS.userMenu.button, icon: _jsx(Avatar, { appearance: 'red', size: 's', name: fullName, showTwoSymbols: true }), "data-pressed": open })), [fullName, inviteCount, onClick, open, setOpen, isMobile, triggerTooltip]);
47
- return (_jsx(Droplist, { open: open, onOpenChange: setOpen, size: 'm', selection: SELECTION_STUB, pinTop: pinTop, items: menuItems, pinBottom: pinBottom, scroll: !isMobile, trigger: 'click', placement: 'bottom-end', className: cn(styles.userMenuDroplist, comfortClassName), closeOnPopstate: true, "data-test-id": TEST_IDS.userMenu.root, label: t('user'), snapPoints: BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS, children: trigger }));
48
+ return (_jsx(Droplist, { open: open, onOpenChange: setOpen, size: 'm', selection: SELECTION_STUB, pinTop: pinTop, items: menuItems, pinBottom: pinBottom, scroll: !isMobile, trigger: 'click', placement: 'bottom-end', className: cn(styles.userMenuDroplist, comfortClassName), closeOnPopstate: true, "data-test-id": TEST_IDS.userMenu.root, label: t('user'), snapPoints: SNAP_POINTS_PRESET.full, safeArea: true, withDividers: false, triggerClassName: styles.userMenuDroplistTrigger, children: trigger }));
48
49
  }
@@ -1,4 +1,4 @@
1
- import { ListProps } from '@cloud-ru/ds-list';
1
+ import { BaseItem } from '@cloud-ru/ds-list';
2
2
  export declare function useLogoutItem({ onLogout }: {
3
3
  onLogout?(): void;
4
- }): ListProps['items'];
4
+ }): BaseItem;
@@ -2,16 +2,16 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { ExitSVG } from '@cloud-ru/ds-icons/interface/product';
3
3
  import { useMemo } from 'react';
4
4
  import { headerLocale } from '../../../locale/index.js';
5
+ import styles from '../styles.module.css';
5
6
  export function useLogoutItem({ onLogout }) {
6
7
  const { t } = headerLocale.useTranslations();
7
- return useMemo(() => [
8
- {
9
- beforeContent: _jsx(ExitSVG, {}),
10
- content: {
11
- label: t('logout'),
12
- },
13
- onClick: onLogout,
14
- 'data-test-id': 'header__user-menu__logout',
8
+ return useMemo(() => ({
9
+ beforeContent: _jsx(ExitSVG, {}),
10
+ content: {
11
+ label: t('logout'),
15
12
  },
16
- ], [onLogout, t]);
13
+ className: styles.logoutItem,
14
+ onClick: onLogout,
15
+ 'data-test-id': 'header__user-menu__logout',
16
+ }), [onLogout, t]);
17
17
  }
@@ -35,11 +35,11 @@ export function useUserMenuItems({ profile, theme, onLogout, topItems = EMPTY_AR
35
35
  }
36
36
  pinTop = pinTop.concat(topItems);
37
37
  let pinBottom = bottomItems.concat(logoutItem);
38
- if (isMobile && Boolean(settingItems?.length)) {
39
- pinBottom = pinBottom.concat([DIVIDER_ITEM, ...settingItems]);
40
- }
41
38
  // В шторке прокручивается всё меню, закреплять части незачем; разделитель над организациями добавляем сами
42
39
  if (isMobile) {
40
+ if (settingItems?.length) {
41
+ pinBottom = [...bottomItems, ...settingItems, DIVIDER_ITEM, logoutItem];
42
+ }
43
43
  return {
44
44
  pinTop: [],
45
45
  items: withClose([...pinTop, DIVIDER_ITEM, ...organizationItems, ...pinBottom]),
@@ -1,7 +1,16 @@
1
+ .userMenuDroplistTrigger{
2
+ display:inline-flex;
3
+ }
4
+
1
5
  .userMenuDroplist{
2
6
  width:256px;
3
7
  }
4
8
 
5
9
  .profileSettingsIcon{
6
10
  color:var(--sn-theme-color-neutral-text, var(--sn-brand-color-neutral-40, var(--sn-brand-palette-gray-40, #81869c)));
11
+ }
12
+
13
+ .logoutItem span,
14
+ .logoutItem svg{
15
+ color:var(--sn-theme-color-red-text, var(--sn-brand-palette-red-40, #a93533));
7
16
  }
@@ -26,4 +26,3 @@ export declare const TEST_IDS: {
26
26
  readonly root: "header__breadcrumbs";
27
27
  };
28
28
  };
29
- export declare const BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS: number[];
@@ -26,5 +26,3 @@ export const TEST_IDS = {
26
26
  root: 'header__breadcrumbs',
27
27
  },
28
28
  };
29
- // Только mobile: в мастере bottomSheet панель открывается на всю высоту экрана.
30
- export const BOTTOM_SHEET_FULLSCREEN_SNAP_POINTS = [1];
@@ -951,10 +951,19 @@
951
951
  color:var(--sn-theme-color-neutral-onAccent, var(--sn-brand-color-neutral-99, var(--sn-brand-palette-gray-99, #ffffff)));
952
952
  background:var(--sn-theme-color-neutral-accent, var(--sn-brand-color-neutral-50, var(--sn-brand-palette-gray-50, #8b8fa7)));
953
953
  }
954
+ .userMenuDroplistTrigger{
955
+ display:inline-flex;
956
+ }
957
+
954
958
  .userMenuDroplist{
955
959
  width:256px;
956
960
  }
957
961
 
958
962
  .profileSettingsIcon{
959
963
  color:var(--sn-theme-color-neutral-text, var(--sn-brand-color-neutral-40, var(--sn-brand-palette-gray-40, #81869c)));
964
+ }
965
+
966
+ .logoutItem span,
967
+ .logoutItem svg{
968
+ color:var(--sn-theme-color-red-text, var(--sn-brand-palette-red-40, #a93533));
960
969
  }