@cloud-ru/ds-uikit-product-header 1.3.9 → 1.3.10-preview-1030ded1.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 (43) hide show
  1. package/README.md +6 -2
  2. package/dist/cjs/components/MainMenu/helperComponents/Banners/MenuBanner.d.ts +1 -1
  3. package/dist/cjs/components/MainMenu/helperComponents/Banners/MenuBanner.js +3 -2
  4. package/dist/cjs/components/MainMenu/helperComponents/Banners/styles.module.css +11 -15
  5. package/dist/cjs/components/MainMenu/helperComponents/Content/Content.js +0 -5
  6. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupCards.js +1 -1
  7. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategory.js +16 -2
  8. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/styles.module.css +7 -1
  9. package/dist/cjs/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/styles.module.css +3 -0
  10. package/dist/cjs/components/MainMenu/helperComponents/Favorites/styles.module.css +2 -2
  11. package/dist/cjs/components/MainMenu/helperComponents/ServiceCard/ServiceCard.js +0 -3
  12. package/dist/cjs/components/MainMenu/types.d.ts +14 -3
  13. package/dist/cjs/components/MainMenu/utils/innerLink.d.ts +10 -0
  14. package/dist/cjs/components/MainMenu/utils/innerLink.js +20 -0
  15. package/dist/cjs/style.css +23 -18
  16. package/dist/esm/components/MainMenu/helperComponents/Banners/MenuBanner.d.ts +1 -1
  17. package/dist/esm/components/MainMenu/helperComponents/Banners/MenuBanner.js +3 -2
  18. package/dist/esm/components/MainMenu/helperComponents/Banners/styles.module.css +11 -15
  19. package/dist/esm/components/MainMenu/helperComponents/Content/Content.js +1 -6
  20. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupCards.js +2 -2
  21. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategory.js +17 -3
  22. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/styles.module.css +7 -1
  23. package/dist/esm/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/styles.module.css +3 -0
  24. package/dist/esm/components/MainMenu/helperComponents/Favorites/styles.module.css +2 -2
  25. package/dist/esm/components/MainMenu/helperComponents/ServiceCard/ServiceCard.js +0 -3
  26. package/dist/esm/components/MainMenu/types.d.ts +14 -3
  27. package/dist/esm/components/MainMenu/utils/innerLink.d.ts +10 -0
  28. package/dist/esm/components/MainMenu/utils/innerLink.js +19 -0
  29. package/dist/esm/style.css +23 -18
  30. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  31. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  32. package/package.json +28 -27
  33. package/src/components/MainMenu/helperComponents/Banners/MenuBanner.tsx +44 -42
  34. package/src/components/MainMenu/helperComponents/Banners/styles.module.scss +15 -22
  35. package/src/components/MainMenu/helperComponents/Content/Content.tsx +1 -7
  36. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SortableGroupCards.tsx +2 -2
  37. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/SubCategory.tsx +61 -2
  38. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/components/SubCategory/styles.module.scss +10 -1
  39. package/src/components/MainMenu/helperComponents/Content/helperComponents/SortableGroup/styles.module.scss +5 -0
  40. package/src/components/MainMenu/helperComponents/Favorites/styles.module.scss +3 -2
  41. package/src/components/MainMenu/helperComponents/ServiceCard/ServiceCard.tsx +0 -4
  42. package/src/components/MainMenu/types.ts +15 -3
  43. package/src/components/MainMenu/utils/innerLink.ts +23 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-ru/ds-uikit-product-header",
3
- "version": "1.3.9",
3
+ "version": "1.3.10-preview-1030ded1.0",
4
4
  "description": "Продуктовая шапка консоли — HeaderLayout, MainMenu, UserMenu, Logo, PathBreadcrumbs и PlatformLogo.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,32 +45,33 @@
45
45
  "@dnd-kit/utilities": "^3.2.2",
46
46
  "classnames": "^2.5.1",
47
47
  "fuse.js": "^7.1.0",
48
- "@cloud-ru/ds-accordion": "^1.0.6",
49
- "@cloud-ru/ds-adaptive": "^1.0.1",
50
- "@cloud-ru/ds-avatar": "^1.0.3",
51
- "@cloud-ru/ds-breadcrumbs": "^1.0.15",
52
- "@cloud-ru/ds-button": "^1.0.2",
53
- "@cloud-ru/ds-drag-and-drop": "^1.1.1",
54
- "@cloud-ru/ds-drawer": "^1.2.0",
55
- "@cloud-ru/ds-divider": "^1.0.1",
56
- "@cloud-ru/ds-info-block": "^1.0.1",
57
- "@cloud-ru/ds-icons": "^1.1.0",
58
- "@cloud-ru/ds-list": "^2.1.4",
59
- "@cloud-ru/ds-locale": "^1.0.0",
60
- "@cloud-ru/ds-modal": "^1.1.1",
61
- "@cloud-ru/ds-portal-context": "^1.0.1",
62
- "@cloud-ru/ds-promo-tag": "^1.0.1",
63
- "@cloud-ru/ds-search": "^1.0.7",
64
- "@cloud-ru/ds-scroll": "^1.0.2",
65
- "@cloud-ru/ds-segment-control": "^1.0.5",
66
- "@cloud-ru/ds-skeleton": "^1.0.1",
67
- "@cloud-ru/ds-theme": "^1.1.0",
68
- "@cloud-ru/ds-tooltip": "^1.0.4",
69
- "@cloud-ru/ds-typography": "^1.0.0",
70
- "@cloud-ru/ds-uikit-product-card-predefined": "^3.0.9",
71
- "@cloud-ru/ds-uikit-product-switch-row": "^1.0.6",
72
- "@cloud-ru/ds-uikit-product-title-clickable": "^1.0.8",
73
- "@cloud-ru/ds-utils": "^1.1.0"
48
+ "@cloud-ru/ds-accordion": "1.0.6",
49
+ "@cloud-ru/ds-adaptive": "1.0.1",
50
+ "@cloud-ru/ds-avatar": "1.0.3",
51
+ "@cloud-ru/ds-card": "1.0.12",
52
+ "@cloud-ru/ds-button": "1.0.2",
53
+ "@cloud-ru/ds-breadcrumbs": "1.0.15",
54
+ "@cloud-ru/ds-divider": "1.0.1",
55
+ "@cloud-ru/ds-drag-and-drop": "1.1.1",
56
+ "@cloud-ru/ds-drawer": "1.2.0",
57
+ "@cloud-ru/ds-icons": "1.1.0",
58
+ "@cloud-ru/ds-list": "2.1.4",
59
+ "@cloud-ru/ds-info-block": "1.0.1",
60
+ "@cloud-ru/ds-locale": "1.0.0",
61
+ "@cloud-ru/ds-modal": "1.1.1",
62
+ "@cloud-ru/ds-portal-context": "1.0.1",
63
+ "@cloud-ru/ds-promo-tag": "1.0.1",
64
+ "@cloud-ru/ds-scroll": "1.0.2",
65
+ "@cloud-ru/ds-search": "1.0.7",
66
+ "@cloud-ru/ds-segment-control": "1.0.5",
67
+ "@cloud-ru/ds-skeleton": "1.0.1",
68
+ "@cloud-ru/ds-theme": "1.1.0",
69
+ "@cloud-ru/ds-tooltip": "1.0.4",
70
+ "@cloud-ru/ds-typography": "1.0.0",
71
+ "@cloud-ru/ds-uikit-product-card-predefined": "3.0.10-preview-1030ded1.0",
72
+ "@cloud-ru/ds-utils": "1.1.0",
73
+ "@cloud-ru/ds-uikit-product-title-clickable": "1.0.8",
74
+ "@cloud-ru/ds-uikit-product-switch-row": "1.0.6"
74
75
  },
75
76
  "publishConfig": {
76
77
  "access": "public"
@@ -1,4 +1,5 @@
1
1
  import { Button } from '@cloud-ru/ds-button';
2
+ import { Card } from '@cloud-ru/ds-card';
2
3
  import { CrossSVG } from '@cloud-ru/ds-icons/interface/system';
3
4
  import { PromoTag, PromoTagProps, ROLE_APPEARANCE, SIZE } from '@cloud-ru/ds-promo-tag';
4
5
  import { useThemeClassnames } from '@cloud-ru/ds-theme';
@@ -33,7 +34,6 @@ export function MenuBanner({
33
34
  onClose,
34
35
  className,
35
36
  href = '#',
36
- target,
37
37
  'data-test-id': dataTestId,
38
38
  ...rest
39
39
  }: MenuBannerProps): ReactElement {
@@ -50,54 +50,56 @@ export function MenuBanner({
50
50
  );
51
51
 
52
52
  return (
53
- <a
53
+ <Card
54
+ as='a'
54
55
  href={href}
55
- target={target}
56
- rel={target === '_blank' ? 'noopener noreferrer' : undefined}
56
+ radius='l'
57
+ view='simple'
57
58
  className={cn(styles.root, className)}
58
59
  data-test-id={dataTestId ?? MENU_BANNER_TEST_IDS.root}
59
60
  {...rest}
60
61
  >
61
- <span className={styles.titleWrapper}>
62
- <Typography
63
- as='span'
64
- variant='body'
65
- size='m'
66
- className={styles.title}
67
- data-test-id={MENU_BANNER_TEST_IDS.title}
68
- >
69
- {title}
70
- </Typography>
71
-
72
- {promoTag && (
73
- <PromoTag
74
- {...promoTag}
62
+ <div className={styles.contentRow}>
63
+ <span className={styles.titleWrapper}>
64
+ <Typography
75
65
  as='span'
76
- role={ROLE_APPEARANCE.Decor}
77
- size={SIZE.Xs}
78
- data-test-id={MENU_BANNER_TEST_IDS.promoTag}
79
- />
80
- )}
81
- </span>
66
+ variant='body'
67
+ size='m'
68
+ className={styles.title}
69
+ data-test-id={MENU_BANNER_TEST_IDS.title}
70
+ >
71
+ {title}
72
+ </Typography>
82
73
 
83
- {afterTitle && (
84
- <div className={styles.afterTitle} data-test-id={MENU_BANNER_TEST_IDS.afterTitle}>
85
- {afterTitle}
86
- </div>
87
- )}
74
+ {promoTag && (
75
+ <PromoTag
76
+ {...promoTag}
77
+ roleAppearance={ROLE_APPEARANCE.Decor}
78
+ size={SIZE.Xs}
79
+ data-test-id={MENU_BANNER_TEST_IDS.promoTag}
80
+ />
81
+ )}
82
+ </span>
88
83
 
89
- {onClose && (
90
- <Button
91
- className={cn(styles.closeButton, compactThemeClassname)}
92
- view='elevated'
93
- appearance='neutral'
94
- size='s'
95
- icon={<CrossSVG size={16} />}
96
- onClick={handleClose}
97
- aria-label={t('close')}
98
- data-test-id={MENU_BANNER_TEST_IDS.close}
99
- />
100
- )}
101
- </a>
84
+ {afterTitle && (
85
+ <div className={styles.afterTitle} data-test-id={MENU_BANNER_TEST_IDS.afterTitle}>
86
+ {afterTitle}
87
+ </div>
88
+ )}
89
+
90
+ {onClose && (
91
+ <Button
92
+ className={cn(styles.closeButton, compactThemeClassname)}
93
+ view='elevated'
94
+ appearance='neutral'
95
+ size='s'
96
+ icon={<CrossSVG size={16} />}
97
+ onClick={handleClose}
98
+ aria-label={t('close')}
99
+ data-test-id={MENU_BANNER_TEST_IDS.close}
100
+ />
101
+ )}
102
+ </div>
103
+ </Card>
102
104
  );
103
105
  }
@@ -11,38 +11,31 @@
11
11
  }
12
12
 
13
13
  .root {
14
- position: relative;
15
-
16
- display: flex;
17
- gap: base.$sn-primitive-dimension-4;
18
- align-items: center;
19
-
20
- box-sizing: border-box;
21
14
  width: 100%;
22
15
  min-width: 0;
23
- min-height: base.$sn-primitive-dimension-48;
24
- padding: 0 base.$sn-primitive-dimension-12;
25
- border-radius: base.$sn-density-radius-2xs;
26
-
27
- font: inherit;
28
- color: inherit;
29
- text-align: left;
30
- text-decoration: none;
31
-
32
- background-color: base.$sn-theme-color-neutral-background1Level;
33
16
 
34
17
  &:hover,
35
18
  &:focus-visible,
36
- &:focus-within {
19
+ &:has(*:focus-visible) {
37
20
  .closeButton {
38
21
  visibility: visible;
39
22
  }
40
23
  }
24
+ }
41
25
 
42
- &:focus-visible {
43
- outline: base.$sn-theme-color-available-complementary solid base.$sn-primitive-strokeWeight-strokeSemiBold;
44
- outline-offset: base.$sn-primitive-strokeWeight-strokeSemiBold;
45
- }
26
+ .contentRow {
27
+ position: relative;
28
+
29
+ display: flex;
30
+ gap: base.$sn-density-spacing-interval-s;
31
+ align-items: center;
32
+
33
+ box-sizing: border-box;
34
+ width: 100%;
35
+ min-width: 0;
36
+ min-height: base.$sn-primitive-dimension-40;
37
+ padding: 0 base.$sn-primitive-dimension-12;
38
+ border-radius: inherit;
46
39
  }
47
40
 
48
41
  .titleWrapper {
@@ -4,7 +4,7 @@ import { CrossSVG, SearchSVG } from '@cloud-ru/ds-icons/interface/system';
4
4
  import { InfoBlock } from '@cloud-ru/ds-info-block';
5
5
  import { useValueControl } from '@cloud-ru/ds-utils';
6
6
  import cn from 'classnames';
7
- import { MouseEvent, ReactNode, useCallback, useDeferredValue, useEffect, useMemo } from 'react';
7
+ import { MouseEvent, ReactNode, useCallback, useDeferredValue, useMemo } from 'react';
8
8
 
9
9
  import { headerLocale } from '../../../../locale';
10
10
  import { shouldBeOpenedInNewTab } from '../../../../utils/shouldBeOpenedInNewTab';
@@ -133,12 +133,6 @@ export function Content({
133
133
  const isSearching = Boolean(searchValue);
134
134
  const enableServiceDrag = Boolean(favorite) && !isMobile && !isSearching;
135
135
 
136
- useEffect(() => {
137
- if (!segments?.some(segment => segment.id === segmentId) && defaultSegmentId) {
138
- setSegmentId(defaultSegmentId);
139
- }
140
- }, [defaultSegmentId, segmentId, setSegmentId, segments]);
141
-
142
136
  const handleLinkClick = useCallback(
143
137
  (service: InnerLink, e?: MouseEvent<HTMLElement>) => {
144
138
  if (service.disabled) {
@@ -1,5 +1,5 @@
1
1
  import { InnerLink } from '../../../../../types';
2
- import { hasNestedItems } from '../../../../../utils/innerLink';
2
+ import { isSubCategoryCard } from '../../../../../utils/innerLink';
3
3
  import { DraggableServiceCard, ServiceCardProps } from '../../../../ServiceCard';
4
4
  import styles from '../styles.module.scss';
5
5
  import { SubCategory } from './SubCategory';
@@ -33,7 +33,7 @@ export function SortableGroupCards({
33
33
 
34
34
  const key = String(groupId) + service.id;
35
35
 
36
- if (hasNestedItems(service)) {
36
+ if (isSubCategoryCard(service)) {
37
37
  return (
38
38
  <SubCategory
39
39
  key={key}
@@ -8,6 +8,8 @@ import baseGroupStyles from '../../styles.module.scss';
8
8
  import styles from './styles.module.scss';
9
9
  import { SubCategoryTitle } from './SubCategoryTitle';
10
10
 
11
+ const NOOP = () => {};
12
+
11
13
  export type SubCategoryProps = Pick<
12
14
  ServiceCardProps,
13
15
  'service' | 'onServiceClick' | 'favorite' | 'showDescription' | 'isMobile'
@@ -30,11 +32,14 @@ export function SubCategory({
30
32
  const subcategoryId = getSubCategoryId(groupId, service.id);
31
33
  const nestedGroupId = getNestedServiceGroupId(groupId, service.id);
32
34
  const nestedItems = useMemo(() => service.items?.filter(item => !item.hidden) ?? [], [service.items]);
35
+ const hasItems = nestedItems.length > 0;
36
+
37
+ const viewMode = service.viewMode ?? 'expanded';
33
38
 
34
- const isExpandableEnabled = service.expandableEnabled ?? false;
39
+ const isExpandableEnabled = hasItems && viewMode === 'expandable';
35
40
 
36
41
  // Раскрытие подкатегории — локальное uncontrolled состояние: наружу его пока не выводим.
37
- const [isExpanded, setIsExpanded] = useState<string | undefined>(subcategoryId);
42
+ const [isExpanded, setIsExpanded] = useState<string | undefined>(hasItems ? subcategoryId : undefined);
38
43
 
39
44
  const handleExpandedChange = useCallback(() => {
40
45
  if (isExpandableEnabled) {
@@ -42,6 +47,60 @@ export function SubCategory({
42
47
  }
43
48
  }, [isExpanded, subcategoryId, isExpandableEnabled]);
44
49
 
50
+ // 'group-title-only' — только заголовок подкатегории, без раскрываемого тела и кнопки
51
+ // переключения: карточка ведёт себя как обычная ссылка (см. isSubCategoryCard в utils/innerLink).
52
+ if (viewMode === 'group-title-only') {
53
+ return (
54
+ <div className={styles.subcategory} data-title-only data-test-id={`${TEST_IDS.subcategory}-${service.id}`}>
55
+ <SubCategoryTitle
56
+ groupId={groupId}
57
+ service={service}
58
+ expandable={{ value: true, onClick: NOOP }}
59
+ showDescription={showDescription}
60
+ isMobile={isMobile}
61
+ dragDisabled={dragDisabled}
62
+ favorite={groupFavoritesEnabled ? favorite : undefined}
63
+ onServiceClick={onServiceClick}
64
+ />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ if (viewMode === 'expanded') {
70
+ return (
71
+ <div className={styles.subcategory} data-test-id={`${TEST_IDS.subcategory}-${service.id}`}>
72
+ <SubCategoryTitle
73
+ groupId={groupId}
74
+ service={service}
75
+ expandable={{
76
+ value: Boolean(isExpanded),
77
+ onClick: handleExpandedChange,
78
+ }}
79
+ showDescription={showDescription}
80
+ isMobile={isMobile}
81
+ dragDisabled={dragDisabled}
82
+ favorite={groupFavoritesEnabled ? favorite : undefined}
83
+ onServiceClick={onServiceClick}
84
+ />
85
+
86
+ <div className={baseGroupStyles.groupBody} data-subcategory-body='expanded' data-mobile={isMobile || undefined}>
87
+ {nestedItems.map(nestedService => (
88
+ <DraggableServiceCard
89
+ key={nestedGroupId + nestedService.id}
90
+ groupId={nestedGroupId}
91
+ service={nestedService}
92
+ favorite={favorite}
93
+ isMobile={isMobile}
94
+ onServiceClick={onServiceClick}
95
+ showDescription={showDescription}
96
+ dragDisabled={dragDisabled}
97
+ />
98
+ ))}
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+
45
104
  return (
46
105
  <div className={styles.subcategory} data-test-id={`${TEST_IDS.subcategory}-${service.id}`}>
47
106
  <Accordion selectionMode='single' expanded={isExpanded} onExpandedChange={handleExpandedChange}>
@@ -13,12 +13,21 @@
13
13
  .subcategory {
14
14
  min-width: 0;
15
15
 
16
- &:has([data-subcategory-body]) {
16
+ &:has([data-subcategory-body]),
17
+ &[data-title-only] {
17
18
  grid-column: 1 / -1;
18
19
  padding: 0 base.$sn-primitive-dimension-4;
19
20
  border-radius: base.$sn-density-radius-2xs;
20
21
  background-color: base.$sn-theme-color-neutral-decorTransparent;
21
22
  }
23
+
24
+ &[data-title-only] {
25
+ padding: base.$sn-primitive-dimension-4;
26
+ }
27
+
28
+ &:has([data-subcategory-body='expanded']) {
29
+ padding: base.$sn-primitive-dimension-4 base.$sn-primitive-dimension-4 0;
30
+ }
22
31
  }
23
32
 
24
33
  .subcategoryAccordion {
@@ -1,5 +1,6 @@
1
1
  @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
2
  @use '@cloud-ru/ds-figma-variables/build/scss/components/button.module' as button;
3
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/accordion.module' as accordion;
3
4
 
4
5
  // Радиус контейнера группы: от него считается радиус подложки внутри рамки выделения.
5
6
  $group-border-radius: base.$sn-density-radius-2xs;
@@ -173,6 +174,10 @@ $map-block-color-decor: (
173
174
  &[data-subcategory-body] {
174
175
  padding-bottom: base.$sn-primitive-dimension-4;
175
176
  }
177
+
178
+ &[data-subcategory-body='expanded'] {
179
+ padding-top: accordion.$sn-accordion-anatomy-accordionTertiary-container-bodyContent-paddingVertical;
180
+ }
176
181
  }
177
182
 
178
183
  .decoration {
@@ -9,7 +9,8 @@
9
9
 
10
10
  box-sizing: border-box;
11
11
  width: 100%;
12
- min-height: 0;
12
+ // высота всего блока с одной карточкой
13
+ min-height: base.$sn-primitive-dimension-88;
13
14
  padding: base.$sn-primitive-dimension-8 0 0;
14
15
  border-radius: base.$sn-density-radius-2xs;
15
16
 
@@ -40,7 +41,7 @@
40
41
  position: relative;
41
42
  flex: 1 1 auto;
42
43
  width: 100%;
43
- min-height: 0;
44
+ min-height: base.$sn-primitive-dimension-44;
44
45
 
45
46
  &[data-insert-new] {
46
47
  [data-favorite] {
@@ -94,10 +94,6 @@ export function DraggableServiceCard({
94
94
 
95
95
  const { tabIndex, ...restAttributes } = attributes ?? {};
96
96
 
97
- if (disabled) {
98
- return <ServiceCard service={service} favorite={favorite} {...props} tabIndex={tabIndex} />;
99
- }
100
-
101
97
  return (
102
98
  <DragGhost
103
99
  innerRef={setNodeRef}
@@ -6,6 +6,8 @@ import { JSXElementConstructor, MouseEvent, ReactNode } from 'react';
6
6
  import { ContentProps } from './helperComponents/Content';
7
7
  import { FavoritesSegment } from './helperComponents/Favorites/constants';
8
8
 
9
+ export type InnerLinkViewMode = 'group-title-only' | 'flat-link' | 'expanded' | 'expandable';
10
+
9
11
  /**
10
12
  * Карточка сервиса или ссылки внутри {@link LinksGroup}.
11
13
  *
@@ -38,15 +40,25 @@ export type InnerLink = {
38
40
  badge?: CardServiceLightProps['promoTag'];
39
41
  /** Разрешено ли добавление карточки в избранное. default=true - разрешено */
40
42
  favoritesEnabled?: boolean;
41
- /** Разрешено ли раскрытие карточки. default=false - Запрещено */
42
- expandableEnabled?: boolean;
43
+ /**
44
+ * Режим отображения карточки. default='expandable'.
45
+ *
46
+ * - `'expandable'` — подкатегория при наличии реальных {@link items}: заголовок
47
+ * {@link TitleClickable} с шевроном и сетка вложенных сервисов (раскрытие/схлопывание —
48
+ * см. {@link expandableEnabled}). Без `items` — обычная карточка сервиса.
49
+ * - `'group-title-only'` — заголовок подкатегории ({@link TitleClickable} с шевроном) без
50
+ * раскрываемого тела, даже если `items` заданы: карточка ведёт себя как обычная ссылка.
51
+ * Полезно для группы, чьи вложенные сервисы показываются на отдельной странице.
52
+ * - `'flat-link'` — всегда обычная карточка сервиса, даже если `items` заданы.
53
+ */
54
+ viewMode?: InnerLinkViewMode;
43
55
  /**
44
56
  * Вложенные сервисы подкатегории.
45
57
  *
46
58
  * При наличии карточка раскрывается аккордеоном: в свёрнутом виде — обычная карточка
47
59
  * с кнопкой раскрытия, в развёрнутом — заголовок {@link TitleClickable} и сетка вложенных сервисов.
48
60
  */
49
- items?: InnerLink[];
61
+ items?: Omit<InnerLink, 'items' | 'viewMode' | 'expandableEnabled'>[];
50
62
  /**
51
63
  * Синонимы для fuzzy-поиска.
52
64
  */
@@ -4,6 +4,29 @@ export function hasNestedItems(item: InnerLink): boolean {
4
4
  return Boolean(item.items?.some(child => !child.hidden));
5
5
  }
6
6
 
7
+ /**
8
+ * Рендерится ли карточка как подкатегория (заголовок {@link TitleClickable} + вложенная сетка).
9
+ *
10
+ * Решение зависит от `viewMode` (не указан — считается `'expandable'`):
11
+ * - `'flat-link'` — никогда, карточка всегда обычная ссылка, даже если `items` заданы.
12
+ * - `'group-title-only'` — всегда, даже без `items`: только заголовок, без раскрываемого тела
13
+ * и кнопки переключения — карточка ведёт себя как обычная ссылка.
14
+ * - `'expandable'` — только при наличии реальных вложенных сервисов.
15
+ */
16
+ export function isSubCategoryCard(item: InnerLink): boolean {
17
+ const viewMode = item.viewMode ?? 'expanded';
18
+
19
+ if (viewMode === 'flat-link') {
20
+ return false;
21
+ }
22
+
23
+ if (viewMode === 'group-title-only') {
24
+ return true;
25
+ }
26
+
27
+ return hasNestedItems(item);
28
+ }
29
+
7
30
  export function flatInnerLinks(items: InnerLink[] = []): InnerLink[] {
8
31
  return items.flatMap(item => {
9
32
  if (item.hidden) {