@cloud-ru/ds-uikit-product-header 2.1.7 → 2.1.8-preview-04e34550.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 (79) hide show
  1. package/dist/cjs/components/ListCustom/helperComponents/ContentWrapperCustom/ContentWrapperCustom.d.ts +33 -0
  2. package/dist/cjs/components/ListCustom/helperComponents/ContentWrapperCustom/ContentWrapperCustom.js +24 -0
  3. package/dist/cjs/components/ListCustom/helperComponents/ContentWrapperCustom/index.d.ts +1 -0
  4. package/dist/cjs/components/ListCustom/helperComponents/ContentWrapperCustom/index.js +17 -0
  5. package/dist/cjs/components/ListCustom/helperComponents/ContentWrapperCustom/styles.module.css +168 -0
  6. package/dist/cjs/components/ListCustom/helperComponents/ListItemCustom/ListItemCustom.d.ts +6 -0
  7. package/dist/cjs/components/ListCustom/helperComponents/ListItemCustom/ListItemCustom.js +42 -0
  8. package/dist/cjs/components/ListCustom/helperComponents/ListItemCustom/constants.d.ts +6 -0
  9. package/dist/cjs/components/ListCustom/helperComponents/ListItemCustom/constants.js +6 -0
  10. package/dist/cjs/components/ListCustom/helperComponents/ListItemCustom/index.d.ts +3 -0
  11. package/dist/cjs/components/ListCustom/helperComponents/ListItemCustom/index.js +19 -0
  12. package/dist/cjs/components/ListCustom/helperComponents/ListItemCustom/styles.module.css +236 -0
  13. package/dist/cjs/components/ListCustom/helperComponents/ListItemCustom/types.d.ts +13 -0
  14. package/dist/cjs/components/ListCustom/helperComponents/ListItemCustom/types.js +2 -0
  15. package/dist/cjs/components/ListCustom/helperComponents/ListItemGroupCustom/ListItemGroupCustom.d.ts +7 -0
  16. package/dist/cjs/components/ListCustom/helperComponents/ListItemGroupCustom/ListItemGroupCustom.js +23 -0
  17. package/dist/cjs/components/ListCustom/helperComponents/ListItemGroupCustom/index.d.ts +1 -0
  18. package/dist/cjs/components/ListCustom/helperComponents/ListItemGroupCustom/index.js +17 -0
  19. package/dist/cjs/components/ListCustom/helperComponents/ListItemGroupCustom/styles.module.css +34 -0
  20. package/dist/cjs/components/ListCustom/index.d.ts +3 -0
  21. package/dist/cjs/components/ListCustom/index.js +19 -0
  22. package/dist/cjs/components/ListCustom/renderListCustom.d.ts +4 -0
  23. package/dist/cjs/components/ListCustom/renderListCustom.js +14 -0
  24. package/dist/cjs/components/MainMenu/helperComponents/Favorites/Favorites.js +2 -3
  25. package/dist/cjs/components/UserMenu/UserMenu.js +2 -4
  26. package/dist/cjs/components/UserMenu/hooks/useThemeItem.js +1 -4
  27. package/dist/cjs/components/index.d.ts +1 -0
  28. package/dist/cjs/components/index.js +1 -0
  29. package/dist/cjs/style.css +438 -0
  30. package/dist/esm/components/ListCustom/helperComponents/ContentWrapperCustom/ContentWrapperCustom.d.ts +33 -0
  31. package/dist/esm/components/ListCustom/helperComponents/ContentWrapperCustom/ContentWrapperCustom.js +18 -0
  32. package/dist/esm/components/ListCustom/helperComponents/ContentWrapperCustom/index.d.ts +1 -0
  33. package/dist/esm/components/ListCustom/helperComponents/ContentWrapperCustom/index.js +1 -0
  34. package/dist/esm/components/ListCustom/helperComponents/ContentWrapperCustom/styles.module.css +168 -0
  35. package/dist/esm/components/ListCustom/helperComponents/ListItemCustom/ListItemCustom.d.ts +6 -0
  36. package/dist/esm/components/ListCustom/helperComponents/ListItemCustom/ListItemCustom.js +36 -0
  37. package/dist/esm/components/ListCustom/helperComponents/ListItemCustom/constants.d.ts +6 -0
  38. package/dist/esm/components/ListCustom/helperComponents/ListItemCustom/constants.js +3 -0
  39. package/dist/esm/components/ListCustom/helperComponents/ListItemCustom/index.d.ts +3 -0
  40. package/dist/esm/components/ListCustom/helperComponents/ListItemCustom/index.js +3 -0
  41. package/dist/esm/components/ListCustom/helperComponents/ListItemCustom/styles.module.css +236 -0
  42. package/dist/esm/components/ListCustom/helperComponents/ListItemCustom/types.d.ts +13 -0
  43. package/dist/esm/components/ListCustom/helperComponents/ListItemCustom/types.js +1 -0
  44. package/dist/esm/components/ListCustom/helperComponents/ListItemGroupCustom/ListItemGroupCustom.d.ts +7 -0
  45. package/dist/esm/components/ListCustom/helperComponents/ListItemGroupCustom/ListItemGroupCustom.js +17 -0
  46. package/dist/esm/components/ListCustom/helperComponents/ListItemGroupCustom/index.d.ts +1 -0
  47. package/dist/esm/components/ListCustom/helperComponents/ListItemGroupCustom/index.js +1 -0
  48. package/dist/esm/components/ListCustom/helperComponents/ListItemGroupCustom/styles.module.css +34 -0
  49. package/dist/esm/components/ListCustom/index.d.ts +3 -0
  50. package/dist/esm/components/ListCustom/index.js +3 -0
  51. package/dist/esm/components/ListCustom/renderListCustom.d.ts +4 -0
  52. package/dist/esm/components/ListCustom/renderListCustom.js +10 -0
  53. package/dist/esm/components/MainMenu/helperComponents/Favorites/Favorites.js +2 -3
  54. package/dist/esm/components/UserMenu/UserMenu.js +2 -4
  55. package/dist/esm/components/UserMenu/hooks/useThemeItem.js +1 -4
  56. package/dist/esm/components/index.d.ts +1 -0
  57. package/dist/esm/components/index.js +1 -0
  58. package/dist/esm/style.css +438 -0
  59. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  60. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  61. package/package.json +31 -28
  62. package/src/components/ListCustom/helperComponents/ContentWrapperCustom/ContentWrapperCustom.tsx +106 -0
  63. package/src/components/ListCustom/helperComponents/ContentWrapperCustom/index.ts +1 -0
  64. package/src/components/ListCustom/helperComponents/ContentWrapperCustom/styles.module.scss +176 -0
  65. package/src/components/ListCustom/helperComponents/ListItemCustom/ListItemCustom.tsx +146 -0
  66. package/src/components/ListCustom/helperComponents/ListItemCustom/constants.ts +4 -0
  67. package/src/components/ListCustom/helperComponents/ListItemCustom/index.ts +3 -0
  68. package/src/components/ListCustom/helperComponents/ListItemCustom/styles.module.scss +218 -0
  69. package/src/components/ListCustom/helperComponents/ListItemCustom/types.ts +18 -0
  70. package/src/components/ListCustom/helperComponents/ListItemGroupCustom/ListItemGroupCustom.tsx +56 -0
  71. package/src/components/ListCustom/helperComponents/ListItemGroupCustom/index.ts +1 -0
  72. package/src/components/ListCustom/helperComponents/ListItemGroupCustom/styles.module.scss +52 -0
  73. package/src/components/ListCustom/index.ts +3 -0
  74. package/src/components/ListCustom/renderListCustom.tsx +13 -0
  75. package/src/components/ListCustom/specs/readme.md +246 -0
  76. package/src/components/MainMenu/helperComponents/Favorites/Favorites.tsx +8 -3
  77. package/src/components/UserMenu/UserMenu.tsx +5 -5
  78. package/src/components/UserMenu/hooks/useThemeItem.tsx +1 -4
  79. package/src/components/index.ts +1 -0
@@ -0,0 +1,218 @@
1
+ @use '@cloud-ru/ds-materials' as m;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/list.module' as list;
3
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
4
+
5
+ // Figma listItemCustom (17001:42063): selectionMode × checked × disabled на anatomy size/s.
6
+
7
+ $level: var(--level, 0);
8
+ $itemPaddingHorizontal: list.$sn-list-anatomy-size-s-listItem-paddingHorizontal;
9
+ $itemGap: list.$sn-list-anatomy-size-s-listItem-gap;
10
+ $nestingLevelStep: list.$sn-list-anatomy-size-s-listItem-nestingLevel-paddingLeft;
11
+
12
+ .stateLayer {
13
+ visibility: initial;
14
+ }
15
+
16
+ .listItem {
17
+ cursor: pointer;
18
+
19
+ position: relative;
20
+ z-index: 1;
21
+
22
+ display: flex;
23
+ flex-direction: row;
24
+ gap: $itemGap;
25
+ align-items: flex-start;
26
+
27
+ box-sizing: border-box;
28
+ width: max-content;
29
+ min-width: 100%;
30
+ margin: 0;
31
+ padding: base.$sn-primitive-dimension-0 $itemPaddingHorizontal;
32
+ border: none;
33
+
34
+ color: base.$sn-theme-color-available-version-textMain;
35
+ text-align: start;
36
+
37
+ appearance: unset;
38
+ background-color: transparent;
39
+ outline: none;
40
+
41
+ &[data-level-one],
42
+ &[data-level-more-one] {
43
+ padding-left: calc(#{$itemPaddingHorizontal} + #{$level} * #{$nestingLevelStep} + #{$itemGap});
44
+ }
45
+
46
+ &:not([data-inactive], [data-disabled]) {
47
+ &[data-focused],
48
+ &:focus-visible {
49
+ outline: base.$sn-theme-color-available-complementary solid base.$sn-primitive-strokeWeight-strokeSemiBold;
50
+ outline-offset: calc(0px - #{base.$sn-primitive-strokeWeight-strokeSemiBold});
51
+ }
52
+ }
53
+
54
+ &[data-non-pointer] {
55
+ cursor: inherit;
56
+ }
57
+ }
58
+
59
+ // Figma markerWrapper: absolute left strip, only when selectionMode=single && checked.
60
+ .marker {
61
+ pointer-events: none;
62
+
63
+ position: absolute;
64
+ top: 0;
65
+ bottom: 0;
66
+ left: 0;
67
+
68
+ display: flex;
69
+ align-items: center;
70
+
71
+ box-sizing: border-box;
72
+ width: list.$sn-list-anatomy-size-s-listItem-markerWrapper-width;
73
+ min-width: list.$sn-list-anatomy-size-s-listItem-markerWrapper-width;
74
+ max-width: list.$sn-list-anatomy-size-s-listItem-markerWrapper-width;
75
+ padding-top: list.$sn-list-anatomy-size-s-listItem-markerWrapper-marker-paddingVertical;
76
+ padding-bottom: list.$sn-list-anatomy-size-s-listItem-markerWrapper-marker-paddingVertical;
77
+
78
+ &::before {
79
+ content: '';
80
+
81
+ flex: 1 0 0;
82
+
83
+ width: 100%;
84
+ min-width: base.$sn-primitive-dimension-0;
85
+ height: 100%;
86
+ border-top-right-radius: list.$sn-list-anatomy-size-s-listItem-markerWrapper-borderTopRightRadius;
87
+ border-bottom-right-radius: list.$sn-list-anatomy-size-s-listItem-markerWrapper-borderBottomRightRadius;
88
+
89
+ background-color: base.$sn-theme-color-primary-accent;
90
+ }
91
+
92
+ &[data-disabled] {
93
+ opacity: base.$sn-theme-effect-opacity-disabled;
94
+
95
+ &::before {
96
+ background-color: base.$sn-theme-color-neutral-accent;
97
+ }
98
+ }
99
+ }
100
+
101
+ // Figma centeredWrapper вокруг checkboxS: padding 4.
102
+ .checkbox {
103
+ display: flex;
104
+ flex-shrink: 0;
105
+ align-items: flex-start;
106
+ padding: base.$sn-primitive-dimension-4;
107
+ }
108
+
109
+ // Figma centeredWrapper вокруг groupIndicator (17001:42268): padding 4 4 4 0.
110
+ .expandCenteredWrapper {
111
+ display: flex;
112
+ flex-shrink: 0;
113
+ align-items: center;
114
+ padding: base.$sn-primitive-dimension-4 base.$sn-primitive-dimension-4 base.$sn-primitive-dimension-4
115
+ base.$sn-primitive-dimension-0;
116
+ }
117
+
118
+ // Figma groupIndicator (17001:42269) на size/s: сетка icon-s (24).
119
+ .groupIndicator {
120
+ pointer-events: none;
121
+
122
+ display: flex;
123
+ flex-shrink: 0;
124
+ align-items: center;
125
+ justify-content: center;
126
+
127
+ box-sizing: border-box;
128
+ width: base.$sn-density-icon-s;
129
+ height: base.$sn-density-icon-s;
130
+
131
+ color: base.$sn-theme-color-available-version-textSecondary;
132
+
133
+ fill: currentcolor;
134
+
135
+ svg {
136
+ width: 100%;
137
+ max-width: 100%;
138
+ height: 100%;
139
+ max-height: 100%;
140
+ }
141
+
142
+ &[data-interactive] {
143
+ pointer-events: auto;
144
+ cursor: pointer;
145
+ }
146
+
147
+ &[data-open] {
148
+ border-radius: base.$sn-density-radius-s;
149
+ color: base.$sn-theme-color-available-version-textMain;
150
+ background: base.$sn-theme-color-neutral-decorTransparent;
151
+ }
152
+
153
+ &[data-disabled] {
154
+ opacity: base.$sn-theme-effect-opacity-disabled;
155
+ }
156
+ }
157
+
158
+ .itemWrapper {
159
+ position: relative;
160
+ z-index: 0;
161
+
162
+ min-width: 100%;
163
+ margin: 0;
164
+ padding: 0;
165
+ border-radius: list.$sn-list-anatomy-size-s-listItem-borderRadius;
166
+
167
+ @include m.has-state-layer-as-child(#{stateLayer});
168
+
169
+ &[data-disabled],
170
+ &:disabled {
171
+ cursor: not-allowed;
172
+ background-color: transparent;
173
+ outline: none;
174
+
175
+ .listItem {
176
+ cursor: not-allowed;
177
+ }
178
+ }
179
+
180
+ &:not([data-inactive], [data-disabled]) {
181
+ &[data-open],
182
+ &[data-focused] {
183
+ .stateLayer[data-state='emptyNeutralOnBackground'] {
184
+ background: base.$sn-theme-color-material-stateLayer-neutral-onBackground-hovered-filled;
185
+ }
186
+
187
+ .stateLayer[data-state='activatedOnBackground'] {
188
+ background: base.$sn-theme-color-material-stateLayer-activated-hovered-filled;
189
+ }
190
+ }
191
+
192
+ &:active {
193
+ .stateLayer[data-state='emptyNeutralOnBackground'] {
194
+ background: base.$sn-theme-color-material-stateLayer-neutral-onBackground-pressed-filled;
195
+ }
196
+
197
+ .stateLayer[data-state='activatedOnBackground'] {
198
+ background: base.$sn-theme-color-material-stateLayer-activated-pressed-filled;
199
+ }
200
+ }
201
+ }
202
+
203
+ &[data-inactive] {
204
+ &:hover,
205
+ &:active {
206
+ .stateLayer[data-state='emptyNeutralOnBackground'],
207
+ .stateLayer[data-state='activatedOnBackground'] {
208
+ background: base.$sn-theme-color-material-stateLayer-neutral-onBackground-defaultEmpty-filled;
209
+ }
210
+ }
211
+ }
212
+
213
+ &[data-checked][data-selection-mode='single']:not([data-disabled]) {
214
+ .listItem:focus-visible {
215
+ outline-color: base.$sn-theme-color-primary-accent;
216
+ }
217
+ }
218
+ }
@@ -0,0 +1,18 @@
1
+ import { ItemRenderProps, Mode } from '@cloud-ru/ds-list';
2
+
3
+ export type SelectionMode = Mode;
4
+
5
+ /**
6
+ * Пропы оболочки. Совместимы с `ItemRenderProps` списка (адаптер — spread).
7
+ * Для Playground часть полей опциональна: `contentNode` / `selectionMode` / `size` / `tabIndex` / `level`.
8
+ */
9
+ export type ListItemCustomProps = Omit<
10
+ ItemRenderProps,
11
+ 'contentNode' | 'selectionMode' | 'size' | 'tabIndex' | 'level'
12
+ > & {
13
+ contentNode?: ItemRenderProps['contentNode'];
14
+ selectionMode?: Mode;
15
+ size?: ItemRenderProps['size'];
16
+ tabIndex?: number;
17
+ level?: number;
18
+ };
@@ -0,0 +1,56 @@
1
+ import { Divider, VARIANT } from '@cloud-ru/ds-divider';
2
+ import { GroupItemRenderProps } from '@cloud-ru/ds-list';
3
+ import { CSSProperties } from 'react';
4
+
5
+ import { ContentWrapperCustom } from '../ContentWrapperCustom';
6
+ import styles from './styles.module.scss';
7
+
8
+ export type ListItemGroupCustomProps = GroupItemRenderProps;
9
+
10
+ /**
11
+ * Кастомная оболочка заголовка группы `List`/`Droplist` (Figma `listItemGroupCustom`).
12
+ * Передаётся в `groupItemRender`: subtitle / subtitleTertiary / divider.
13
+ */
14
+ export function ListItemGroupCustom({
15
+ label,
16
+ beforeContent,
17
+ truncate,
18
+ groupVariant = 'subtitleTertiary',
19
+ divider,
20
+ selectButtonNode,
21
+ level,
22
+ }: ListItemGroupCustomProps) {
23
+ if (label) {
24
+ return (
25
+ <div
26
+ className={styles.root}
27
+ data-level-one={level === 1 || undefined}
28
+ data-level-more-one={level > 1 || undefined}
29
+ style={{ '--level': level } as CSSProperties}
30
+ >
31
+ <ContentWrapperCustom
32
+ layout='group'
33
+ beforeContent={beforeContent}
34
+ label={label}
35
+ truncate={truncate ? { variant: truncate.variant, label: 1 } : undefined}
36
+ groupVariant={groupVariant}
37
+ selectButton={selectButtonNode}
38
+ />
39
+
40
+ {divider && (
41
+ <Divider className={styles.divider} variant={groupVariant === 'subtitle' ? VARIANT.Regular : VARIANT.Thin} />
42
+ )}
43
+ </div>
44
+ );
45
+ }
46
+
47
+ if (divider) {
48
+ return (
49
+ <div className={styles.dividerOnly}>
50
+ <Divider variant={VARIANT.Regular} />
51
+ </div>
52
+ );
53
+ }
54
+
55
+ return null;
56
+ }
@@ -0,0 +1 @@
1
+ export * from './ListItemGroupCustom';
@@ -0,0 +1,52 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/list.module' as list;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
3
+
4
+ // Figma listItemGroupCustom (17001:43680): subtitle / subtitleTertiary / divider.
5
+
6
+ $level: var(--level, 0);
7
+ $groupPaddingHorizontal: base.$sn-primitive-dimension-8;
8
+ $itemGap: list.$sn-list-anatomy-size-s-listItem-gap;
9
+ $nestingLevelStep: list.$sn-list-anatomy-size-s-listItem-nestingLevel-paddingLeft;
10
+
11
+ .root {
12
+ position: relative;
13
+
14
+ display: flex;
15
+ gap: $itemGap;
16
+ align-items: center;
17
+
18
+ box-sizing: border-box;
19
+ width: 100%;
20
+ min-width: 100%;
21
+ padding: list.$sn-list-anatomy-size-s-subtitle-paddingTop $groupPaddingHorizontal
22
+ list.$sn-list-anatomy-size-s-listItem-paddingVertical;
23
+
24
+ &:focus-visible {
25
+ outline: base.$sn-theme-color-available-complementary solid base.$sn-primitive-strokeWeight-strokeSemiBold;
26
+ outline-offset: calc(0px - #{base.$sn-primitive-strokeWeight-strokeSemiBold});
27
+ }
28
+
29
+ &[data-level-one],
30
+ &[data-level-more-one] {
31
+ padding-left: calc(#{$groupPaddingHorizontal} + #{$level} * #{$nestingLevelStep} + #{$itemGap});
32
+ }
33
+ }
34
+
35
+ .dividerOnly {
36
+ display: flex;
37
+ align-items: flex-end;
38
+
39
+ box-sizing: border-box;
40
+ width: 100%;
41
+ padding: list.$sn-list-anatomy-size-s-listItem-paddingVertical $groupPaddingHorizontal;
42
+ }
43
+
44
+ .divider {
45
+ position: absolute;
46
+ top: 0;
47
+ right: 0;
48
+ left: 0;
49
+
50
+ width: auto;
51
+ min-width: 0;
52
+ }
@@ -0,0 +1,3 @@
1
+ export * from './helperComponents/ListItemCustom';
2
+ export * from './helperComponents/ListItemGroupCustom';
3
+ export * from './renderListCustom';
@@ -0,0 +1,13 @@
1
+ import { GroupItemRenderProps, ItemRenderProps } from '@cloud-ru/ds-list';
2
+
3
+ import { ListItemCustom } from './helperComponents/ListItemCustom';
4
+ import { ListItemGroupCustom } from './helperComponents/ListItemGroupCustom';
5
+
6
+ /** Стабильные рендеры для `itemRender` / `groupItemRender` у `List`/`Droplist`. */
7
+ export function renderListItemCustom(props: ItemRenderProps) {
8
+ return <ListItemCustom {...props} />;
9
+ }
10
+
11
+ export function renderListItemGroupCustom(props: GroupItemRenderProps) {
12
+ return <ListItemGroupCustom {...props} />;
13
+ }
@@ -0,0 +1,246 @@
1
+ # ListCustom — устройство и логика
2
+
3
+ Спецификация кастомных оболочек строки и заголовка группы для `List` / `Droplist` в хедере. Ось размера мастера — только `s` (Figma `list/anatomy/size/s`).
4
+
5
+ ## Зачем
6
+
7
+ Стандартный `BaseItem` / `Separator` из `@cloud-ru/ds-list` рисуют раскладку списка. Пропы `contentRender` и `itemWrapRender` не позволяют заменить отступы и типографику оболочки строки, не затрагивая остальных потребителей List.
8
+
9
+ В хедере нужна раскладка мастеров `listItemCustom` / `listItemGroupCustom` / `contentWrapperCustom` (файл Figma `VcK2QJzt2cWJiHzTzpKAuZ`). Для этого в List добавлены опциональные слоты:
10
+
11
+ | Проп List | Когда задан | Когда не задан |
12
+ |-----------|-------------|----------------|
13
+ | `itemRender` | вместо JSX строки `BaseItem` | дефолтная разметка `BaseItem` |
14
+ | `groupItemRender` | вместо разметки `Separator` | дефолтный `Separator` |
15
+
16
+ Логика выбора, клавиатуры, вложенных списков и закрытия дроплиста остаётся в List. Оболочки отвечают только за визуальную раскладку и индикаторы выбора (маркер, чекбокс, подложка) по макету хедера.
17
+
18
+ `contentRender` и `itemWrapRender` не меняют сигнатуру и место в дереве: `itemWrapRender` по-прежнему оборачивает результат `itemRender` (например, ссылка профиля в UserMenu).
19
+
20
+ ## Состав модуля
21
+
22
+ ```
23
+ ListCustom/
24
+ ├── index.ts # публичный экспорт
25
+ ├── renderListCustom.tsx # адаптеры ItemRenderProps → оболочки
26
+ ├── specs/readme.md # этот файл
27
+ └── helperComponents/
28
+ ├── ContentWrapperCustom/ # приватный слот before + текст + after
29
+ ├── ListItemCustom/ # публичная оболочка строки
30
+ └── ListItemGroupCustom/ # публичная оболочка группы
31
+ ```
32
+
33
+ **Публичный API пакета:** `ListItemCustom`, `ListItemGroupCustom`, `renderListItemCustom`, `renderListItemGroupCustom`, константа `SELECTION_MODE` / тип `SelectionMode`.
34
+
35
+ `ContentWrapperCustom` наружу не экспортируется.
36
+
37
+ ## Поток данных
38
+
39
+ ```mermaid
40
+ flowchart TD
41
+ Droplist["List / Droplist"] --> BaseItem
42
+ Droplist --> Separator
43
+ BaseItem -->|"itemRender не задан"| DefaultRow["дефолтная строка BaseItem"]
44
+ BaseItem -->|"itemRender задан"| Adapter["renderListItemCustom"]
45
+ Adapter --> ListItemCustom
46
+ ListItemCustom --> CWCItem["ContentWrapperCustom layout=item"]
47
+ Separator -->|"groupItemRender не задан"| DefaultGroup["дефолтный Separator"]
48
+ Separator -->|"groupItemRender задан"| AdapterG["renderListItemGroupCustom"]
49
+ AdapterG --> ListItemGroupCustom
50
+ ListItemGroupCustom --> CWCGroup["ContentWrapperCustom layout=group"]
51
+ ListItemCustom --> Wrap["itemWrapRender если задан"]
52
+ DefaultRow --> Wrap
53
+ ```
54
+
55
+ Подключение в продукте (прямой `List` / `Droplist`):
56
+
57
+ - `UserMenu` — `itemRender` / `groupItemRender` + `size="s"` на Droplist.
58
+ - `Favorites` — то же на дроплисте действий избранного.
59
+
60
+ Типичный вызов с адаптерами хедера:
61
+
62
+ ```ts
63
+ import { renderListItemCustom, renderListItemGroupCustom } from '@cloud-ru/ds-uikit-product-header';
64
+
65
+ <Droplist
66
+ size='s'
67
+ itemRender={renderListItemCustom}
68
+ groupItemRender={renderListItemGroupCustom}
69
+ // …
70
+ />
71
+ ```
72
+
73
+ ### Транзитивные потребители List (проброс API)
74
+
75
+ Оболочки ListCustom подключаются и там, где `Droplist` спрятан внутри другого пакета. В этих пакетах публичный API дополнен теми же слотами `itemRender` / `groupItemRender` (`Pick` из `DroplistProps`); значения уходят во внутренний `Droplist` и не попадают на триггер.
76
+
77
+ | Пакет | Публичная точка | Куда уходит |
78
+ |-------|-----------------|-------------|
79
+ | `@cloud-ru/ds-chips` | `ChipChoice.Single`, `ChipChoice.Multiple` (`ChipChoiceSelectCommonProps`) | внутренний `Droplist` списка опций |
80
+ | `@cloud-ru/ds-uikit-product-notification` | `NotificationPanelSettings` (`settings` у `NotificationPanel`); `NotificationCard`; `NotificationCardStack` | `NotificationPanelSettingsDroplist` / `ActionsButton` → `Droplist` |
81
+
82
+ Вне скоупа проброса (календарь / кастомный контент, не List-опции): `ChipChoice.Date`, `Time`, `DateRange`, `Custom`.
83
+
84
+ Пример для фильтров project-select и панели уведомлений:
85
+
86
+ ```ts
87
+ import { ChipChoice } from '@cloud-ru/ds-chips';
88
+ import { NotificationPanel } from '@cloud-ru/ds-uikit-product-notification';
89
+ import { renderListItemCustom, renderListItemGroupCustom } from '@cloud-ru/ds-uikit-product-header';
90
+
91
+ <ChipChoice.Single
92
+ itemRender={renderListItemCustom}
93
+ groupItemRender={renderListItemGroupCustom}
94
+ // …
95
+ />
96
+
97
+ <NotificationPanel
98
+ settings={{
99
+ actions,
100
+ itemRender: renderListItemCustom,
101
+ groupItemRender: renderListItemGroupCustom,
102
+ }}
103
+ // …
104
+ />
105
+ ```
106
+
107
+ Плотность (`useDesktopComfortClassName` / `dropDownClassName`) и оболочки ListCustom — разные оси: comfort меняет токены density контейнера; `itemRender` заменяет разметку строки по макету хедера.
108
+
109
+ ## ContentWrapperCustom (приватный)
110
+
111
+ Общий внутренний блок `beforeContent` + текст + `afterContent`. Аналог связки `contentWrapper` + `ItemContent` списка, с отступами мастера хедера (gap `primitive/6`, padding `4` / `8` для `layout=item`).
112
+
113
+ | Проп | Назначение |
114
+ |------|------------|
115
+ | `layout` | `'item'` — строка списка; `'group'` — заголовок группы (другие отступы и типографика) |
116
+ | `label` / `caption` / `description` | структурный текст; усечение через `TruncateString` |
117
+ | `contentNode` | готовый узел вместо структурного текста (например результат `contentRender`) |
118
+ | `beforeContent` / `afterContent` | иконочные / произвольные слоты |
119
+ | `selectButton` | кнопка массового выбора в заголовке группы |
120
+ | `groupVariant` | `'subtitle'` \| `'subtitleTertiary'` — цвет подписи группы |
121
+ | `disabled` | пониженная opacity слотов |
122
+
123
+ Структурный текст рендерится только если есть `label` и нет `contentNode`. `caption` и `description` показываются только при `layout=item`.
124
+
125
+ ## ListItemCustom
126
+
127
+ Оболочка строки (Figma `listItemCustom`, узел `17001:42063`). Визуальный аналог `BaseItem`, но индикаторы выбора и state layer собраны внутри по декларативным пропам мастера.
128
+
129
+ ### Пропы выбора (Figma)
130
+
131
+ | Проп | Значения | Эффект |
132
+ |------|----------|--------|
133
+ | `selectionMode` | `'none'` \| `'single'` \| `'multiple'` (дефолт `none`; из List приходит явно) | single → маркер; multiple → чекбокс; none — без индикаторов |
134
+ | `checked` | `boolean` | выбранность; для single ещё включает activated-подложку |
135
+ | `disabled` | `boolean` | курсор, opacity контента, цвет маркера; state layer не рисуется |
136
+
137
+ Тип пропсов совместим с `ItemRenderProps` из `@cloud-ru/ds-list` (`SELECTION_MODE` = `MODE` списка).
138
+
139
+ ### Логика индикаторов выбора
140
+
141
+ ```
142
+ showMarker = single && checked && !isParentNode && !switchControl
143
+ showCheckbox = multiple && !switchControl
144
+
145
+ stateLayer = activatedOnBackground только если single && checked && !disabled && !isParentNode && !switchControl
146
+ иначе emptyNeutralOnBackground (и только если !disabled)
147
+ ```
148
+
149
+ - **single + checked** — вертикальный маркер слева (`primary/accent`; при `disabled` — `neutral/accent` + opacity).
150
+ - **multiple** — `Checkbox` size `s` (мастер `checkboxS`), всегда в потоке строки, независимо от `checked`.
151
+ - **activated**-подложка только у single-выбранной активной строки; у multiple фон не меняется при выборе.
152
+ - Маркер и activated не показываются у parent-строки и при наличии `switchControl` (паритет с правилами `BaseItem`).
153
+
154
+ Фокус — `outline` на интерактивном `<li>`, без переноса `focusedFrame` из Figma.
155
+
156
+ ### Контент строки
157
+
158
+ 1. Если `contentFromRender` — структурный разбор пропускается, в слот текста идёт `contentNode`.
159
+ 2. Иначе `content` вида `{ label, caption?, description? }` или примитив → поля в `ContentWrapperCustom`.
160
+ 3. Слоты снаружи текста: `dragHandle` → маркер/чекбокс → `ContentWrapperCustom` → `switchControl` → `expandIcon`.
161
+
162
+ Обработчики (`onClick`, `onKeyDown`, …), `itemRef`, `tabIndex`, `data-test-id` вешаются на `<li>`; выбор и клавиатура по-прежнему живут в `BaseItem`, оболочка только принимает уже подготовленные колбэки.
163
+
164
+ ### Вложенность
165
+
166
+ `level` задаёт CSS-переменную `--level` и атрибуты `data-level-one` / `data-level-more-one` для отступа nesting из токенов списка size `s`.
167
+
168
+ ## ListItemGroupCustom
169
+
170
+ Оболочка заголовка группы (Figma `listItemGroupCustom`, узел `17001:43680`). Тип пропсов = `GroupItemRenderProps` из `@cloud-ru/ds-list`.
171
+
172
+ ### Figma-триада vs API: почему нет `groupVariant: 'divider'`
173
+
174
+ В мастере Figma у группы три **презентации**: подпись `subtitle`, подпись `subtitleTertiary`, только линия (divider-only). В коде это **не** один enum из трёх значений.
175
+
176
+ | Ось | API | Роль |
177
+ |-----|-----|------|
178
+ | Стиль подписи | `groupVariant?: 'subtitle' \| 'subtitleTertiary'` | цвет / вес текста заголовка |
179
+ | Линия | `divider?: boolean` | разделитель над/под группой (в оболочке хедера — под заголовком) |
180
+
181
+ Третий вид макета (только линия) — ветка рендера **`divider: true` без `label`**, а не третье значение `groupVariant`.
182
+
183
+ Почему так, а не 1:1 с Figma:
184
+
185
+ 1. **Оси ортогональны.** Уже есть кейс «подпись + линия»: `groupVariant: 'subtitle'` и одновременно `divider: true`. Один enum `'divider'` эту композицию не выражает без второго пропа.
186
+ 2. **Совпадение с `@cloud-ru/ds-list`.** `Separator`, `CommonGroupItem` и `GroupItemRenderProps` уже разделяют `groupVariant` и `divider`; оболочка хедера повторяет тот же контракт.
187
+ 3. **Divider-only — другая структура, не стиль текста.** Без `label` не применяется типографика `subtitle` / `subtitleTertiary`; рисуется только `Divider` (`variant=Regular`). Класть это в `groupVariant` смешивало бы «цвет подписи» и «тип строки».
188
+
189
+ Итого: в Controls / данных списка достаточно связки **`groupVariant` + `divider`** (+ наличие или отсутствие `label`). Значение `'divider'` в `groupVariant` в API не добавляется.
190
+
191
+ Ветки рендера:
192
+
193
+ | Условие | Результат |
194
+ |---------|-----------|
195
+ | есть `label` | `ContentWrapperCustom` с `layout=group`; при `divider` — ещё `Divider` под заголовком (`Regular` для `subtitle`, `Thin` для `subtitleTertiary`) |
196
+ | нет `label`, есть `divider` | только разделитель (`dividerOnly`) |
197
+ | иначе | `null` |
198
+
199
+ `selectButtonNode` из List передаётся в `ContentWrapperCustom` как `selectButton`. Уровень вложенности — тот же механизм `--level`, что у строки.
200
+
201
+ ## Адаптеры `renderList*`
202
+
203
+ Стабильные функции для пропсов List (ссылка не меняется между рендерами). Оба — прямой spread:
204
+
205
+ ```ts
206
+ export function renderListItemCustom(props: ItemRenderProps) {
207
+ return <ListItemCustom {...props} />;
208
+ }
209
+ export function renderListItemGroupCustom(props: GroupItemRenderProps) {
210
+ return <ListItemGroupCustom {...props} />;
211
+ }
212
+ ```
213
+
214
+ `BaseItem` кладёт в `ItemRenderProps.selectionMode` значение из `SelectionProvider` (`none` / `single` / `multiple`) и **не** передаёт готовые узлы маркера/чекбокса — их рисует оболочка.
215
+
216
+ ## Связь с List
217
+
218
+ | Слой | Ответственность |
219
+ |------|-----------------|
220
+ | `SelectionProvider` / `BaseItem` | режим выбора, значение, клик; сбор `ItemRenderProps` с `selectionMode` |
221
+ | `renderListItemCustom` | `<ListItemCustom {...props} />` |
222
+ | `ListItemCustom` | раскладка и индикаторы выбора по `selectionMode` / `checked` / `disabled` |
223
+ | `itemWrapRender` | опциональная обёртка поверх уже собранной оболочки (ссылка и т.п.) |
224
+
225
+ Ось `size` у оболочек не дублируется: отступы зашиты под мастер `s`. Потребители в хедере ставят `size="s"` на List/Droplist, чтобы служебные узлы списка (если останутся) совпадали по шкале.
226
+
227
+ ## Стори
228
+
229
+ | Story | Назначение |
230
+ |-------|------------|
231
+ | `ListItemCustom.Playground` | Controls: `selectionMode`, `checked`, `disabled`, `asParentGroup`, `asNested` |
232
+ | `ListItemCustom.VisualMatrix` | две таблицы: `single` и `multiple`; строки enabled/disabled × колонки unchecked/checked |
233
+ | `ListItemGroupCustom.Playground` | Controls: `groupVariant` + `divider` (+ story-only `withLabel` для divider-only) |
234
+ | `ListItemGroupCustom.VisualMatrix` | вертикально три презентации Figma: subtitle / subtitleTertiary / divider-only |
235
+ | `ListCustom.Playground` | smoke: List + оба рендера (legacy, будет удалена позже) |
236
+ | `ListCustom.VisualMatrix` | объединённая матрица (legacy, будет удалена позже) |
237
+
238
+ ## Figma (ориентиры)
239
+
240
+ | Мастер | Node |
241
+ |--------|------|
242
+ | `listItemCustom` | `17001:42063` |
243
+ | `contentWrapperCustom` | `17001:43602` |
244
+ | `listItemGroupCustom` | `17001:43680` |
245
+
246
+ Варианты строки: `selectionMode` × `checked` × `disabled` (восемь комбинаций single/multiple).
@@ -6,8 +6,8 @@ import { useEventHandler, useValueControl } from '@cloud-ru/ds-utils';
6
6
  import cn from 'classnames';
7
7
  import { MouseEvent, useMemo } from 'react';
8
8
 
9
- import { useDesktopComfortClassName } from '../../../../hooks/useDesktopComfortClassName';
10
9
  import { headerLocale } from '../../../../locale';
10
+ import { renderListItemCustom, renderListItemGroupCustom } from '../../../ListCustom';
11
11
  import { FavoriteProps, InnerLink } from '../../types';
12
12
  import { resolveInnerLinksByIds } from '../../utils';
13
13
  import { Content, FavoritesItemsSkeleton } from './components';
@@ -28,7 +28,6 @@ export type FavoritesProps = {
28
28
  export function Favorites({ favorite, servicesById, headerClassName, isMobile }: FavoritesProps) {
29
29
  const { t } = headerLocale.useTranslations();
30
30
  const loading = favorite.loading;
31
- const comfortClassName = useDesktopComfortClassName();
32
31
 
33
32
  const [segment, setSegment] = useValueControl<FavoritesSegment>({
34
33
  value: favorite.segment,
@@ -82,7 +81,13 @@ export function Favorites({ favorite, servicesById, headerClassName, isMobile }:
82
81
  />
83
82
 
84
83
  {favorite.actions && (
85
- <Droplist size='m' {...favorite.actions} className={comfortClassName} closeDroplistOnItemClick>
84
+ <Droplist
85
+ {...favorite.actions}
86
+ size='s'
87
+ closeDroplistOnItemClick
88
+ itemRender={renderListItemCustom}
89
+ groupItemRender={renderListItemGroupCustom}
90
+ >
86
91
  <Button
87
92
  view='simple'
88
93
  size='m'