@cloud-ru/ds-uikit-product-header 1.5.0 → 1.5.1-preview-c228f409.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 (118) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/components/MainMenu/MainMenu.js +9 -6
  3. package/dist/cjs/components/MainMenu/helperComponents/Content/Content.js +35 -11
  4. package/dist/cjs/components/MainMenu/helperComponents/Content/cardsContext.d.ts +20 -0
  5. package/dist/cjs/components/MainMenu/helperComponents/Content/cardsContext.js +17 -0
  6. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/SortableGroup.d.ts +16 -4
  7. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/SortableGroup.js +38 -10
  8. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupCards.d.ts +4 -4
  9. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupCards.js +9 -4
  10. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupHeader.d.ts +3 -1
  11. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupHeader.js +6 -4
  12. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategory.d.ts +6 -4
  13. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategory.js +16 -11
  14. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategoryTitle.d.ts +7 -3
  15. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategoryTitle.js +24 -18
  16. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/styles.module.css +17 -0
  17. package/dist/cjs/components/MainMenu/helperComponents/Content/hooks/useContentSegmentsSortable.js +4 -2
  18. package/dist/cjs/components/MainMenu/helperComponents/Content/hooks/useProgressiveCount.d.ts +12 -0
  19. package/dist/cjs/components/MainMenu/helperComponents/Content/hooks/useProgressiveCount.js +39 -0
  20. package/dist/cjs/components/MainMenu/helperComponents/Content/hooks/useSegmentDnd.d.ts +4 -4
  21. package/dist/cjs/components/MainMenu/helperComponents/Content/hooks/useSegmentDnd.js +3 -3
  22. package/dist/cjs/components/MainMenu/helperComponents/Content/styles.module.css +16 -0
  23. package/dist/cjs/components/MainMenu/helperComponents/Favorites/Favorites.d.ts +4 -4
  24. package/dist/cjs/components/MainMenu/helperComponents/Favorites/Favorites.js +12 -9
  25. package/dist/cjs/components/MainMenu/helperComponents/MenuDesktop/MenuDesktop.d.ts +1 -1
  26. package/dist/cjs/components/MainMenu/helperComponents/MenuDesktop/MenuDesktop.js +7 -7
  27. package/dist/cjs/components/MainMenu/helperComponents/MenuDesktop/styles.module.css +0 -5
  28. package/dist/cjs/components/MainMenu/helperComponents/MenuMobile/MenuMobile.d.ts +1 -1
  29. package/dist/cjs/components/MainMenu/helperComponents/MenuMobile/MenuMobile.js +5 -6
  30. package/dist/cjs/components/MainMenu/helperComponents/MenuMobile/styles.module.css +0 -7
  31. package/dist/cjs/components/MainMenu/helperComponents/MountAnimation/MountAnimation.d.ts +1 -1
  32. package/dist/cjs/components/MainMenu/helperComponents/MountAnimation/MountAnimation.js +4 -1
  33. package/dist/cjs/components/MainMenu/helperComponents/ServiceCard/ServiceCard.d.ts +23 -6
  34. package/dist/cjs/components/MainMenu/helperComponents/ServiceCard/ServiceCard.js +30 -21
  35. package/dist/cjs/components/MainMenu/helperComponents/ServiceCard/styles.module.css +6 -1
  36. package/dist/cjs/components/MainMenu/hooks/useMainMenuDnd.d.ts +4 -4
  37. package/dist/cjs/components/MainMenu/hooks/useMainMenuDnd.js +4 -6
  38. package/dist/cjs/components/MainMenu/hooks/useMenuItems.js +12 -7
  39. package/dist/cjs/components/MainMenu/utils/filterLinksGroupsFuzzy.d.ts +5 -0
  40. package/dist/cjs/components/MainMenu/utils/filterLinksGroupsFuzzy.js +21 -7
  41. package/dist/cjs/components/MainMenu/utils/flatLinksGroups.d.ts +2 -0
  42. package/dist/cjs/components/MainMenu/utils/flatLinksGroups.js +5 -0
  43. package/dist/cjs/components/MainMenu/utils/resolveInnerLinksByIds.d.ts +2 -2
  44. package/dist/cjs/components/MainMenu/utils/resolveInnerLinksByIds.js +2 -4
  45. package/dist/cjs/style.css +39 -13
  46. package/dist/esm/components/MainMenu/MainMenu.js +10 -7
  47. package/dist/esm/components/MainMenu/helperComponents/Content/Content.js +37 -13
  48. package/dist/esm/components/MainMenu/helperComponents/Content/cardsContext.d.ts +20 -0
  49. package/dist/esm/components/MainMenu/helperComponents/Content/cardsContext.js +13 -0
  50. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/SortableGroup.d.ts +16 -4
  51. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/SortableGroup.js +37 -9
  52. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupCards.d.ts +4 -4
  53. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupCards.js +9 -4
  54. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupHeader.d.ts +3 -1
  55. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupHeader.js +5 -3
  56. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategory.d.ts +6 -4
  57. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategory.js +17 -12
  58. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategoryTitle.d.ts +7 -3
  59. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategoryTitle.js +25 -19
  60. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/styles.module.css +17 -0
  61. package/dist/esm/components/MainMenu/helperComponents/Content/hooks/useContentSegmentsSortable.js +4 -2
  62. package/dist/esm/components/MainMenu/helperComponents/Content/hooks/useProgressiveCount.d.ts +12 -0
  63. package/dist/esm/components/MainMenu/helperComponents/Content/hooks/useProgressiveCount.js +35 -0
  64. package/dist/esm/components/MainMenu/helperComponents/Content/hooks/useSegmentDnd.d.ts +4 -4
  65. package/dist/esm/components/MainMenu/helperComponents/Content/hooks/useSegmentDnd.js +3 -3
  66. package/dist/esm/components/MainMenu/helperComponents/Content/styles.module.css +16 -0
  67. package/dist/esm/components/MainMenu/helperComponents/Favorites/Favorites.d.ts +4 -4
  68. package/dist/esm/components/MainMenu/helperComponents/Favorites/Favorites.js +12 -9
  69. package/dist/esm/components/MainMenu/helperComponents/MenuDesktop/MenuDesktop.d.ts +1 -1
  70. package/dist/esm/components/MainMenu/helperComponents/MenuDesktop/MenuDesktop.js +7 -7
  71. package/dist/esm/components/MainMenu/helperComponents/MenuDesktop/styles.module.css +0 -5
  72. package/dist/esm/components/MainMenu/helperComponents/MenuMobile/MenuMobile.d.ts +1 -1
  73. package/dist/esm/components/MainMenu/helperComponents/MenuMobile/MenuMobile.js +5 -6
  74. package/dist/esm/components/MainMenu/helperComponents/MenuMobile/styles.module.css +0 -7
  75. package/dist/esm/components/MainMenu/helperComponents/MountAnimation/MountAnimation.d.ts +1 -1
  76. package/dist/esm/components/MainMenu/helperComponents/MountAnimation/MountAnimation.js +4 -1
  77. package/dist/esm/components/MainMenu/helperComponents/ServiceCard/ServiceCard.d.ts +23 -6
  78. package/dist/esm/components/MainMenu/helperComponents/ServiceCard/ServiceCard.js +30 -20
  79. package/dist/esm/components/MainMenu/helperComponents/ServiceCard/styles.module.css +6 -1
  80. package/dist/esm/components/MainMenu/hooks/useMainMenuDnd.d.ts +4 -4
  81. package/dist/esm/components/MainMenu/hooks/useMainMenuDnd.js +6 -8
  82. package/dist/esm/components/MainMenu/hooks/useMenuItems.js +13 -8
  83. package/dist/esm/components/MainMenu/utils/filterLinksGroupsFuzzy.d.ts +5 -0
  84. package/dist/esm/components/MainMenu/utils/filterLinksGroupsFuzzy.js +20 -7
  85. package/dist/esm/components/MainMenu/utils/flatLinksGroups.d.ts +2 -0
  86. package/dist/esm/components/MainMenu/utils/flatLinksGroups.js +4 -0
  87. package/dist/esm/components/MainMenu/utils/resolveInnerLinksByIds.d.ts +2 -2
  88. package/dist/esm/components/MainMenu/utils/resolveInnerLinksByIds.js +2 -4
  89. package/dist/esm/style.css +39 -13
  90. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  91. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  92. package/package.json +28 -28
  93. package/src/components/MainMenu/MainMenu.tsx +11 -6
  94. package/src/components/MainMenu/helperComponents/Content/Content.tsx +100 -58
  95. package/src/components/MainMenu/helperComponents/Content/cardsContext.ts +36 -0
  96. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/SortableGroup.tsx +99 -47
  97. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupCards.tsx +17 -30
  98. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupHeader.tsx +11 -6
  99. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategory.tsx +69 -86
  100. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategoryTitle.tsx +64 -48
  101. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/styles.module.scss +27 -0
  102. package/src/components/MainMenu/helperComponents/Content/hooks/useContentSegmentsSortable.ts +4 -2
  103. package/src/components/MainMenu/helperComponents/Content/hooks/useProgressiveCount.ts +46 -0
  104. package/src/components/MainMenu/helperComponents/Content/hooks/useSegmentDnd.tsx +8 -7
  105. package/src/components/MainMenu/helperComponents/Content/styles.module.scss +19 -0
  106. package/src/components/MainMenu/helperComponents/Favorites/Favorites.tsx +26 -23
  107. package/src/components/MainMenu/helperComponents/MenuDesktop/MenuDesktop.tsx +17 -11
  108. package/src/components/MainMenu/helperComponents/MenuDesktop/styles.module.scss +0 -6
  109. package/src/components/MainMenu/helperComponents/MenuMobile/MenuMobile.tsx +6 -9
  110. package/src/components/MainMenu/helperComponents/MenuMobile/styles.module.scss +0 -7
  111. package/src/components/MainMenu/helperComponents/MountAnimation/MountAnimation.tsx +5 -1
  112. package/src/components/MainMenu/helperComponents/ServiceCard/ServiceCard.tsx +85 -41
  113. package/src/components/MainMenu/helperComponents/ServiceCard/styles.module.scss +7 -1
  114. package/src/components/MainMenu/hooks/useMainMenuDnd.tsx +7 -12
  115. package/src/components/MainMenu/hooks/useMenuItems.tsx +34 -10
  116. package/src/components/MainMenu/utils/filterLinksGroupsFuzzy.ts +29 -8
  117. package/src/components/MainMenu/utils/flatLinksGroups.ts +5 -0
  118. package/src/components/MainMenu/utils/resolveInnerLinksByIds.ts +3 -6
package/README.md CHANGED
@@ -478,7 +478,7 @@ import { MainMenu, MenuMobile } from '@cloud-ru/ds-uikit-product-header';
478
478
  | `onSegmentOrderChange` | `((segmentId: string, orderedGroupIds: string[]) => void)` | — | Колбэк после DnD групп в сегменте (без id синтетической группы избранного). |
479
479
  | `onSegmentServiceClick` | `((service: InnerLink, e?: MouseEvent<HTMLElement, MouseEvent>) => void)` | — | Колбэк клика по карточке сервиса в сегменте. |
480
480
  | `onWidthChangeEnd` | `((width: number) => void)` | — | Вызывается при окончании изменения ширины дровера (desktop only) |
481
- | `open` | `boolean` | — | |
481
+ | `open` | `boolean` | `false` | |
482
482
  | `platformGroups` | `LinksGroup` | — | Платформенные группы (например «Облачные продукты», «Другие продукты»). <br/> Без поиска в сетке карточек **не отображаются**. <br/> С поиском: попадают в результаты при совпадении; порядок — <br/> после совпадений из сегментов без `pinBottomOnSearch`, перед сегментами с `pinBottomOnSearch`. <br/> Обычно `favoritesEnabled: false`; карточки могут быть без `icon` (Avatar по `label`). |
483
483
  | `preferences` | `MainMenuPreferencesProps` | — | Настройки меню (модалка по кнопке в тулбаре): описания карточек, цвета групп. <br/> Не передано — кнопка настроек в тулбаре не отображается. |
484
484
  | `rightTop` | `ReactNode` | — | Слот над тулбаром правой колонки (например, баннеры) |
@@ -14,13 +14,16 @@ const MenuMobile_1 = require("./helperComponents/MenuMobile");
14
14
  function MainMenu({ open: openProp, setOpen: setOpenProp, settingItems, platformGroups, logo, leftTop, leftBottom, rightTop, segments, segmentPrefs, activeSegmentId, onActiveSegmentChange, onSegmentOrderChange, onSegmentExpandedChange, onSegmentServiceClick, favorite, search, preferences, disabled, defaultWidth, onWidthChangeEnd, draggerTooltip, loading, }) {
15
15
  const { t } = locale_1.headerLocale.useTranslations();
16
16
  const [open = false, setOpen] = (0, utils_1.useValueControl)({ value: openProp, onChange: setOpenProp });
17
+ const searchValue = search?.value;
18
+ const onSearchChange = search?.onChange;
17
19
  (0, react_1.useEffect)(() => {
18
- if (!open && search?.value)
19
- search?.onChange('');
20
- }, [open, search]);
20
+ if (!open && searchValue)
21
+ onSearchChange?.('');
22
+ }, [open, searchValue, onSearchChange]);
23
+ const handleOpen = (0, react_1.useCallback)(() => {
24
+ setOpen(true);
25
+ }, [setOpen]);
21
26
  const isMobile = (0, useMobileLayout_1.useMobileLayout)();
22
27
  const MenuComponent = isMobile ? MenuMobile_1.MenuMobile : MenuDesktop_1.MenuDesktop;
23
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(HeaderButton_1.HeaderButton, { tooltip: open ? undefined : { tip: t('services') }, isMobile: isMobile, disabled: disabled, icon: (0, jsx_runtime_1.jsx)(product_1.MainMenuSVG, {}), onClick: () => {
24
- setOpen(true);
25
- }, "data-test-id": constants_1.TEST_IDS.mainMenu.drawerButton }), (0, jsx_runtime_1.jsx)(MenuComponent, { settingItems: settingItems, segments: segments, segmentPrefs: segmentPrefs, activeSegmentId: activeSegmentId, onActiveSegmentChange: onActiveSegmentChange, onSegmentOrderChange: onSegmentOrderChange, onSegmentExpandedChange: onSegmentExpandedChange, onSegmentServiceClick: onSegmentServiceClick, platformGroups: platformGroups, search: search, logo: logo, leftTop: leftTop, leftBottom: leftBottom, rightTop: rightTop, favorite: favorite, open: open, setOpen: setOpen, preferences: preferences, defaultWidth: defaultWidth, onWidthChangeEnd: onWidthChangeEnd, draggerTooltip: draggerTooltip, loading: loading })] }));
28
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(HeaderButton_1.HeaderButton, { tooltip: open ? undefined : { tip: t('services') }, isMobile: isMobile, disabled: disabled, icon: (0, jsx_runtime_1.jsx)(product_1.MainMenuSVG, {}), onClick: handleOpen, "data-test-id": constants_1.TEST_IDS.mainMenu.drawerButton }), (0, jsx_runtime_1.jsx)(MenuComponent, { settingItems: settingItems, segments: segments, segmentPrefs: segmentPrefs, activeSegmentId: activeSegmentId, onActiveSegmentChange: onActiveSegmentChange, onSegmentOrderChange: onSegmentOrderChange, onSegmentExpandedChange: onSegmentExpandedChange, onSegmentServiceClick: onSegmentServiceClick, platformGroups: platformGroups, search: search, logo: logo, leftTop: leftTop, leftBottom: leftBottom, rightTop: rightTop, favorite: favorite, open: open, setOpen: setOpen, preferences: preferences, defaultWidth: defaultWidth, onWidthChangeEnd: onWidthChangeEnd, draggerTooltip: draggerTooltip, loading: loading })] }));
26
29
  }
@@ -15,12 +15,16 @@ const react_1 = require("react");
15
15
  const locale_1 = require("../../../../locale");
16
16
  const shouldBeOpenedInNewTab_1 = require("../../../../utils/shouldBeOpenedInNewTab");
17
17
  const utils_2 = require("../../utils");
18
+ const MountAnimation_1 = require("../MountAnimation");
19
+ const cardsContext_1 = require("./cardsContext");
18
20
  const constants_1 = require("./constants");
19
21
  const ContentToolbar_1 = require("./helperComponents/ContentToolbar");
20
22
  const SortableGroup_1 = require("./helperComponents/SortableGroup");
21
23
  const useContentSegmentsSortable_1 = require("./hooks/useContentSegmentsSortable");
24
+ const useProgressiveCount_1 = require("./hooks/useProgressiveCount");
22
25
  const useSegmentDnd_1 = require("./hooks/useSegmentDnd");
23
26
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
27
+ const noopFavoriteChange = () => { };
24
28
  function Content({ searchValue, search, rightTop, segments, searchGroups = [], segmentPrefs, activeSegmentId, onActiveSegmentChange, onSegmentOrderChange, onSegmentExpandedChange, onSegmentServiceClick, className, footer, favorite, isMobile, preferences, loading, }) {
25
29
  const { t } = locale_1.headerLocale.useTranslations();
26
30
  const showDescription = (0, react_1.useDeferredValue)(preferences?.showDescription.value ?? false);
@@ -33,7 +37,8 @@ function Content({ searchValue, search, rightTop, segments, searchGroups = [], s
33
37
  });
34
38
  const isSearching = Boolean(searchValue);
35
39
  const enableServiceDrag = Boolean(favorite) && !isMobile && !isSearching;
36
- const handleLinkClick = (0, react_1.useCallback)((service, e) => {
40
+ // Ссылки на обработчики стабильны: иначе `memo` карточек ломается при каждом рендере потребителя.
41
+ const handleLinkClick = (0, utils_1.useEventHandler)((service, e) => {
37
42
  if (service.disabled) {
38
43
  e?.preventDefault();
39
44
  return;
@@ -43,7 +48,11 @@ function Content({ searchValue, search, rightTop, segments, searchGroups = [], s
43
48
  }
44
49
  onSegmentServiceClick?.(service, e);
45
50
  service.onClick?.(e);
46
- }, [onSegmentServiceClick]);
51
+ });
52
+ const handleFavoriteChange = (0, utils_1.useEventHandler)((productId) => favorite ? favorite.onChange(productId) : noopFavoriteChange);
53
+ const favoriteValue = favorite?.value;
54
+ const favoriteIds = (0, react_1.useMemo)(() => (favoriteValue ? new Set(favoriteValue) : undefined), [favoriteValue]);
55
+ const showGroupsColors = preferences?.showGroupsColors?.value;
47
56
  const { orderedGroups, expandedIds, onExpandedChange, handleDragEnd: handleSortableDragEnd, } = (0, useContentSegmentsSortable_1.useContentSegmentsSortable)({
48
57
  segments,
49
58
  activeSegmentId: segmentId,
@@ -61,35 +70,50 @@ function Content({ searchValue, search, rightTop, segments, searchGroups = [], s
61
70
  expandedIds,
62
71
  onSortableDragEnd: handleSortableDragEnd,
63
72
  showDescription,
64
- showGroupsColors: preferences?.showGroupsColors?.value,
65
- favorite,
73
+ showGroupsColors,
74
+ favoriteIds,
75
+ onFavoriteChange: handleFavoriteChange,
66
76
  });
67
- const allGroupsExpanded = visibleGroups.length > 0 && visibleGroups.every(({ id }) => expandedIds.includes(id));
77
+ // Первый экран монтируется сразу, остальные группы дорисовываются порциями (см. `useProgressiveCount`).
78
+ const renderedGroupsCount = (0, useProgressiveCount_1.useProgressiveCount)(visibleGroups.length, `${segmentId}|${isSearching}`, !loading);
79
+ const renderedGroups = (0, react_1.useMemo)(() => (renderedGroupsCount < visibleGroups.length ? visibleGroups.slice(0, renderedGroupsCount) : visibleGroups), [visibleGroups, renderedGroupsCount]);
80
+ const expandedIdsSet = (0, react_1.useMemo)(() => new Set(expandedIds), [expandedIds]);
81
+ const visibleGroupIds = (0, react_1.useMemo)(() => visibleGroups.map(({ id }) => id), [visibleGroups]);
82
+ const allGroupsExpanded = visibleGroups.length > 0 && visibleGroupIds.every(id => expandedIdsSet.has(id));
68
83
  const handleToggleAllGroupsExpanded = (0, react_1.useCallback)(() => {
69
- const visibleIds = visibleGroups.map(({ id }) => id);
70
84
  if (allGroupsExpanded) {
71
- onExpandedChange(expandedIds.filter(id => !visibleIds.includes(id)));
85
+ const visibleIdsSet = new Set(visibleGroupIds);
86
+ onExpandedChange(expandedIds.filter(id => !visibleIdsSet.has(id)));
72
87
  return;
73
88
  }
74
- onExpandedChange([...new Set([...expandedIds, ...visibleIds])]);
75
- }, [allGroupsExpanded, expandedIds, onExpandedChange, visibleGroups]);
89
+ onExpandedChange([...new Set([...expandedIds, ...visibleGroupIds])]);
90
+ }, [allGroupsExpanded, expandedIds, onExpandedChange, visibleGroupIds]);
91
+ const cardsContext = (0, react_1.useMemo)(() => ({
92
+ showDescription,
93
+ isMobile,
94
+ dragEnabled: enableServiceDrag,
95
+ favoriteIds,
96
+ onFavoriteChange: favoriteIds ? handleFavoriteChange : undefined,
97
+ onServiceClick: handleLinkClick,
98
+ }), [showDescription, isMobile, enableServiceDrag, favoriteIds, handleFavoriteChange, handleLinkClick]);
76
99
  const segmentItems = (0, react_1.useMemo)(() => segments?.map(segment => ({
77
100
  value: segment.id,
78
101
  label: segment.label,
79
102
  icon: segment.icon,
80
103
  })), [segments]);
81
104
  const hasCards = visibleGroups.length > 0;
105
+ const mountAnimationType = isMobile ? undefined : 'fade-slide-up-right';
82
106
  const cards = (() => {
83
107
  if (loading) {
84
108
  return Array.from({ length: 5 }).map((_, index) => (0, jsx_runtime_1.jsx)(SortableGroup_1.SortableGroupSkeleton, { isMobile: isMobile }, index));
85
109
  }
86
110
  if (hasCards) {
87
- return ((0, jsx_runtime_1.jsx)(sortable_1.SortableContext, { items: visibleGroups.map(({ id }) => id), strategy: sortable_1.verticalListSortingStrategy, children: (0, jsx_runtime_1.jsx)(accordion_1.Accordion, { selectionMode: 'multiple', expanded: expandedIds, onExpandedChange: onExpandedChange, children: visibleGroups.map(({ id, label, icon, items, favoritesEnabled, blockColor, highlight }) => ((0, jsx_runtime_1.jsx)(SortableGroup_1.SortableGroup, { id: id, icon: icon, label: label, items: items, isExpanded: expandedIds.includes(id), blockColor: (0, utils_2.resolveGroupBlockColor)(blockColor, preferences?.showGroupsColors?.value), showDescription: showDescription, isMobile: isMobile, enableServiceDrag: enableServiceDrag, favorite: favorite, groupFavoritesEnabled: favoritesEnabled, onServiceClick: handleLinkClick, highlight: highlight }, id))) }) }));
111
+ return ((0, jsx_runtime_1.jsx)(sortable_1.SortableContext, { items: visibleGroupIds, strategy: sortable_1.verticalListSortingStrategy, children: (0, jsx_runtime_1.jsx)(cardsContext_1.CardsContext.Provider, { value: cardsContext, children: (0, jsx_runtime_1.jsx)(accordion_1.Accordion, { selectionMode: 'multiple', expanded: expandedIds, onExpandedChange: onExpandedChange, children: renderedGroups.map(({ id, label, icon, items, favoritesEnabled, blockColor, highlight }) => ((0, jsx_runtime_1.jsx)(SortableGroup_1.SortableGroup, { id: id, icon: icon, label: label, items: items, isExpanded: expandedIdsSet.has(id), blockColor: (0, utils_2.resolveGroupBlockColor)(blockColor, showGroupsColors), isMobile: isMobile, enableServiceDrag: enableServiceDrag, groupFavoritesEnabled: favoritesEnabled, appear: !isMobile, highlight: highlight }, id))) }) }) }));
88
112
  }
89
113
  return ((0, jsx_runtime_1.jsx)(info_block_1.InfoBlock, { size: 'm', icon: {
90
114
  icon: isSearching ? system_1.SearchSVG : system_1.CrossSVG,
91
115
  appearance: 'neutral',
92
116
  }, content: isSearching ? t('noDataFound') : t('noData'), "data-test-id": isSearching ? constants_1.TEST_IDS.noDataFound : constants_1.TEST_IDS.noData, "data-mobile": isMobile || undefined, className: styles_module_scss_1.default.noData }));
93
117
  })();
94
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [search, (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.content, className), "data-empty": (!loading && !hasCards) || undefined, children: [!isSearching && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ContentToolbar_1.ContentToolbar, { segment: segmentId, onSegmentChange: setSegmentId, segmentItems: segmentItems, allGroupsExpanded: allGroupsExpanded, onToggleAllGroupsExpanded: handleToggleAllGroupsExpanded, preferences: preferences, isMobile: isMobile, loading: loading }), rightTop] })), cards, footer] })] }));
118
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [search, (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.content, className), "data-mobile": isMobile || undefined, "data-empty": (!loading && !hasCards) || undefined, children: [!isSearching && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)(MountAnimation_1.MountAnimation, { type: mountAnimationType, className: styles_module_scss_1.default.top, children: [(0, jsx_runtime_1.jsx)(ContentToolbar_1.ContentToolbar, { segment: segmentId, onSegmentChange: setSegmentId, segmentItems: segmentItems, allGroupsExpanded: allGroupsExpanded, onToggleAllGroupsExpanded: handleToggleAllGroupsExpanded, preferences: preferences, isMobile: isMobile, loading: loading }), rightTop] }) })), cards, footer] })] }));
95
119
  }
@@ -0,0 +1,20 @@
1
+ import { MouseEvent } from 'react';
2
+ import { FavoriteProps, InnerLink } from '../../types';
3
+ export type CardsContextValue = {
4
+ showDescription: boolean;
5
+ isMobile?: boolean;
6
+ /** Карточки можно перетаскивать в избранное (иначе рендерятся без dnd-обвязки). */
7
+ dragEnabled: boolean;
8
+ /** Id избранных сервисов. Не передано — избранное для карточек недоступно. */
9
+ favoriteIds?: ReadonlySet<string>;
10
+ /** Стабильная по ссылке обёртка над `favorite.onChange`. */
11
+ onFavoriteChange?: FavoriteProps['onChange'];
12
+ /** Стабильная по ссылке обёртка над обработчиком клика по карточке. */
13
+ onServiceClick?(service: InnerLink, event?: MouseEvent<HTMLElement>): void;
14
+ };
15
+ /**
16
+ * Общие настройки карточек сервисов внутри группы. Заменяет сквозную передачу
17
+ * `showDescription` / `isMobile` / `favorite` / `onServiceClick` через все уровни сетки.
18
+ */
19
+ export declare const CardsContext: import("react").Context<CardsContextValue>;
20
+ export declare function useCardsContext(): CardsContextValue;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CardsContext = void 0;
4
+ exports.useCardsContext = useCardsContext;
5
+ const react_1 = require("react");
6
+ const DEFAULT_VALUE = {
7
+ showDescription: false,
8
+ dragEnabled: false,
9
+ };
10
+ /**
11
+ * Общие настройки карточек сервисов внутри группы. Заменяет сквозную передачу
12
+ * `showDescription` / `isMobile` / `favorite` / `onServiceClick` через все уровни сетки.
13
+ */
14
+ exports.CardsContext = (0, react_1.createContext)(DEFAULT_VALUE);
15
+ function useCardsContext() {
16
+ return (0, react_1.useContext)(exports.CardsContext);
17
+ }
@@ -1,12 +1,24 @@
1
1
  import { LinksGroup, LinksGroupBlockColor, LinksGroupTitle } from '../../../../types';
2
- import { SortableGroupCardsProps } from './components';
3
- export type SortableGroupProps = Omit<SortableGroupCardsProps, 'groupId'> & Pick<LinksGroup, 'icon'> & {
2
+ import { CardsContextValue } from '../../cardsContext';
3
+ export type SortableGroupProps = Pick<LinksGroup, 'icon' | 'items'> & {
4
4
  id: string;
5
5
  label: LinksGroupTitle;
6
6
  isExpanded?: boolean;
7
+ isMobile?: boolean;
8
+ /** Группу можно перетаскивать (иначе рендерится без `useSortable`). */
9
+ enableServiceDrag?: boolean;
7
10
  blockColor?: LinksGroupBlockColor;
8
11
  highlight?: boolean;
9
12
  groupFavoritesEnabled?: boolean;
13
+ /** Группа появляется с fade-in при монтировании (на desktop; на mobile анимирует контейнер). */
14
+ appear?: boolean;
10
15
  };
11
- export declare function SortableGroup({ id, icon, label, items, isExpanded, showDescription, isMobile, enableServiceDrag, favorite, groupFavoritesEnabled, onServiceClick, blockColor, highlight, }: SortableGroupProps): import("react/jsx-runtime").JSX.Element;
12
- export declare function SortableGroupDragPreview({ id, label, items, isExpanded, showDescription, isMobile, favorite, groupFavoritesEnabled, blockColor, highlight, }: Omit<SortableGroupProps, 'enableServiceDrag'>): import("react/jsx-runtime").JSX.Element;
16
+ declare function SortableGroupBase(props: SortableGroupProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare const SortableGroup: import("react").MemoExoticComponent<typeof SortableGroupBase>;
18
+ export type SortableGroupDragPreviewProps = Omit<SortableGroupProps, 'enableServiceDrag'> & Pick<CardsContextValue, 'showDescription' | 'favoriteIds' | 'onFavoriteChange'>;
19
+ /**
20
+ * Превью группы в `DragOverlay`. Рендерится вне `Content`, поэтому свой `CardsContext` собирает
21
+ * сам: без drag-обвязки карточек и без обработчика клика.
22
+ */
23
+ export declare function SortableGroupDragPreview({ id, label, items, isExpanded, showDescription, isMobile, favoriteIds, onFavoriteChange, groupFavoritesEnabled, blockColor, highlight, }: SortableGroupDragPreviewProps): import("react/jsx-runtime").JSX.Element;
24
+ export {};
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SortableGroup = SortableGroup;
6
+ exports.SortableGroup = void 0;
7
7
  exports.SortableGroupDragPreview = SortableGroupDragPreview;
8
8
  const jsx_runtime_1 = require("react/jsx-runtime");
9
9
  const sortable_1 = require("@dnd-kit/sortable");
@@ -11,14 +11,27 @@ const utilities_1 = require("@dnd-kit/utilities");
11
11
  const accordion_1 = require("@cloud-ru/ds-accordion");
12
12
  const drag_and_drop_1 = require("@cloud-ru/ds-drag-and-drop");
13
13
  const theme_1 = require("@cloud-ru/ds-theme");
14
+ const react_1 = require("react");
15
+ const cardsContext_1 = require("../../cardsContext");
14
16
  const constants_1 = require("../../constants");
15
17
  const components_1 = require("./components");
16
18
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
17
- function SortableGroup({ id, icon, label, items, isExpanded, showDescription, isMobile, enableServiceDrag, favorite, groupFavoritesEnabled = true, onServiceClick, blockColor, highlight, }) {
18
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = (0, sortable_1.useSortable)({
19
- id,
20
- disabled: !enableServiceDrag,
21
- });
19
+ function GroupView({ id, icon, label, items, isExpanded, isMobile, enableServiceDrag, groupFavoritesEnabled = true, blockColor, highlight, appear, attributes, listeners, setNodeRef, style, isDragging, }) {
20
+ const { colorScheme } = (0, theme_1.useThemeAppearance)().appearance;
21
+ // Появление проигрывается один раз. React переставляет узлы `insertBefore`, а браузер при
22
+ // повторной вставке перезапускает CSS-анимацию — при перетаскивании группы соседи «моргали».
23
+ // После окончания флаг снимается, и перестановка анимацию уже не запускает.
24
+ const [isAppearing, setIsAppearing] = (0, react_1.useState)(Boolean(appear));
25
+ const handleAnimationEnd = (0, react_1.useCallback)((event) => {
26
+ // `animationend` всплывает от вложенных анимаций — нужна только своя.
27
+ if (event.target === event.currentTarget) {
28
+ setIsAppearing(false);
29
+ }
30
+ }, []);
31
+ return ((0, jsx_runtime_1.jsxs)(drag_and_drop_1.DragGhost, { innerRef: setNodeRef, id: id, style: style, className: styles_module_scss_1.default.group, dragging: isDragging, mode: drag_and_drop_1.DRAG_MODE.Dynamic, "data-appear": isAppearing || undefined, onAnimationEnd: handleAnimationEnd, "data-test-id": `${constants_1.TEST_IDS.groupCard}-${id}`, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.decoration, "data-color-scheme": colorScheme, "data-is-highlighted": highlight || undefined, children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.decorationBackground, children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.colorMarker, "data-block-color": blockColor }) }) }), (0, jsx_runtime_1.jsx)(accordion_1.Accordion.CollapseBlockTertiary, { id: id, showChevron: false, afterTitle: (0, jsx_runtime_1.jsx)(components_1.SortableGroupHeader, { label: label, icon: icon, isExpanded: isExpanded, enableServiceDrag: enableServiceDrag, attributes: attributes, listeners: listeners, isMobile: isMobile }), children: (0, jsx_runtime_1.jsx)(components_1.SortableGroupCards, { groupId: id, items: items, groupFavoritesEnabled: groupFavoritesEnabled }) })] }));
32
+ }
33
+ function DraggableGroup(props) {
34
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = (0, sortable_1.useSortable)({ id: props.id });
22
35
  const style = {
23
36
  // CSS.Transform добавляет scaleX/scaleY, из-за чего перетаскиваемая группа "растягивается"
24
37
  // при пересечении с соседней группой другой высоты. CSS.Translate использует только сдвиг.
@@ -27,10 +40,25 @@ function SortableGroup({ id, icon, label, items, isExpanded, showDescription, is
27
40
  transform: transform ? utilities_1.CSS.Translate.toString(transform) : undefined,
28
41
  transition,
29
42
  };
30
- const { colorScheme } = (0, theme_1.useThemeAppearance)().appearance;
31
- return ((0, jsx_runtime_1.jsxs)(drag_and_drop_1.DragGhost, { innerRef: setNodeRef, id: id, style: style, className: styles_module_scss_1.default.group, dragging: isDragging, mode: drag_and_drop_1.DRAG_MODE.Dynamic, "data-test-id": `${constants_1.TEST_IDS.groupCard}-${id}`, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.decoration, "data-color-scheme": colorScheme, "data-is-highlighted": highlight || undefined, children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.decorationBackground, children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.colorMarker, "data-block-color": blockColor }) }) }), (0, jsx_runtime_1.jsx)(accordion_1.Accordion.CollapseBlockTertiary, { id: id, showChevron: false, afterTitle: (0, jsx_runtime_1.jsx)(components_1.SortableGroupHeader, { label: label, icon: icon, isExpanded: isExpanded, enableServiceDrag: enableServiceDrag, attributes: attributes, listeners: listeners, isMobile: isMobile }), children: (0, jsx_runtime_1.jsx)(components_1.SortableGroupCards, { groupId: id, items: items, showDescription: showDescription, isMobile: isMobile, enableServiceDrag: enableServiceDrag, favorite: favorite, groupFavoritesEnabled: groupFavoritesEnabled, onServiceClick: onServiceClick }) })] }));
43
+ return ((0, jsx_runtime_1.jsx)(GroupView, { ...props, attributes: attributes, listeners: listeners, setNodeRef: setNodeRef, style: style, isDragging: isDragging }));
32
44
  }
33
- function SortableGroupDragPreview({ id, label, items, isExpanded, showDescription, isMobile, favorite, groupFavoritesEnabled, blockColor, highlight, }) {
45
+ function SortableGroupBase(props) {
46
+ // Без drag (mobile, поиск, нет избранного) группа не регистрируется в `SortableContext`.
47
+ return props.enableServiceDrag ? (0, jsx_runtime_1.jsx)(DraggableGroup, { ...props }) : (0, jsx_runtime_1.jsx)(GroupView, { ...props });
48
+ }
49
+ exports.SortableGroup = (0, react_1.memo)(SortableGroupBase);
50
+ /**
51
+ * Превью группы в `DragOverlay`. Рендерится вне `Content`, поэтому свой `CardsContext` собирает
52
+ * сам: без drag-обвязки карточек и без обработчика клика.
53
+ */
54
+ function SortableGroupDragPreview({ id, label, items, isExpanded, showDescription, isMobile, favoriteIds, onFavoriteChange, groupFavoritesEnabled, blockColor, highlight, }) {
34
55
  const { colorScheme } = (0, theme_1.useThemeAppearance)().appearance;
35
- return ((0, jsx_runtime_1.jsx)(drag_and_drop_1.DragPreview, { className: styles_module_scss_1.default.groupDragPreview, children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.group, "data-drag-preview": true, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.decoration, "data-color-scheme": colorScheme, "data-is-highlighted": highlight || undefined, children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.decorationBackground, children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.colorMarker, "data-block-color": blockColor }) }) }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.groupDragPreviewHeader, children: (0, jsx_runtime_1.jsx)(components_1.SortableGroupHeader, { label: label, isExpanded: isExpanded, isMobile: isMobile, enableServiceDrag: true }) }), isExpanded && ((0, jsx_runtime_1.jsx)(components_1.SortableGroupCards, { groupId: id, items: items, showDescription: showDescription, isMobile: isMobile, favorite: favorite, groupFavoritesEnabled: groupFavoritesEnabled }))] }) }));
56
+ const cardsContext = {
57
+ showDescription,
58
+ isMobile,
59
+ dragEnabled: false,
60
+ favoriteIds,
61
+ onFavoriteChange,
62
+ };
63
+ return ((0, jsx_runtime_1.jsx)(drag_and_drop_1.DragPreview, { className: styles_module_scss_1.default.groupDragPreview, children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.group, "data-drag-preview": true, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.decoration, "data-color-scheme": colorScheme, "data-is-highlighted": highlight || undefined, children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.decorationBackground, children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.colorMarker, "data-block-color": blockColor }) }) }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.groupDragPreviewHeader, children: (0, jsx_runtime_1.jsx)(components_1.SortableGroupHeader, { label: label, isExpanded: isExpanded, isMobile: isMobile, enableServiceDrag: true }) }), isExpanded && ((0, jsx_runtime_1.jsx)(cardsContext_1.CardsContext.Provider, { value: cardsContext, children: (0, jsx_runtime_1.jsx)(components_1.SortableGroupCards, { groupId: id, items: items, groupFavoritesEnabled: groupFavoritesEnabled }) }))] }) }));
36
64
  }
@@ -1,9 +1,9 @@
1
1
  import { InnerLink } from '../../../../../types';
2
- import { ServiceCardProps } from '../../../../ServiceCard';
3
- export type SortableGroupCardsProps = Pick<ServiceCardProps, 'showDescription' | 'isMobile' | 'favorite' | 'onServiceClick'> & {
2
+ export type SortableGroupCardsProps = {
4
3
  groupId: string;
5
4
  items: InnerLink[];
6
- enableServiceDrag?: boolean;
7
5
  groupFavoritesEnabled?: boolean;
8
6
  };
9
- export declare function SortableGroupCards({ groupId, items, showDescription, isMobile, enableServiceDrag, favorite, groupFavoritesEnabled, onServiceClick, }: SortableGroupCardsProps): import("react/jsx-runtime").JSX.Element;
7
+ declare function SortableGroupCardsBase({ groupId, items, groupFavoritesEnabled }: SortableGroupCardsProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare const SortableGroupCards: import("react").MemoExoticComponent<typeof SortableGroupCardsBase>;
9
+ export {};
@@ -3,21 +3,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SortableGroupCards = SortableGroupCards;
6
+ exports.SortableGroupCards = void 0;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
8
9
  const innerLink_1 = require("../../../../../utils/innerLink");
9
10
  const ServiceCard_1 = require("../../../../ServiceCard");
11
+ const cardsContext_1 = require("../../../cardsContext");
10
12
  const styles_module_scss_1 = __importDefault(require('../styles.module.css'));
11
13
  const SubCategory_1 = require("./SubCategory");
12
- function SortableGroupCards({ groupId, items, showDescription, isMobile, enableServiceDrag, favorite, groupFavoritesEnabled, onServiceClick, }) {
14
+ function SortableGroupCardsBase({ groupId, items, groupFavoritesEnabled }) {
15
+ const { isMobile, showDescription, dragEnabled, favoriteIds, onFavoriteChange, onServiceClick } = (0, cardsContext_1.useCardsContext)();
16
+ const cardFavoriteChange = favoriteIds ? onFavoriteChange : undefined;
13
17
  return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.groupBody, "data-mobile": isMobile || undefined, children: items.map(service => {
14
18
  if (service.hidden) {
15
19
  return null;
16
20
  }
17
21
  const key = String(groupId) + service.id;
18
22
  if ((0, innerLink_1.isSubCategoryCard)(service)) {
19
- return ((0, jsx_runtime_1.jsx)(SubCategory_1.SubCategory, { groupId: groupId, service: service, showDescription: showDescription, isMobile: isMobile, dragDisabled: !enableServiceDrag, favorite: favorite, groupFavoritesEnabled: groupFavoritesEnabled, onServiceClick: onServiceClick }, key));
23
+ return ((0, jsx_runtime_1.jsx)(SubCategory_1.SubCategory, { groupId: groupId, service: service, groupFavoritesEnabled: groupFavoritesEnabled }, key));
20
24
  }
21
- return ((0, jsx_runtime_1.jsx)(ServiceCard_1.DraggableServiceCard, { groupId: groupId, service: service, favorite: favorite, isMobile: isMobile, onServiceClick: onServiceClick, showDescription: showDescription, dragDisabled: !enableServiceDrag }, key));
25
+ return ((0, jsx_runtime_1.jsx)(ServiceCard_1.GridServiceCard, { groupId: groupId, service: service, isFavorite: favoriteIds?.has(service.id), onFavoriteChange: cardFavoriteChange, isMobile: isMobile, onServiceClick: onServiceClick, showDescription: showDescription, dragEnabled: dragEnabled }, key));
22
26
  }) }));
23
27
  }
28
+ exports.SortableGroupCards = (0, react_1.memo)(SortableGroupCardsBase);
@@ -6,4 +6,6 @@ export type SortableGroupHeaderProps = Pick<LinksGroup, 'icon'> & {
6
6
  enableServiceDrag?: boolean;
7
7
  isMobile?: boolean;
8
8
  } & SortableGroupDragHandleProps;
9
- export declare function SortableGroupHeader({ label, icon, isExpanded, enableServiceDrag, attributes, listeners, isMobile, }: SortableGroupHeaderProps): import("react/jsx-runtime").JSX.Element;
9
+ declare function SortableGroupHeaderBase({ label, icon, isExpanded, enableServiceDrag, attributes, listeners, isMobile, }: SortableGroupHeaderProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare const SortableGroupHeader: import("react").MemoExoticComponent<typeof SortableGroupHeaderBase>;
11
+ export {};
@@ -3,19 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SortableGroupHeader = SortableGroupHeader;
6
+ exports.SortableGroupHeader = void 0;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const button_1 = require("@cloud-ru/ds-button");
9
9
  const system_1 = require("@cloud-ru/ds-icons/interface/system");
10
10
  const typography_1 = require("@cloud-ru/ds-typography");
11
11
  const uikit_product_title_clickable_1 = require("@cloud-ru/ds-uikit-product-title-clickable");
12
+ const react_1 = require("react");
12
13
  const utils_1 = require("../../../../../utils");
13
14
  const styles_module_scss_1 = __importDefault(require('../styles.module.css'));
14
15
  const SortableGroupDragHandle_1 = require("./SortableGroupDragHandle");
15
- function SortableGroupHeader({ label, icon, isExpanded, enableServiceDrag, attributes, listeners, isMobile, }) {
16
- const handleLabelClick = e => {
16
+ function SortableGroupHeaderBase({ label, icon, isExpanded, enableServiceDrag, attributes, listeners, isMobile, }) {
17
+ const handleLabelClick = (0, react_1.useCallback)(e => {
17
18
  e.stopPropagation();
18
19
  label?.onClick?.(e);
19
- };
20
+ }, [label]);
20
21
  return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.header, "data-expanded": isExpanded || undefined, children: [(0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: 'title', size: 'm', className: styles_module_scss_1.default.headerTitle, "data-test-id": 'header__drawer-menu__group-card-title', children: label.onClick || label.href ? ((0, jsx_runtime_1.jsx)(uikit_product_title_clickable_1.TitleClickable, { ...(label.href ? { as: 'a', href: label.href } : { as: 'div' }), onClick: handleLabelClick, title: label.text, icon: (0, utils_1.getLinkEmblem)({ icon }) })) : (label.text) }), (enableServiceDrag || !isMobile) && ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.headerActions, "data-always-visible": isMobile || undefined, children: [enableServiceDrag && (0, jsx_runtime_1.jsx)(SortableGroupDragHandle_1.SortableGroupDragHandle, { attributes: attributes, listeners: listeners }), !isMobile && ((0, jsx_runtime_1.jsx)(button_1.Button, { view: 'elevated', appearance: 'neutral', size: 's', icon: isExpanded ? (0, jsx_runtime_1.jsx)(system_1.ChevronUpSVG, {}) : (0, jsx_runtime_1.jsx)(system_1.ChevronDownSVG, {}), "data-test-id": 'header__drawer-menu__group-card-collapse' }))] }))] }));
21
22
  }
23
+ exports.SortableGroupHeader = (0, react_1.memo)(SortableGroupHeaderBase);
@@ -1,7 +1,9 @@
1
- import { ServiceCardProps } from '../../../../../ServiceCard';
2
- export type SubCategoryProps = Pick<ServiceCardProps, 'service' | 'onServiceClick' | 'favorite' | 'showDescription' | 'isMobile'> & {
1
+ import { InnerLink } from '../../../../../../types';
2
+ export type SubCategoryProps = {
3
3
  groupId: string;
4
- dragDisabled?: boolean;
4
+ service: InnerLink;
5
5
  groupFavoritesEnabled?: boolean;
6
6
  };
7
- export declare function SubCategory({ groupId, service, showDescription, isMobile, dragDisabled, favorite, onServiceClick, groupFavoritesEnabled, }: SubCategoryProps): import("react/jsx-runtime").JSX.Element;
7
+ declare function SubCategoryBase({ groupId, service, groupFavoritesEnabled }: SubCategoryProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare const SubCategory: import("react").MemoExoticComponent<typeof SubCategoryBase>;
9
+ export {};
@@ -3,18 +3,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SubCategory = SubCategory;
6
+ exports.SubCategory = void 0;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const accordion_1 = require("@cloud-ru/ds-accordion");
9
9
  const react_1 = require("react");
10
10
  const innerLink_1 = require("../../../../../../utils/innerLink");
11
11
  const ServiceCard_1 = require("../../../../../ServiceCard");
12
+ const cardsContext_1 = require("../../../../cardsContext");
12
13
  const constants_1 = require("../../../../constants");
13
14
  const styles_module_scss_1 = __importDefault(require('../../styles.module.css'));
14
15
  const styles_module_scss_2 = __importDefault(require('./styles.module.css'));
15
16
  const SubCategoryTitle_1 = require("./SubCategoryTitle");
16
17
  const noop = () => { };
17
- function SubCategory({ groupId, service, showDescription, isMobile, dragDisabled, favorite, onServiceClick, groupFavoritesEnabled, }) {
18
+ const TITLE_ONLY_EXPANDABLE = { value: true, onClick: noop };
19
+ const SubCategoryBody = (0, react_1.memo)(function SubCategoryBody({ nestedGroupId, items, variant }) {
20
+ const { isMobile, showDescription, dragEnabled, favoriteIds, onFavoriteChange, onServiceClick } = (0, cardsContext_1.useCardsContext)();
21
+ const cardFavoriteChange = favoriteIds ? onFavoriteChange : undefined;
22
+ return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.groupBody, "data-subcategory-body": variant === 'expanded' ? 'expanded' : true, "data-mobile": isMobile || undefined, children: items.map(nestedService => ((0, jsx_runtime_1.jsx)(ServiceCard_1.GridServiceCard, { groupId: nestedGroupId, service: nestedService, isFavorite: favoriteIds?.has(nestedService.id), onFavoriteChange: cardFavoriteChange, isMobile: isMobile, onServiceClick: onServiceClick, showDescription: showDescription, dragEnabled: dragEnabled }, nestedGroupId + nestedService.id))) }));
23
+ });
24
+ function SubCategoryBase({ groupId, service, groupFavoritesEnabled }) {
18
25
  const subcategoryId = (0, innerLink_1.getSubCategoryId)(groupId, service.id);
19
26
  const nestedGroupId = (0, innerLink_1.getNestedServiceGroupId)(groupId, service.id);
20
27
  const nestedItems = (0, react_1.useMemo)(() => service.items?.filter(item => !item.hidden) ?? [], [service.items]);
@@ -28,19 +35,17 @@ function SubCategory({ groupId, service, showDescription, isMobile, dragDisabled
28
35
  setIsExpanded(isExpanded ? undefined : subcategoryId);
29
36
  }
30
37
  }, [isExpanded, subcategoryId, isExpandableEnabled]);
38
+ const expandable = (0, react_1.useMemo)(() => ({ value: Boolean(isExpanded), onClick: handleExpandedChange }), [isExpanded, handleExpandedChange]);
39
+ const testId = `${constants_1.TEST_IDS.subcategory}-${service.id}`;
40
+ const title = ((0, jsx_runtime_1.jsx)(SubCategoryTitle_1.SubCategoryTitle, { groupId: groupId, service: service, expandable: viewMode === 'group-title-only' ? TITLE_ONLY_EXPANDABLE : expandable, groupFavoritesEnabled: groupFavoritesEnabled }));
31
41
  // 'group-title-only' — только заголовок подкатегории, без раскрываемого тела и кнопки
32
42
  // переключения: карточка ведёт себя как обычная ссылка (см. isSubCategoryCard в utils/innerLink).
33
43
  if (viewMode === 'group-title-only') {
34
- return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.subcategory, "data-title-only": true, "data-test-id": `${constants_1.TEST_IDS.subcategory}-${service.id}`, children: (0, jsx_runtime_1.jsx)(SubCategoryTitle_1.SubCategoryTitle, { groupId: groupId, service: service, expandable: { value: true, onClick: noop }, showDescription: showDescription, isMobile: isMobile, dragDisabled: dragDisabled, favorite: groupFavoritesEnabled ? favorite : undefined, onServiceClick: onServiceClick }) }));
44
+ return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.subcategory, "data-title-only": true, "data-test-id": testId, children: title }));
35
45
  }
36
46
  if (viewMode === 'expanded') {
37
- return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_2.default.subcategory, "data-test-id": `${constants_1.TEST_IDS.subcategory}-${service.id}`, children: [(0, jsx_runtime_1.jsx)(SubCategoryTitle_1.SubCategoryTitle, { groupId: groupId, service: service, expandable: {
38
- value: Boolean(isExpanded),
39
- onClick: handleExpandedChange,
40
- }, showDescription: showDescription, isMobile: isMobile, dragDisabled: dragDisabled, favorite: groupFavoritesEnabled ? favorite : undefined, onServiceClick: onServiceClick }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.groupBody, "data-subcategory-body": 'expanded', "data-mobile": isMobile || undefined, children: nestedItems.map(nestedService => ((0, jsx_runtime_1.jsx)(ServiceCard_1.DraggableServiceCard, { groupId: nestedGroupId, service: nestedService, favorite: favorite, isMobile: isMobile, onServiceClick: onServiceClick, showDescription: showDescription, dragDisabled: dragDisabled }, nestedGroupId + nestedService.id))) })] }));
47
+ return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_2.default.subcategory, "data-test-id": testId, children: [title, (0, jsx_runtime_1.jsx)(SubCategoryBody, { nestedGroupId: nestedGroupId, items: nestedItems, variant: 'expanded' })] }));
41
48
  }
42
- return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.subcategory, "data-test-id": `${constants_1.TEST_IDS.subcategory}-${service.id}`, children: (0, jsx_runtime_1.jsx)(accordion_1.Accordion, { selectionMode: 'single', expanded: isExpanded, onExpandedChange: handleExpandedChange, children: (0, jsx_runtime_1.jsx)(accordion_1.Accordion.CollapseBlockTertiary, { id: subcategoryId, showChevron: false, className: styles_module_scss_2.default.subcategoryAccordion, afterTitle: (0, jsx_runtime_1.jsx)(SubCategoryTitle_1.SubCategoryTitle, { groupId: groupId, service: service, expandable: {
43
- value: Boolean(isExpanded),
44
- onClick: handleExpandedChange,
45
- }, showDescription: showDescription, isMobile: isMobile, dragDisabled: dragDisabled, favorite: groupFavoritesEnabled ? favorite : undefined, onServiceClick: onServiceClick }), children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.groupBody, "data-subcategory-body": true, "data-mobile": isMobile || undefined, children: nestedItems.map(nestedService => ((0, jsx_runtime_1.jsx)(ServiceCard_1.DraggableServiceCard, { groupId: nestedGroupId, service: nestedService, favorite: favorite, isMobile: isMobile, onServiceClick: onServiceClick, showDescription: showDescription, dragDisabled: dragDisabled }, nestedGroupId + nestedService.id))) }) }) }) }));
49
+ return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.subcategory, "data-test-id": testId, children: (0, jsx_runtime_1.jsx)(accordion_1.Accordion, { selectionMode: 'single', expanded: isExpanded, onExpandedChange: handleExpandedChange, children: (0, jsx_runtime_1.jsx)(accordion_1.Accordion.CollapseBlockTertiary, { id: subcategoryId, showChevron: false, className: styles_module_scss_2.default.subcategoryAccordion, afterTitle: title, children: (0, jsx_runtime_1.jsx)(SubCategoryBody, { nestedGroupId: nestedGroupId, items: nestedItems, variant: 'expandable' }) }) }) }));
46
50
  }
51
+ exports.SubCategory = (0, react_1.memo)(SubCategoryBase);
@@ -1,5 +1,9 @@
1
- import { DraggableServiceCardProps } from '../../../../../ServiceCard';
1
+ import { GridServiceCardProps } from '../../../../../ServiceCard';
2
2
  export type SubCategoryTitleProps = {
3
3
  groupId: string;
4
- } & Pick<DraggableServiceCardProps, 'expandable' | 'service' | 'onServiceClick' | 'favorite' | 'showDescription' | 'isMobile' | 'dragDisabled'>;
5
- export declare function SubCategoryTitle({ groupId, service, showDescription: showDescriptionProp, isMobile, dragDisabled, favorite, onServiceClick, expandable, }: SubCategoryTitleProps): import("react/jsx-runtime").JSX.Element;
4
+ /** Кнопка избранного показывается только если для группы избранное включено. */
5
+ groupFavoritesEnabled?: boolean;
6
+ } & Pick<GridServiceCardProps, 'expandable' | 'service'>;
7
+ declare function SubCategoryTitleBase({ groupId, service, groupFavoritesEnabled, expandable }: SubCategoryTitleProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare const SubCategoryTitle: import("react").MemoExoticComponent<typeof SubCategoryTitleBase>;
9
+ export {};
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SubCategoryTitle = SubCategoryTitle;
6
+ exports.SubCategoryTitle = void 0;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const typography_1 = require("@cloud-ru/ds-typography");
9
9
  const uikit_product_card_predefined_1 = require("@cloud-ru/ds-uikit-product-card-predefined");
@@ -12,53 +12,59 @@ const utils_1 = require("@cloud-ru/ds-utils");
12
12
  const react_1 = require("react");
13
13
  const utils_2 = require("../../../../../../utils");
14
14
  const ServiceCard_1 = require("../../../../../ServiceCard");
15
+ const cardsContext_1 = require("../../../../cardsContext");
15
16
  const constants_1 = require("../../../../constants");
16
17
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
17
- function SubCategoryTitle({ groupId, service, showDescription: showDescriptionProp, isMobile, dragDisabled, favorite, onServiceClick, expandable, }) {
18
+ function SubCategoryTitleBase({ groupId, service, groupFavoritesEnabled, expandable }) {
19
+ const { isMobile, showDescription: showDescriptionProp, dragEnabled, favoriteIds, onFavoriteChange, onServiceClick, } = (0, cardsContext_1.useCardsContext)();
18
20
  const titleRef = (0, react_1.useRef)(null);
19
21
  const tooltipTriggerRef = (0, react_1.useRef)(null);
20
22
  const favoriteRef = (0, react_1.useRef)(null);
21
- const handleServiceClick = (event) => {
23
+ const { id, description, icon } = service;
24
+ const handleServiceClick = (0, react_1.useCallback)((event) => {
22
25
  (0, utils_1.stopEventPropagation)(event);
23
26
  onServiceClick?.(service, event);
24
- };
25
- const titleTestId = `${constants_1.TEST_IDS.subcategoryTitle}-${service.id}`;
26
- const isFavorite = Boolean(favorite?.value.includes(service.id));
27
- const showDescription = showDescriptionProp && Boolean(service.description);
28
- const hasTooltip = !showDescription && service.description;
29
- const handleKeyDown = (0, uikit_product_card_predefined_1.createCardActionsKeyDownHandler)({
27
+ }, [onServiceClick, service]);
28
+ const titleTestId = `${constants_1.TEST_IDS.subcategoryTitle}-${id}`;
29
+ const cardFavoriteChange = groupFavoritesEnabled && favoriteIds ? onFavoriteChange : undefined;
30
+ const isFavorite = Boolean(favoriteIds?.has(id));
31
+ const showDescription = showDescriptionProp && Boolean(description);
32
+ const hasTooltip = !showDescription && Boolean(description);
33
+ const handleKeyDown = (0, react_1.useMemo)(() => (0, uikit_product_card_predefined_1.createCardActionsKeyDownHandler)({
30
34
  cardRef: titleRef,
31
35
  items: [
32
36
  hasTooltip ? { ref: tooltipTriggerRef } : null,
33
- favorite
37
+ cardFavoriteChange
34
38
  ? {
35
39
  ref: favoriteRef,
36
- onActivate: () => favorite.onChange(service.id)(!isFavorite),
40
+ onActivate: () => cardFavoriteChange(id)(!isFavorite),
37
41
  }
38
42
  : null,
39
43
  ],
40
- });
44
+ }), [hasTooltip, cardFavoriteChange, id, isFavorite]);
45
+ const emblem = (0, react_1.useMemo)(() => (0, utils_2.getLinkEmblem)({ icon }), [icon]);
41
46
  if (expandable?.value) {
42
- return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.subcategoryTitle, "data-mobile": isMobile || undefined, children: [(0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.subcategoryTitleRow, onKeyDown: handleKeyDown, children: [(0, jsx_runtime_1.jsx)(uikit_product_title_clickable_1.TitleClickable, { as: 'a', href: service.href, onClick: handleServiceClick, innerRef: titleRef, title: service.label, icon: (0, utils_2.getLinkEmblem)(service), className: styles_module_scss_1.default.subcategoryTitleClickable, "data-test-id": titleTestId }), (0, jsx_runtime_1.jsx)(uikit_product_card_predefined_1.CardActionsSurface, { className: styles_module_scss_1.default.subcategoryTitleActions, actionsVisibility: 'always', actionsSize: isMobile ? 's' : 'm', tooltip: hasTooltip
47
+ return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.subcategoryTitle, "data-mobile": isMobile || undefined, children: [(0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.subcategoryTitleRow, onKeyDown: handleKeyDown, children: [(0, jsx_runtime_1.jsx)(uikit_product_title_clickable_1.TitleClickable, { as: 'a', href: service.href, onClick: handleServiceClick, innerRef: titleRef, title: service.label, icon: emblem, className: styles_module_scss_1.default.subcategoryTitleClickable, "data-test-id": titleTestId }), (0, jsx_runtime_1.jsx)(uikit_product_card_predefined_1.CardActionsSurface, { className: styles_module_scss_1.default.subcategoryTitleActions, actionsVisibility: 'always', actionsSize: isMobile ? 's' : 'm', tooltip: hasTooltip
43
48
  ? {
44
- tip: service.description,
49
+ tip: description,
45
50
  buttonRef: tooltipTriggerRef,
46
51
  trigger: isMobile ? 'click' : 'hoverAndFocusVisible',
47
52
  'data-test-id': `${titleTestId}-tooltip`,
48
53
  }
49
- : undefined, favorite: favorite
54
+ : undefined, favorite: cardFavoriteChange
50
55
  ? {
51
56
  enabled: true,
52
57
  checked: isFavorite,
53
- onChange: favorite.onChange(service.id),
58
+ onChange: (checked) => cardFavoriteChange(id)(checked),
54
59
  buttonRef: favoriteRef,
55
60
  'data-test-id': `${titleTestId}-favorite`,
56
61
  }
57
- : undefined })] }), showDescription && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { as: 'p', variant: 'body', size: 's', className: styles_module_scss_1.default.subcategoryDescription, "data-test-id": `${titleTestId}-description`, children: service.description }))] }));
62
+ : undefined })] }), showDescription && ((0, jsx_runtime_1.jsx)(typography_1.Typography, { as: 'p', variant: 'body', size: 's', className: styles_module_scss_1.default.subcategoryDescription, "data-test-id": `${titleTestId}-description`, children: description }))] }));
58
63
  }
59
64
  return (
60
65
  // Карточка — ссылка `<a>`: клик по ней бы долетал до `onClick={toggleOpen}` заголовка
61
66
  // родительского CollapseBlockTertiary (см. TODO там) и лишний раз переключал аккордеон.
62
67
  // eslint-disable-next-line jsx-a11y/no-static-element-interactions
63
- (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.subcategoryTitleDraggable, onPointerDown: utils_1.stopEventPropagation, onClick: utils_1.stopEventPropagation, children: (0, jsx_runtime_1.jsx)(ServiceCard_1.DraggableServiceCard, { groupId: groupId, service: service, favorite: favorite, isMobile: isMobile, onServiceClick: onServiceClick, showDescription: showDescriptionProp, dragDisabled: dragDisabled, expandable: expandable }) }));
68
+ (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.subcategoryTitleDraggable, onPointerDown: utils_1.stopEventPropagation, onClick: utils_1.stopEventPropagation, children: (0, jsx_runtime_1.jsx)(ServiceCard_1.GridServiceCard, { groupId: groupId, service: service, isFavorite: isFavorite, onFavoriteChange: cardFavoriteChange, isMobile: isMobile, onServiceClick: onServiceClick, showDescription: showDescriptionProp, dragEnabled: dragEnabled, expandable: expandable }) }));
64
69
  }
70
+ exports.SubCategoryTitle = (0, react_1.memo)(SubCategoryTitleBase);
@@ -46,6 +46,8 @@
46
46
  }
47
47
 
48
48
  .group{
49
+ content-visibility:auto;
50
+ contain-intrinsic-size:auto var(--sn-primitive-dimension-128, 128px);
49
51
  position:relative;
50
52
  overflow-x:clip;
51
53
  display:flex;
@@ -56,6 +58,14 @@
56
58
  border-radius:var(--sn-density-radius-2xs, var(--sn-primitive-dimension-4, 4px));
57
59
  background-color:var(--sn-theme-color-neutral-background1Level, var(--sn-brand-color-neutral-95, var(--sn-primitive-color-gray-95, #fdfdfd)));
58
60
  }
61
+ .group[data-appear]{
62
+ animation:group-appear 0.24s cubic-bezier(0.4, 0, 0.2, 1) backwards;
63
+ }
64
+ @media (prefers-reduced-motion: reduce){
65
+ .group[data-appear]{
66
+ animation:none;
67
+ }
68
+ }
59
69
  .group[data-drag-preview]{
60
70
  cursor:grabbing;
61
71
  }
@@ -233,4 +243,11 @@
233
243
 
234
244
  .skeletonService{
235
245
  padding:var(--sn-primitive-dimension-4, 4px);
246
+ }
247
+
248
+ @keyframes group-appear{
249
+ from{
250
+ transform:translate(calc(-1 * var(--sn-primitive-dimension-12, 12px)), var(--sn-primitive-dimension-4, 4px));
251
+ opacity:0;
252
+ }
236
253
  }