@cloud-ru/ds-popup-private 1.0.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 (112) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +163 -0
  4. package/dist/cjs/components/FooterActions/FooterActions.d.ts +44 -0
  5. package/dist/cjs/components/FooterActions/FooterActions.js +44 -0
  6. package/dist/cjs/components/FooterActions/index.d.ts +1 -0
  7. package/dist/cjs/components/FooterActions/index.js +17 -0
  8. package/dist/cjs/components/FooterActions/styles.module.css +20 -0
  9. package/dist/cjs/components/PopupBody/PopupBody.d.ts +3 -0
  10. package/dist/cjs/components/PopupBody/PopupBody.js +15 -0
  11. package/dist/cjs/components/PopupBody/index.d.ts +1 -0
  12. package/dist/cjs/components/PopupBody/index.js +17 -0
  13. package/dist/cjs/components/PopupBody/styles.module.css +16 -0
  14. package/dist/cjs/components/PopupCloseButton/PopupCloseButton.d.ts +11 -0
  15. package/dist/cjs/components/PopupCloseButton/PopupCloseButton.js +16 -0
  16. package/dist/cjs/components/PopupCloseButton/index.d.ts +1 -0
  17. package/dist/cjs/components/PopupCloseButton/index.js +17 -0
  18. package/dist/cjs/components/PopupCloseButton/styles.module.css +109 -0
  19. package/dist/cjs/components/PopupFooter/PopupFooter.d.ts +3 -0
  20. package/dist/cjs/components/PopupFooter/PopupFooter.js +17 -0
  21. package/dist/cjs/components/PopupFooter/index.d.ts +1 -0
  22. package/dist/cjs/components/PopupFooter/index.js +17 -0
  23. package/dist/cjs/components/PopupFooter/styles.module.css +32 -0
  24. package/dist/cjs/components/PopupHeader/PopupHeader.d.ts +3 -0
  25. package/dist/cjs/components/PopupHeader/PopupHeader.js +37 -0
  26. package/dist/cjs/components/PopupHeader/index.d.ts +1 -0
  27. package/dist/cjs/components/PopupHeader/index.js +17 -0
  28. package/dist/cjs/components/PopupHeader/styles.module.css +128 -0
  29. package/dist/cjs/components/PopupMedia/PopupMedia.d.ts +7 -0
  30. package/dist/cjs/components/PopupMedia/PopupMedia.js +14 -0
  31. package/dist/cjs/components/PopupMedia/index.d.ts +1 -0
  32. package/dist/cjs/components/PopupMedia/index.js +17 -0
  33. package/dist/cjs/components/PopupMedia/styles.module.css +27 -0
  34. package/dist/cjs/components/index.d.ts +6 -0
  35. package/dist/cjs/components/index.js +22 -0
  36. package/dist/cjs/constants.d.ts +68 -0
  37. package/dist/cjs/constants.js +73 -0
  38. package/dist/cjs/context/overlaySurface.d.ts +35 -0
  39. package/dist/cjs/context/overlaySurface.js +45 -0
  40. package/dist/cjs/index.d.ts +4 -0
  41. package/dist/cjs/index.js +20 -0
  42. package/dist/cjs/package.json +3 -0
  43. package/dist/cjs/style.css +332 -0
  44. package/dist/cjs/types.d.ts +95 -0
  45. package/dist/cjs/types.js +2 -0
  46. package/dist/esm/components/FooterActions/FooterActions.d.ts +44 -0
  47. package/dist/esm/components/FooterActions/FooterActions.js +38 -0
  48. package/dist/esm/components/FooterActions/index.d.ts +1 -0
  49. package/dist/esm/components/FooterActions/index.js +1 -0
  50. package/dist/esm/components/FooterActions/styles.module.css +20 -0
  51. package/dist/esm/components/PopupBody/PopupBody.d.ts +3 -0
  52. package/dist/esm/components/PopupBody/PopupBody.js +9 -0
  53. package/dist/esm/components/PopupBody/index.d.ts +1 -0
  54. package/dist/esm/components/PopupBody/index.js +1 -0
  55. package/dist/esm/components/PopupBody/styles.module.css +16 -0
  56. package/dist/esm/components/PopupCloseButton/PopupCloseButton.d.ts +11 -0
  57. package/dist/esm/components/PopupCloseButton/PopupCloseButton.js +10 -0
  58. package/dist/esm/components/PopupCloseButton/index.d.ts +1 -0
  59. package/dist/esm/components/PopupCloseButton/index.js +1 -0
  60. package/dist/esm/components/PopupCloseButton/styles.module.css +109 -0
  61. package/dist/esm/components/PopupFooter/PopupFooter.d.ts +3 -0
  62. package/dist/esm/components/PopupFooter/PopupFooter.js +11 -0
  63. package/dist/esm/components/PopupFooter/index.d.ts +1 -0
  64. package/dist/esm/components/PopupFooter/index.js +1 -0
  65. package/dist/esm/components/PopupFooter/styles.module.css +32 -0
  66. package/dist/esm/components/PopupHeader/PopupHeader.d.ts +3 -0
  67. package/dist/esm/components/PopupHeader/PopupHeader.js +31 -0
  68. package/dist/esm/components/PopupHeader/index.d.ts +1 -0
  69. package/dist/esm/components/PopupHeader/index.js +1 -0
  70. package/dist/esm/components/PopupHeader/styles.module.css +128 -0
  71. package/dist/esm/components/PopupMedia/PopupMedia.d.ts +7 -0
  72. package/dist/esm/components/PopupMedia/PopupMedia.js +8 -0
  73. package/dist/esm/components/PopupMedia/index.d.ts +1 -0
  74. package/dist/esm/components/PopupMedia/index.js +1 -0
  75. package/dist/esm/components/PopupMedia/styles.module.css +27 -0
  76. package/dist/esm/components/index.d.ts +6 -0
  77. package/dist/esm/components/index.js +6 -0
  78. package/dist/esm/constants.d.ts +68 -0
  79. package/dist/esm/constants.js +70 -0
  80. package/dist/esm/context/overlaySurface.d.ts +35 -0
  81. package/dist/esm/context/overlaySurface.js +38 -0
  82. package/dist/esm/index.d.ts +4 -0
  83. package/dist/esm/index.js +4 -0
  84. package/dist/esm/style.css +332 -0
  85. package/dist/esm/types.d.ts +95 -0
  86. package/dist/esm/types.js +1 -0
  87. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  88. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  89. package/package.json +46 -0
  90. package/src/components/FooterActions/FooterActions.tsx +111 -0
  91. package/src/components/FooterActions/index.ts +1 -0
  92. package/src/components/FooterActions/styles.module.scss +32 -0
  93. package/src/components/PopupBody/PopupBody.tsx +20 -0
  94. package/src/components/PopupBody/index.ts +1 -0
  95. package/src/components/PopupBody/styles.module.scss +24 -0
  96. package/src/components/PopupCloseButton/PopupCloseButton.tsx +37 -0
  97. package/src/components/PopupCloseButton/index.ts +1 -0
  98. package/src/components/PopupCloseButton/styles.module.scss +39 -0
  99. package/src/components/PopupFooter/PopupFooter.tsx +24 -0
  100. package/src/components/PopupFooter/index.ts +1 -0
  101. package/src/components/PopupFooter/styles.module.scss +49 -0
  102. package/src/components/PopupHeader/PopupHeader.tsx +125 -0
  103. package/src/components/PopupHeader/index.ts +1 -0
  104. package/src/components/PopupHeader/styles.module.scss +158 -0
  105. package/src/components/PopupMedia/PopupMedia.tsx +18 -0
  106. package/src/components/PopupMedia/index.ts +1 -0
  107. package/src/components/PopupMedia/styles.module.scss +33 -0
  108. package/src/components/index.ts +6 -0
  109. package/src/constants.ts +72 -0
  110. package/src/context/overlaySurface.tsx +65 -0
  111. package/src/index.ts +4 -0
  112. package/src/types.ts +110 -0
@@ -0,0 +1,24 @@
1
+ import { extractSupportProps } from '@cloud-ru/ds-utils';
2
+ import cn from 'classnames';
3
+
4
+ import { TEST_IDS } from '../../constants';
5
+ import { useSurfaceTokenSegment } from '../../context/overlaySurface';
6
+ import { PopupFooterProps } from '../../types';
7
+ import styles from './styles.module.scss';
8
+
9
+ /** Нижняя action-зона overlay'я (рендерит `children` в `bottomBar`-секции). Раскладка по поверхности. */
10
+ export function PopupFooter({ children, className, ...rest }: PopupFooterProps) {
11
+ const surface = useSurfaceTokenSegment();
12
+
13
+ return (
14
+ <div
15
+ className={cn(styles.root, className)}
16
+ data-surface={surface}
17
+ // Дефолтный id идёт до spread — потребитель-обёртка (drawer) может переопределить своим.
18
+ data-test-id={TEST_IDS.footer}
19
+ {...extractSupportProps(rest)}
20
+ >
21
+ <div className={styles.contentWrapper}>{children}</div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1 @@
1
+ export * from './PopupFooter';
@@ -0,0 +1,49 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/popupPrivate.module' as popupPrivate;
3
+
4
+ // `bottomBar` overlay'я. Раскладка и паддинги зависят от поверхности (`data-surface`):
5
+ // `bottomSheet` — sheet-мастер (drag-зона, растяжение row-ButtonGroup), `window` — modal/drawer.
6
+ .root {
7
+ display: flex;
8
+ flex-shrink: 0;
9
+ box-sizing: border-box;
10
+
11
+ // bottomSheet: flex column, justify-center. `align-items: stretch` (не items-start) —
12
+ // чтобы row-ButtonGroup растягивался на всю ширину и `break`/space-between прижимал кнопки к краям.
13
+ &[data-surface='bottomSheet'] {
14
+ // Drag-зона (footer). Гасим нативные жесты; tap'ы по кнопкам внутри работают как обычно.
15
+ touch-action: none;
16
+
17
+ flex-direction: column;
18
+ align-items: stretch;
19
+ justify-content: center;
20
+
21
+ padding: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'footer', 'padding-top')
22
+ base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'footer', 'padding-right')
23
+ base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'footer', 'padding-bottom')
24
+ base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'footer', 'padding-left');
25
+ }
26
+
27
+ // window: modal / drawer(desktop). Композит window.footer (paddingTop у мастера нет — 0).
28
+ &[data-surface='window'] {
29
+ align-items: center;
30
+ align-self: stretch;
31
+
32
+ @include base.composite-var(popupPrivate.$popupPrivate, 'anatomy', 'window', 'footer');
33
+ }
34
+ }
35
+
36
+ .contentWrapper {
37
+ display: flex;
38
+ flex-direction: column;
39
+
40
+ [data-surface='bottomSheet'] & {
41
+ gap: base.$sn-density-spacing-interval-m;
42
+ }
43
+
44
+ [data-surface='window'] & {
45
+ flex: 1 0 0;
46
+ align-items: flex-start;
47
+ justify-content: center;
48
+ }
49
+ }
@@ -0,0 +1,125 @@
1
+ import { Button } from '@cloud-ru/ds-button';
2
+ import { ArrowLeftSVG } from '@cloud-ru/ds-icons/interface/system';
3
+ import { TruncateString } from '@cloud-ru/ds-truncate-string';
4
+ import { Typography } from '@cloud-ru/ds-typography';
5
+ import { extractSupportProps } from '@cloud-ru/ds-utils';
6
+ import cn from 'classnames';
7
+
8
+ import { TEST_IDS } from '../../constants';
9
+ import { OVERLAY_SURFACE, useOverlaySurface, useSurfaceTokenSegment } from '../../context/overlaySurface';
10
+ import { PopupHeaderProps } from '../../types';
11
+ import styles from './styles.module.scss';
12
+
13
+ /** Header overlay'я: back-button + title + slotAfterTitle + actionButton + subtitle + slotSecondTitle. */
14
+ export function PopupHeader({
15
+ title,
16
+ titleId,
17
+ slotAfterTitle,
18
+ subtitle,
19
+ slotSecondTitle,
20
+ onBackButtonClick,
21
+ actionButton,
22
+ truncate,
23
+ testIds,
24
+ className,
25
+ ...rest
26
+ }: PopupHeaderProps) {
27
+ const surface = useSurfaceTokenSegment();
28
+ // `slotSecondTitle` (search/segment) есть только в мастере bottomSheet — на window/modal/drawer игнор.
29
+ const isSheet = useOverlaySurface() === OVERLAY_SURFACE.Sheet;
30
+ const hasHeadline = Boolean(title || onBackButtonClick || actionButton || slotAfterTitle);
31
+
32
+ // Мобильный sheet — крупный `title-l`; desktop window (modal/drawer) — `headline-s`.
33
+ const titleContent =
34
+ truncate?.title !== undefined && typeof title === 'string' ? (
35
+ <TruncateString maxLines={truncate.title} text={title} />
36
+ ) : (
37
+ title
38
+ );
39
+ const subtitleContent =
40
+ truncate?.subtitle !== undefined && typeof subtitle === 'string' ? (
41
+ <TruncateString maxLines={truncate.subtitle} text={subtitle} />
42
+ ) : (
43
+ subtitle
44
+ );
45
+
46
+ // Потребитель-обёртка может переопределить id слотов; пропущенные берём из `TEST_IDS`.
47
+ const ids = {
48
+ header: testIds?.header ?? TEST_IDS.header,
49
+ title: testIds?.title ?? TEST_IDS.title,
50
+ slotAfterTitle: testIds?.slotAfterTitle ?? TEST_IDS.slotAfterTitle,
51
+ subtitle: testIds?.subtitle ?? TEST_IDS.subtitle,
52
+ slotSecondTitle: testIds?.slotSecondTitle ?? TEST_IDS.slotSecondTitle,
53
+ backButton: testIds?.backButton ?? TEST_IDS.backButton,
54
+ actionButton: testIds?.actionButton ?? TEST_IDS.actionButton,
55
+ };
56
+
57
+ return (
58
+ <div
59
+ className={cn(styles.root, className)}
60
+ data-surface={surface}
61
+ data-with-before={Boolean(onBackButtonClick) || undefined}
62
+ {...extractSupportProps(rest)}
63
+ data-test-id={ids.header}
64
+ >
65
+ {hasHeadline && (
66
+ <div
67
+ className={styles.headlineWrapper}
68
+ data-with-before={Boolean(onBackButtonClick) || undefined}
69
+ data-with-after={Boolean(actionButton) || undefined}
70
+ >
71
+ {onBackButtonClick && (
72
+ <Button
73
+ view='function'
74
+ appearance='neutral'
75
+ size='m'
76
+ icon={<ArrowLeftSVG />}
77
+ aria-label='Назад'
78
+ onClick={onBackButtonClick}
79
+ data-test-id={ids.backButton}
80
+ />
81
+ )}
82
+
83
+ <div className={styles.headline}>
84
+ {title && (
85
+ <Typography
86
+ variant={isSheet ? 'title' : 'headline'}
87
+ size={isSheet ? 'l' : 's'}
88
+ id={titleId}
89
+ className={styles.title}
90
+ data-test-id={ids.title}
91
+ >
92
+ {titleContent}
93
+ </Typography>
94
+ )}
95
+ {slotAfterTitle && (
96
+ <span className={styles.slotAfterTitle} data-test-id={ids.slotAfterTitle}>
97
+ {slotAfterTitle}
98
+ </span>
99
+ )}
100
+ </div>
101
+
102
+ {actionButton && (
103
+ <span className={styles.actionButton} data-test-id={ids.actionButton}>
104
+ {actionButton}
105
+ </span>
106
+ )}
107
+ </div>
108
+ )}
109
+
110
+ {subtitle && (
111
+ <div className={styles.subtitleWrapper} data-test-id={ids.subtitle}>
112
+ <Typography variant='body' size='m' className={styles.subtitle}>
113
+ {subtitleContent}
114
+ </Typography>
115
+ </div>
116
+ )}
117
+
118
+ {isSheet && slotSecondTitle && (
119
+ <div className={styles.secondWrapper} data-test-id={ids.slotSecondTitle}>
120
+ {slotSecondTitle}
121
+ </div>
122
+ )}
123
+ </div>
124
+ );
125
+ }
@@ -0,0 +1 @@
1
+ export * from './PopupHeader';
@@ -0,0 +1,158 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/popupPrivate.module' as popupPrivate;
3
+
4
+ // Header overlay'я. Раскладка и токены зависят от поверхности (`data-surface` на `.root`):
5
+ // `bottomSheet` — sheet-мастер (drag-зона, before/after-кнопки, secondWrapper/search),
6
+ // `window` — modal / drawer(desktop).
7
+
8
+ .root {
9
+ display: flex;
10
+ flex-direction: column;
11
+ flex-shrink: 0;
12
+ box-sizing: border-box;
13
+
14
+ &[data-surface='bottomSheet'] {
15
+ gap: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'gap');
16
+ padding-top: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'padding-top');
17
+ padding-bottom: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'padding-bottom');
18
+ }
19
+
20
+ &[data-surface='window'] {
21
+ align-items: flex-start;
22
+ align-self: stretch;
23
+ justify-content: center;
24
+
25
+ @include base.composite-var(popupPrivate.$popupPrivate, 'anatomy', 'window', 'header');
26
+
27
+ // Зазор шапки в макете есть только с кнопкой «назад»: 80 против 76 без неё.
28
+ // Через `:not(...)`, а не голым `gap: 0`: stylelint поднимает свойство выше миксина.
29
+ &:not([data-with-before]) {
30
+ gap: 0;
31
+ }
32
+ }
33
+ }
34
+
35
+ .headlineWrapper {
36
+ display: flex;
37
+ // `.headline` (py + lineHeight) ниже кнопки (48) — центрируем, чтобы заголовок был на одной
38
+ // линии с back/action-кнопкой.
39
+ align-items: center;
40
+
41
+ [data-surface='bottomSheet'] & {
42
+ // `touch-action: none` гасит нативные жесты; tap'ы по кнопкам внутри продолжают работать.
43
+ touch-action: none;
44
+ // gap между wrapper-слотами появляется только при наличии кнопок (control=no → gap 0).
45
+ gap: 0;
46
+
47
+ &[data-with-before],
48
+ &[data-with-after] {
49
+ gap: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'headline', 'gap');
50
+ }
51
+
52
+ &[data-with-before] {
53
+ padding-left: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'headline', 'padding-left');
54
+ }
55
+
56
+ &[data-with-after] {
57
+ padding-right: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'headline', 'padding-right');
58
+ }
59
+ }
60
+
61
+ // window: modal/drawer. Токен `headline-paddingHorizontal` симметричный, но в макете правый
62
+ // отступ нулевой — headline упирается в правый край окна, отступ текста даёт inner-wrapper.
63
+ [data-surface='window'] & {
64
+ align-self: stretch;
65
+
66
+ @include base.composite-var(popupPrivate.$popupPrivate, 'anatomy', 'window', 'header', 'headline');
67
+
68
+ padding-right: 0;
69
+
70
+ &:not([data-with-before]) {
71
+ padding-left: 0;
72
+ }
73
+ }
74
+ }
75
+
76
+ .headline {
77
+ display: flex;
78
+ align-items: center;
79
+ box-sizing: border-box;
80
+ min-width: 0;
81
+
82
+ [data-surface='bottomSheet'] & {
83
+ flex: 1 1 auto;
84
+ gap: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'headline', 'headlineWrapper', 'gap');
85
+ padding-top: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'headline', 'headlineWrapper', 'padding-top');
86
+ padding-bottom: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'headline', 'headlineWrapper', 'padding-bottom');
87
+ }
88
+
89
+ [data-surface='window'] & {
90
+ flex: 1 0 0;
91
+
92
+ @include base.composite-var(popupPrivate.$popupPrivate, 'anatomy', 'window', 'header', 'headline', 'headlineWrapper');
93
+ }
94
+ }
95
+
96
+ [data-surface='bottomSheet'] .headlineWrapper:not([data-with-before]) .headline {
97
+ padding-left: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'headline', 'headlineWrapper', 'padding-left');
98
+ }
99
+
100
+ [data-surface='bottomSheet'] .headlineWrapper:not([data-with-after]) .headline {
101
+ padding-right: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'headline', 'headlineWrapper', 'padding-right');
102
+ }
103
+
104
+ // window: на стороне С кнопкой inner-wrapper не добавляет левый отступ (его даёт headline).
105
+ [data-surface='window'] .headlineWrapper[data-with-before] .headline {
106
+ padding-left: 0;
107
+ }
108
+
109
+ .title {
110
+ flex: 0 1 auto;
111
+ min-width: 0;
112
+ color: base.$sn-theme-color-available-version-textMain;
113
+ }
114
+
115
+ // Слот занимает остаток строки заголовка: потребитель кладёт сюда и компактный тултип,
116
+ // и полноширинный контент (счётчик выделения тулбара), который выравнивается по правому краю.
117
+ .slotAfterTitle {
118
+ display: inline-flex;
119
+ flex: 1 1 auto;
120
+ align-items: center;
121
+ min-width: 0;
122
+ }
123
+
124
+ .actionButton {
125
+ display: inline-flex;
126
+ flex-shrink: 0;
127
+ align-items: center;
128
+ }
129
+
130
+ .subtitleWrapper {
131
+ display: flex;
132
+ flex-direction: column;
133
+ align-items: flex-start;
134
+ box-sizing: border-box;
135
+
136
+ [data-surface='bottomSheet'] & {
137
+ padding-right: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'subHeadlineWrapper', 'padding-right');
138
+ padding-left: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'subHeadlineWrapper', 'padding-left');
139
+ }
140
+
141
+ [data-surface='window'] & {
142
+ align-self: stretch;
143
+
144
+ @include base.composite-var(popupPrivate.$popupPrivate, 'anatomy', 'window', 'header', 'subHeadlineWrapper');
145
+ }
146
+ }
147
+
148
+ .subtitle {
149
+ color: base.$sn-theme-color-available-version-textSecondary;
150
+ }
151
+
152
+ .secondWrapper {
153
+ box-sizing: border-box;
154
+
155
+ [data-surface='bottomSheet'] & {
156
+ @include base.composite-var(popupPrivate.$popupPrivate, 'anatomy', 'bottomSheet', 'header', 'secondWrapper');
157
+ }
158
+ }
@@ -0,0 +1,18 @@
1
+ import cn from 'classnames';
2
+
3
+ import { MEDIA_KIND, TEST_IDS } from '../../constants';
4
+ import { PopupMediaProps } from '../../types';
5
+ import styles from './styles.module.scss';
6
+
7
+ type MediaProps = PopupMediaProps & {
8
+ className?: string;
9
+ };
10
+
11
+ /** Media-блок над headline: `kind='image'` — во всю ширину, `kind='icon'` — иконка. */
12
+ export function PopupMedia({ src, alt, kind = MEDIA_KIND.Image, className }: MediaProps) {
13
+ return (
14
+ <div className={cn(styles.wrapper, className)} data-media-kind={kind} data-test-id={TEST_IDS.media}>
15
+ <img src={src} alt={alt} className={kind === MEDIA_KIND.Image ? styles.image : styles.icon} />
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1 @@
1
+ export * from './PopupMedia';
@@ -0,0 +1,33 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/popupPrivate.module' as popupPrivate;
3
+
4
+ .wrapper {
5
+ // Drag-зона (media над шапкой). Гасим нативные жесты на стороне браузера.
6
+ touch-action: none;
7
+
8
+ overflow: hidden;
9
+ display: flex;
10
+ flex-shrink: 0;
11
+ align-items: center;
12
+ justify-content: center;
13
+
14
+ box-sizing: border-box;
15
+ width: 100%;
16
+
17
+ &[data-media-kind='icon'] {
18
+ padding-top: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'window', 'media', 'icon', 'padding-top');
19
+ }
20
+ }
21
+
22
+ .image {
23
+ display: block;
24
+ width: 100%;
25
+ min-height: base.simple-var(popupPrivate.$popupPrivate, 'anatomy', 'window', 'media', 'image', 'min-height');
26
+ object-fit: cover;
27
+ }
28
+
29
+ .icon {
30
+ display: block;
31
+ max-width: 100%;
32
+ height: auto;
33
+ }
@@ -0,0 +1,6 @@
1
+ export * from './PopupHeader';
2
+ export * from './PopupBody';
3
+ export * from './PopupFooter';
4
+ export * from './PopupMedia';
5
+ export * from './PopupCloseButton';
6
+ export * from './FooterActions';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Тип media-блока в overlay-шапке.
3
+ *
4
+ * - `Image` — изображение во всю ширину (высота 184px), прижато к шапке (убирается
5
+ * верхний отступ контент-блока). Горизонтальные паддинги body этим не меняются — они отдельно через `bodyPadding`.
6
+ * - `Icon` — иконка с `padding-top: 24px`.
7
+ *
8
+ * Соответствие Figma: `bottomSheetMediaImage` (360×184) и `bottomSheetMediaIcon` (360×104).
9
+ */
10
+ export const MEDIA_KIND = {
11
+ Image: 'image',
12
+ Icon: 'icon',
13
+ } as const;
14
+
15
+ /**
16
+ * Ориентация кнопок футера, собранных из `approveButton` / `cancelButton` / `additionalButton`.
17
+ * Применяется только при ровно двух кнопках (см. `footerActionsOrientation`).
18
+ *
19
+ * - `Horizontal` — кнопки в ряд через space-between: secondary слева, primary справа, ширина по
20
+ * контенту. Дефолт — точное соответствие Figma `bottomBar.buttonGroup` (cancel/confirm).
21
+ * - `Vertical` — кнопки в столбик, full-width (primary сверху).
22
+ */
23
+ export const FOOTER_ACTIONS_ORIENTATION = {
24
+ Vertical: 'vertical',
25
+ Horizontal: 'horizontal',
26
+ } as const;
27
+
28
+ /**
29
+ * Стабильные `data-test-id` для слотов overlay-chrome (header / body / footer / media / close).
30
+ *
31
+ * Используются для e2e-локаторов в Playwright и Storybook play-функциях. Реэкспортируется из
32
+ * `@cloud-ru/ds-bottom-sheet`, чтобы потребитель (приложение) ссылался на те же id, что и наши тесты.
33
+ *
34
+ * Значения намеренно сохраняют префикс `bottom-sheet__` — на фазе выноса слоя из `@cloud-ru/ds-bottom-sheet`
35
+ * они переносятся 1:1 без смены поведения; переименование в `popup-private__*` — отдельная фаза.
36
+ *
37
+ * Figma-typo-мост (см. `.claude/rules/figma-integration.md`):
38
+ * Figma `badgeHandle` → код `handle` (показывается при `swipeEnabled`)
39
+ * Figma `blackout` → код `backdrop` / `showBackdrop`
40
+ * Figma `topBar` → код `header` / `onBackButtonClick`
41
+ * Figma `bottomBar` → код `footer`
42
+ * Figma `+ slotMedia` → код `media`
43
+ * Figma `+ slotSubtitle` → код `subtitle` (текст под title)
44
+ * Figma `+ slotSecondTitle` → код `slotSecondTitle` (search/segment, только sheet)
45
+ * Figma `+ slotAfterHeadline` → код `slotAfterTitle`
46
+ * Figma `+ slotBottomBar` → код `footer` (ReactNode)
47
+ * Figma `buttonBefore` (48×48) → код auto-back-button по `onBackButtonClick`
48
+ * Figma `buttonAfter` (48×48) → код `actionButton` (ReactNode)
49
+ */
50
+ export const TEST_IDS = {
51
+ root: 'bottom-sheet',
52
+ backdrop: 'bottom-sheet__backdrop',
53
+ handle: 'bottom-sheet__handle',
54
+ header: 'bottom-sheet__header',
55
+ title: 'bottom-sheet__title',
56
+ slotAfterTitle: 'bottom-sheet__slot-after-title',
57
+ subtitle: 'bottom-sheet__subtitle',
58
+ slotSecondTitle: 'bottom-sheet__slot-second-title',
59
+ backButton: 'bottom-sheet__back-button',
60
+ actionButton: 'bottom-sheet__action-button',
61
+ media: 'bottom-sheet__media',
62
+ closeButton: 'popup__close-button',
63
+ body: 'bottom-sheet__body',
64
+ footer: 'bottom-sheet__footer',
65
+ footerApprove: 'bottom-sheet__footer-approve',
66
+ footerCancel: 'bottom-sheet__footer-cancel',
67
+ footerAdditional: 'bottom-sheet__footer-additional',
68
+ // Два разных разделителя (topBar↔body и body↔footer) — разные id, чтобы каждый адресовался
69
+ // однозначно (без strict-mode-коллизии `getByTestId`).
70
+ dividerTop: 'bottom-sheet__divider-top',
71
+ dividerBottom: 'bottom-sheet__divider-bottom',
72
+ } as const;
@@ -0,0 +1,65 @@
1
+ import { ValueOf } from '@cloud-ru/ds-utils';
2
+ import { createContext, ReactNode, useContext } from 'react';
3
+
4
+ /** Поверхность overlay'я, в которой рендерятся общие слоты (`Header` / `Body` / `Footer` / `Media`). */
5
+ export const OVERLAY_SURFACE = {
6
+ Modal: 'modal',
7
+ Drawer: 'drawer',
8
+ Sheet: 'sheet',
9
+ } as const;
10
+
11
+ export type OverlaySurface = ValueOf<typeof OVERLAY_SURFACE>;
12
+
13
+ /**
14
+ * Сегмент коллекции `popupPrivate.anatomy.*`, из которого слоты берут токены раскладки.
15
+ * Modal и Drawer(desktop) рисуются мастером `window`; Sheet — `bottomSheet`.
16
+ */
17
+ export const SURFACE_TOKEN_SEGMENT = {
18
+ [OVERLAY_SURFACE.Modal]: 'window',
19
+ [OVERLAY_SURFACE.Drawer]: 'window',
20
+ [OVERLAY_SURFACE.Sheet]: 'bottomSheet',
21
+ } as const;
22
+
23
+ export type SurfaceTokenSegment = ValueOf<typeof SURFACE_TOKEN_SEGMENT>;
24
+
25
+ type OverlaySurfaceContextValue = {
26
+ surface: OverlaySurface;
27
+ /** Только drawer: высота панели по контенту (`position: top|bottom`) — прокидывается во `Body`. */
28
+ bodyHeightAuto: boolean;
29
+ };
30
+
31
+ // Дефолт `'sheet'`: слоты без провайдера ведут себя как bottom-sheet.
32
+ const OverlaySurfaceContext = createContext<OverlaySurfaceContextValue>({
33
+ surface: OVERLAY_SURFACE.Sheet,
34
+ bodyHeightAuto: false,
35
+ });
36
+
37
+ type OverlaySurfaceProviderProps = {
38
+ /** Поверхность, в которой рендерятся дочерние слоты. */
39
+ surface: OverlaySurface;
40
+ /** Только drawer: высота панели по контенту → `Body` (drawer-ветка) добавляет соответствующий класс. */
41
+ bodyHeightAuto?: boolean;
42
+ children: ReactNode;
43
+ };
44
+
45
+ /** Провайдер поверхности overlay'я. Frame (sheet / modal / drawer) оборачивает в него свои слоты. */
46
+ export function OverlaySurfaceProvider({ surface, bodyHeightAuto = false, children }: OverlaySurfaceProviderProps) {
47
+ return (
48
+ <OverlaySurfaceContext.Provider value={{ surface, bodyHeightAuto }}>{children}</OverlaySurfaceContext.Provider>
49
+ );
50
+ }
51
+
52
+ /** Текущая поверхность overlay'я. Без провайдера возвращает `'sheet'`. */
53
+ export function useOverlaySurface(): OverlaySurface {
54
+ return useContext(OverlaySurfaceContext).surface;
55
+ }
56
+
57
+ /** Только drawer-ветка `Body`: высота панели по контенту (из `DrawerFrame`). */
58
+ export function useOverlayBodyHeightAuto(): boolean {
59
+ return useContext(OverlaySurfaceContext).bodyHeightAuto;
60
+ }
61
+
62
+ /** Токен-сегмент текущей поверхности (`window` / `bottomSheet`). Без провайдера — `bottomSheet`. */
63
+ export function useSurfaceTokenSegment(): SurfaceTokenSegment {
64
+ return SURFACE_TOKEN_SEGMENT[useOverlaySurface()];
65
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './components';
2
+ export * from './constants';
3
+ export * from './context/overlaySurface';
4
+ export * from './types';