@cloud-ru/ds-uikit-product-modal-predefined 1.0.18-preview-424ddd29.0 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cjs/constants.d.ts +0 -2
  3. package/dist/cjs/constants.js +1 -3
  4. package/dist/cjs/helperComponents/NoteItem/NoteItem.js +1 -1
  5. package/dist/cjs/helperComponents/NoteItem/NoteItemSkeleton.d.ts +1 -3
  6. package/dist/cjs/helperComponents/NoteItem/NoteItemSkeleton.js +2 -2
  7. package/dist/cjs/helperComponents/NoteItem/styles.module.css +4 -15
  8. package/dist/cjs/helperComponents/ReleaseNotesContent/ReleaseNotesContent.js +6 -9
  9. package/dist/cjs/helperComponents/ReleaseNotesContent/styles.module.css +8 -0
  10. package/dist/cjs/hooks.d.ts +0 -6
  11. package/dist/cjs/hooks.js +0 -19
  12. package/dist/cjs/style.css +12 -15
  13. package/dist/esm/constants.d.ts +0 -2
  14. package/dist/esm/constants.js +0 -2
  15. package/dist/esm/helperComponents/NoteItem/NoteItem.js +1 -1
  16. package/dist/esm/helperComponents/NoteItem/NoteItemSkeleton.d.ts +1 -3
  17. package/dist/esm/helperComponents/NoteItem/NoteItemSkeleton.js +2 -2
  18. package/dist/esm/helperComponents/NoteItem/styles.module.css +4 -15
  19. package/dist/esm/helperComponents/ReleaseNotesContent/ReleaseNotesContent.js +6 -9
  20. package/dist/esm/helperComponents/ReleaseNotesContent/styles.module.css +8 -0
  21. package/dist/esm/hooks.d.ts +0 -6
  22. package/dist/esm/hooks.js +1 -19
  23. package/dist/esm/style.css +12 -15
  24. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  25. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  26. package/package.json +19 -19
  27. package/src/constants.ts +0 -3
  28. package/src/helperComponents/NoteItem/NoteItem.tsx +1 -1
  29. package/src/helperComponents/NoteItem/NoteItemSkeleton.tsx +5 -13
  30. package/src/helperComponents/NoteItem/styles.module.scss +5 -21
  31. package/src/helperComponents/ReleaseNotesContent/ReleaseNotesContent.tsx +16 -10
  32. package/src/helperComponents/ReleaseNotesContent/styles.module.scss +23 -0
  33. package/src/hooks.ts +1 -25
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## 1.0.18 (2026-09-06)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **FF-8945:** align release notes with the layout and stabilize its size ([e1a32a6](https://github.com/cloud-ru-tech/snack-v2/commit/e1a32a6e5219acc6bf68f116010cbb169255f949))
11
+ - **FF-8977:** shorten CI critical path via build split and faster tests ([3ce8ae9](https://github.com/cloud-ru-tech/snack-v2/commit/3ce8ae91f9e109380a015d6a2762e8676afc87ed))
12
+
6
13
  ## 1.0.17 (2026-09-03)
7
14
 
8
15
  **Note:** Version bump only for package @ds/uikit-product-modal-predefined
@@ -13,8 +13,6 @@ export declare const SURFACE: {
13
13
  * слайдов разной длины. Переопределяется пропом `snapPoint`.
14
14
  */
15
15
  export declare const DEFAULT_RELEASE_NOTES_SNAP_POINT = 1;
16
- /** Задержка перед показом скелетона, мс. Ниже неё загрузка не успевает стать заметной. */
17
- export declare const SKELETON_DELAY_MS = 200;
18
16
  /** Состояние загрузки иллюстрации/видео в карточке новости. */
19
17
  export declare const MEDIA_STATUS: {
20
18
  readonly Pending: "pending";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TEST_IDS = exports.CONFIRM_TEXT_VARIANT = exports.MEDIA_STATUS = exports.SKELETON_DELAY_MS = exports.DEFAULT_RELEASE_NOTES_SNAP_POINT = exports.SURFACE = exports.CONTENT_STATE = void 0;
3
+ exports.TEST_IDS = exports.CONFIRM_TEXT_VARIANT = exports.MEDIA_STATUS = exports.DEFAULT_RELEASE_NOTES_SNAP_POINT = exports.SURFACE = exports.CONTENT_STATE = void 0;
4
4
  exports.CONTENT_STATE = {
5
5
  Data: 'data',
6
6
  NoData: 'noData',
@@ -16,8 +16,6 @@ exports.SURFACE = {
16
16
  * слайдов разной длины. Переопределяется пропом `snapPoint`.
17
17
  */
18
18
  exports.DEFAULT_RELEASE_NOTES_SNAP_POINT = 1;
19
- /** Задержка перед показом скелетона, мс. Ниже неё загрузка не успевает стать заметной. */
20
- exports.SKELETON_DELAY_MS = 200;
21
19
  /** Состояние загрузки иллюстрации/видео в карточке новости. */
22
20
  exports.MEDIA_STATUS = {
23
21
  Pending: 'pending',
@@ -42,5 +42,5 @@ function NoteItemMedia({ image, video }) {
42
42
  status !== constants_1.MEDIA_STATUS.Error && ((0, jsx_runtime_1.jsx)("img", { ref: handleImageRef, className: styles_module_scss_1.default.file, src: image.src, alt: image.alt, onLoad: handleImageLoad, onError: handleImageError, "data-test-id": constants_1.TEST_IDS.releaseNotesImage }))), status === constants_1.MEDIA_STATUS.Pending && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.mediaOverlay, "aria-hidden": true, children: (0, jsx_runtime_1.jsx)(skeleton_1.Skeleton, { loading: true, width: '100%', height: '100%' }) })), status === constants_1.MEDIA_STATUS.Error && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.mediaOverlay, role: 'img', "aria-label": image.alt, "data-test-id": constants_1.TEST_IDS.releaseNotesMediaFallback, children: (0, jsx_runtime_1.jsx)(system_1.ImageSVG, { className: styles_module_scss_1.default.mediaFallbackIcon, size: FALLBACK_ICON_SIZE }) }))] }));
43
43
  }
44
44
  function NoteItem({ title, description, image, video, surface = constants_1.SURFACE.Modal }) {
45
- return ((0, jsx_runtime_1.jsxs)("article", { className: styles_module_scss_1.default.root, "data-surface": surface, "data-test-id": constants_1.TEST_IDS.releaseNotesItem, children: [(0, jsx_runtime_1.jsx)(NoteItemMedia, { image: image, video: video }, `${video ?? ''}|${image.src}`), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.content, children: [(0, jsx_runtime_1.jsx)(typography_1.Typography, { variant: 'headline', size: 's', children: title }), (0, jsx_runtime_1.jsx)(scroll_1.Scroll, { className: styles_module_scss_1.default.description, barHideStrategy: scroll_1.BAR_HIDE_STRATEGY.Leave, children: (0, jsx_runtime_1.jsx)(markdown_1.Markdown, { value: description }) })] })] }));
45
+ return ((0, jsx_runtime_1.jsxs)("article", { className: styles_module_scss_1.default.root, "data-surface": surface, "data-test-id": constants_1.TEST_IDS.releaseNotesItem, children: [(0, jsx_runtime_1.jsx)(NoteItemMedia, { image: image, video: video }, `${video ?? ''}|${image.src}`), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.content, children: [(0, jsx_runtime_1.jsx)(typography_1.Typography, { className: styles_module_scss_1.default.title, variant: 'title', size: 'l', children: title }), (0, jsx_runtime_1.jsx)(scroll_1.Scroll, { className: styles_module_scss_1.default.description, barHideStrategy: scroll_1.BAR_HIDE_STRATEGY.Leave, children: (0, jsx_runtime_1.jsx)(markdown_1.Markdown, { value: description }) })] })] }));
46
46
  }
@@ -2,11 +2,9 @@ import { Surface } from '../../types';
2
2
  export type NoteItemSkeletonProps = {
3
3
  /** Поверхность отображения */
4
4
  surface?: Surface;
5
- /** Показывать ли сами плейсхолдеры. При `false` карточка занимает место, но остаётся пустой */
6
- loading?: boolean;
7
5
  };
8
6
  /**
9
7
  * Скелетон карточки новости: те же классы сетки и тот же медиа-слот, что и у `NoteItem`,
10
8
  * поэтому переход `loading → data` не меняет геометрию.
11
9
  */
12
- export declare function NoteItemSkeleton({ surface, loading }: NoteItemSkeletonProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function NoteItemSkeleton({ surface }: NoteItemSkeletonProps): import("react/jsx-runtime").JSX.Element;
@@ -15,6 +15,6 @@ const PARAGRAPH_LINES = [2, 3, 3];
15
15
  * Скелетон карточки новости: те же классы сетки и тот же медиа-слот, что и у `NoteItem`,
16
16
  * поэтому переход `loading → data` не меняет геометрию.
17
17
  */
18
- function NoteItemSkeleton({ surface = constants_1.SURFACE.Modal, loading = true }) {
19
- return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.root, "data-surface": surface, "data-loading": loading, "aria-hidden": true, "data-test-id": constants_1.TEST_IDS.releaseNotesSkeleton, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.media, "data-test-id": constants_1.TEST_IDS.releaseNotesMedia, children: (0, jsx_runtime_1.jsx)(skeleton_1.Skeleton, { loading: loading, width: '100%', height: '100%' }) }), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.content, children: [(0, jsx_runtime_1.jsx)(skeleton_1.SkeletonText, { loading: loading, lines: TITLE_LINES, variant: 'headline', size: 's' }), PARAGRAPH_LINES.map((lines, index) => ((0, jsx_runtime_1.jsx)(skeleton_1.SkeletonText, { loading: loading, lines: lines }, index)))] })] }));
18
+ function NoteItemSkeleton({ surface = constants_1.SURFACE.Modal }) {
19
+ return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.root, "data-surface": surface, "aria-hidden": true, "data-test-id": constants_1.TEST_IDS.releaseNotesSkeleton, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.media, "data-test-id": constants_1.TEST_IDS.releaseNotesMedia, children: (0, jsx_runtime_1.jsx)(skeleton_1.Skeleton, { loading: true, width: '100%', height: '100%' }) }), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.content, children: [(0, jsx_runtime_1.jsx)(skeleton_1.SkeletonText, { loading: true, lines: TITLE_LINES, variant: 'headline', size: 's' }), PARAGRAPH_LINES.map((lines, index) => ((0, jsx_runtime_1.jsx)(skeleton_1.SkeletonText, { loading: true, lines: lines }, index)))] })] }));
20
20
  }
@@ -4,15 +4,6 @@
4
4
  -moz-column-gap:var(--sn-primitive-dimension-8, 8px);
5
5
  column-gap:var(--sn-primitive-dimension-8, 8px);
6
6
  height:calc(6 * var(--sn-primitive-dimension-64, 64px) - var(--sn-primitive-dimension-4, 4px));
7
- animation:appear 0.2s ease-out;
8
- }
9
- @keyframes appear{
10
- from{
11
- opacity:0;
12
- }
13
- to{
14
- opacity:1;
15
- }
16
7
  }
17
8
  .root[data-surface=bottomSheet]{
18
9
  grid-template-columns:minmax(0, 1fr);
@@ -63,6 +54,10 @@
63
54
  min-height:0;
64
55
  }
65
56
 
57
+ .title{
58
+ color:var(--sn-theme-color-available-version-textMain, var(--sn-brand-color-text-version-main, #41424e));
59
+ }
60
+
66
61
  .description{
67
62
  flex:1;
68
63
  min-height:0;
@@ -75,10 +70,4 @@
75
70
 
76
71
  .root[data-surface=modal] .media{
77
72
  order:2;
78
- }
79
-
80
- @media (prefers-reduced-motion: reduce){
81
- .root{
82
- animation:none;
83
- }
84
73
  }
@@ -11,22 +11,19 @@ const system_1 = require("@cloud-ru/ds-icons/interface/system");
11
11
  const info_block_1 = require("@cloud-ru/ds-info-block");
12
12
  const classnames_1 = __importDefault(require("classnames"));
13
13
  const constants_1 = require("../../constants");
14
- const hooks_1 = require("../../hooks");
15
14
  const NoteItem_1 = require("../NoteItem");
16
15
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
17
16
  function ReleaseNotesContent({ contentState, items, loading, pageIndex, surface, noDataTitle, noDataDescription, errorTitle, errorDescription, retryLabel, onPageChange, onDataErrorRetryClick, }) {
18
17
  const total = items.length;
19
18
  const hasItems = total > 0;
20
- const showSkeleton = (0, hooks_1.useDelayedLoading)(loading);
21
- if (!loading && contentState === constants_1.CONTENT_STATE.Error) {
19
+ if (loading) {
20
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.root, styles_module_scss_1.default[surface]), children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.skeletonSlide, children: (0, jsx_runtime_1.jsx)(NoteItem_1.NoteItemSkeleton, { surface: surface }) }) }));
21
+ }
22
+ if (contentState === constants_1.CONTENT_STATE.Error) {
22
23
  return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.stateWrapper, "data-surface": surface, children: (0, jsx_runtime_1.jsx)(info_block_1.InfoBlock, { className: styles_module_scss_1.default.state, size: 'l', title: errorTitle, content: errorDescription, icon: { icon: system_1.CrossSVG, appearance: 'neutral', background: true }, "data-test-id": constants_1.TEST_IDS.releaseNotesError, footer: (0, jsx_runtime_1.jsx)(button_1.Button, { view: 'filled', appearance: 'neutral', size: 'l', label: retryLabel, onClick: onDataErrorRetryClick, "data-test-id": constants_1.TEST_IDS.releaseNotesRetryButton }) }) }));
23
24
  }
24
- if (!loading && (contentState === constants_1.CONTENT_STATE.NoData || !hasItems)) {
25
+ if (contentState === constants_1.CONTENT_STATE.NoData || !hasItems) {
25
26
  return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.stateWrapper, "data-surface": surface, children: (0, jsx_runtime_1.jsx)(info_block_1.InfoBlock, { className: styles_module_scss_1.default.state, size: 'l', title: noDataTitle, content: noDataDescription, "data-test-id": constants_1.TEST_IDS.releaseNotesNoData }) }));
26
27
  }
27
- // Скелетон отрисован слайдом той же карусели: у неё есть собственные отступы, и отдельная
28
- // обёртка для состояния загрузки дала бы другую высоту на переходе `loading → data`.
29
- return ((0, jsx_runtime_1.jsx)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.root, styles_module_scss_1.default[surface]), children: (0, jsx_runtime_1.jsx)(carousel_1.Carousel, { arrows: false, pagination: false, swipe: !loading, state: { page: pageIndex, onChange: onPageChange }, className: styles_module_scss_1.default.carousel, children: loading
30
- ? [(0, jsx_runtime_1.jsx)(NoteItem_1.NoteItemSkeleton, { surface: surface, loading: showSkeleton }, 'skeleton')]
31
- : items.map(item => (0, jsx_runtime_1.jsx)(NoteItem_1.NoteItem, { ...item, surface: surface }, `${item.title}-${item.image.src}`)) }) }));
28
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.root, styles_module_scss_1.default[surface]), children: (0, jsx_runtime_1.jsx)(carousel_1.Carousel, { arrows: false, pagination: false, swipe: true, state: { page: pageIndex, onChange: onPageChange }, className: styles_module_scss_1.default.carousel, children: items.map(item => ((0, jsx_runtime_1.jsx)(NoteItem_1.NoteItem, { ...item, surface: surface }, `${item.title}-${item.image.src}`))) }) }));
32
29
  }
@@ -2,6 +2,8 @@
2
2
  display:flex;
3
3
  flex-direction:column;
4
4
  row-gap:var(--sn-primitive-dimension-16, 16px);
5
+ box-sizing:border-box;
6
+ width:100%;
5
7
  }
6
8
 
7
9
  .modal{
@@ -16,6 +18,12 @@
16
18
  min-width:0;
17
19
  }
18
20
 
21
+ .skeletonSlide{
22
+ box-sizing:border-box;
23
+ width:100%;
24
+ padding:calc(var(--sn-carousel-anatomy-contentWrapper-content-paddingVertical, var(--sn-primitive-dimension-4, 4px)) + var(--sn-primitive-strokeWeight-strokeRegular, 1px)) var(--sn-primitive-strokeWeight-strokeRegular, 1px);
25
+ }
26
+
19
27
  .stateWrapper{
20
28
  display:flex;
21
29
  flex-direction:column;
@@ -23,10 +23,4 @@ export declare function useReleaseNotesNavigation({ onClose, onReadLaterClick, o
23
23
  handleNextPageClick: (totalPages: number) => void;
24
24
  handlePrevPageClick: () => void;
25
25
  };
26
- /**
27
- * Возвращает `true`, только если загрузка длится дольше `delay`. Ответ из кеша приходит за считаные
28
- * кадры, и скелетон в таком сценарии успевает мигнуть — подмена читается как рывок, хотя ждать
29
- * было нечего.
30
- */
31
- export declare function useDelayedLoading(loading?: boolean, delay?: number): boolean;
32
26
  export {};
package/dist/cjs/hooks.js CHANGED
@@ -2,9 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useTextFieldValidation = useTextFieldValidation;
4
4
  exports.useReleaseNotesNavigation = useReleaseNotesNavigation;
5
- exports.useDelayedLoading = useDelayedLoading;
6
5
  const react_1 = require("react");
7
- const constants_1 = require("./constants");
8
6
  function useTextFieldValidation({ target, errorText }) {
9
7
  const [value, setValue] = (0, react_1.useState)('');
10
8
  const [error, setError] = (0, react_1.useState)('');
@@ -59,20 +57,3 @@ function useReleaseNotesNavigation({ onClose, onReadLaterClick, onSlideChange })
59
57
  handlePrevPageClick,
60
58
  };
61
59
  }
62
- /**
63
- * Возвращает `true`, только если загрузка длится дольше `delay`. Ответ из кеша приходит за считаные
64
- * кадры, и скелетон в таком сценарии успевает мигнуть — подмена читается как рывок, хотя ждать
65
- * было нечего.
66
- */
67
- function useDelayedLoading(loading, delay = constants_1.SKELETON_DELAY_MS) {
68
- const [delayPassed, setDelayPassed] = (0, react_1.useState)(false);
69
- (0, react_1.useEffect)(() => {
70
- if (!loading) {
71
- setDelayPassed(false);
72
- return;
73
- }
74
- const timer = setTimeout(() => setDelayPassed(true), delay);
75
- return () => clearTimeout(timer);
76
- }, [loading, delay]);
77
- return Boolean(loading) && delayPassed;
78
- }
@@ -80,15 +80,6 @@
80
80
  -moz-column-gap:var(--sn-primitive-dimension-8, 8px);
81
81
  column-gap:var(--sn-primitive-dimension-8, 8px);
82
82
  height:calc(6 * var(--sn-primitive-dimension-64, 64px) - var(--sn-primitive-dimension-4, 4px));
83
- animation:appear 0.2s ease-out;
84
- }
85
- @keyframes appear{
86
- from{
87
- opacity:0;
88
- }
89
- to{
90
- opacity:1;
91
- }
92
83
  }
93
84
  .root[data-surface=bottomSheet]{
94
85
  grid-template-columns:minmax(0, 1fr);
@@ -139,6 +130,10 @@
139
130
  min-height:0;
140
131
  }
141
132
 
133
+ .title{
134
+ color:var(--sn-theme-color-available-version-textMain, var(--sn-brand-color-text-version-main, #41424e));
135
+ }
136
+
142
137
  .description{
143
138
  flex:1;
144
139
  min-height:0;
@@ -152,16 +147,12 @@
152
147
  .root[data-surface=modal] .media{
153
148
  order:2;
154
149
  }
155
-
156
- @media (prefers-reduced-motion: reduce){
157
- .root{
158
- animation:none;
159
- }
160
- }
161
150
  .root{
162
151
  display:flex;
163
152
  flex-direction:column;
164
153
  row-gap:var(--sn-primitive-dimension-16, 16px);
154
+ box-sizing:border-box;
155
+ width:100%;
165
156
  }
166
157
 
167
158
  .modal{
@@ -176,6 +167,12 @@
176
167
  min-width:0;
177
168
  }
178
169
 
170
+ .skeletonSlide{
171
+ box-sizing:border-box;
172
+ width:100%;
173
+ padding:calc(var(--sn-carousel-anatomy-contentWrapper-content-paddingVertical, var(--sn-primitive-dimension-4, 4px)) + var(--sn-primitive-strokeWeight-strokeRegular, 1px)) var(--sn-primitive-strokeWeight-strokeRegular, 1px);
174
+ }
175
+
179
176
  .stateWrapper{
180
177
  display:flex;
181
178
  flex-direction:column;
@@ -13,8 +13,6 @@ export declare const SURFACE: {
13
13
  * слайдов разной длины. Переопределяется пропом `snapPoint`.
14
14
  */
15
15
  export declare const DEFAULT_RELEASE_NOTES_SNAP_POINT = 1;
16
- /** Задержка перед показом скелетона, мс. Ниже неё загрузка не успевает стать заметной. */
17
- export declare const SKELETON_DELAY_MS = 200;
18
16
  /** Состояние загрузки иллюстрации/видео в карточке новости. */
19
17
  export declare const MEDIA_STATUS: {
20
18
  readonly Pending: "pending";
@@ -13,8 +13,6 @@ export const SURFACE = {
13
13
  * слайдов разной длины. Переопределяется пропом `snapPoint`.
14
14
  */
15
15
  export const DEFAULT_RELEASE_NOTES_SNAP_POINT = 1;
16
- /** Задержка перед показом скелетона, мс. Ниже неё загрузка не успевает стать заметной. */
17
- export const SKELETON_DELAY_MS = 200;
18
16
  /** Состояние загрузки иллюстрации/видео в карточке новости. */
19
17
  export const MEDIA_STATUS = {
20
18
  Pending: 'pending',
@@ -36,5 +36,5 @@ function NoteItemMedia({ image, video }) {
36
36
  status !== MEDIA_STATUS.Error && (_jsx("img", { ref: handleImageRef, className: styles.file, src: image.src, alt: image.alt, onLoad: handleImageLoad, onError: handleImageError, "data-test-id": TEST_IDS.releaseNotesImage }))), status === MEDIA_STATUS.Pending && (_jsx("div", { className: styles.mediaOverlay, "aria-hidden": true, children: _jsx(Skeleton, { loading: true, width: '100%', height: '100%' }) })), status === MEDIA_STATUS.Error && (_jsx("div", { className: styles.mediaOverlay, role: 'img', "aria-label": image.alt, "data-test-id": TEST_IDS.releaseNotesMediaFallback, children: _jsx(ImageSVG, { className: styles.mediaFallbackIcon, size: FALLBACK_ICON_SIZE }) }))] }));
37
37
  }
38
38
  export function NoteItem({ title, description, image, video, surface = SURFACE.Modal }) {
39
- return (_jsxs("article", { className: styles.root, "data-surface": surface, "data-test-id": TEST_IDS.releaseNotesItem, children: [_jsx(NoteItemMedia, { image: image, video: video }, `${video ?? ''}|${image.src}`), _jsxs("div", { className: styles.content, children: [_jsx(Typography, { variant: 'headline', size: 's', children: title }), _jsx(Scroll, { className: styles.description, barHideStrategy: BAR_HIDE_STRATEGY.Leave, children: _jsx(Markdown, { value: description }) })] })] }));
39
+ return (_jsxs("article", { className: styles.root, "data-surface": surface, "data-test-id": TEST_IDS.releaseNotesItem, children: [_jsx(NoteItemMedia, { image: image, video: video }, `${video ?? ''}|${image.src}`), _jsxs("div", { className: styles.content, children: [_jsx(Typography, { className: styles.title, variant: 'title', size: 'l', children: title }), _jsx(Scroll, { className: styles.description, barHideStrategy: BAR_HIDE_STRATEGY.Leave, children: _jsx(Markdown, { value: description }) })] })] }));
40
40
  }
@@ -2,11 +2,9 @@ import { Surface } from '../../types.js';
2
2
  export type NoteItemSkeletonProps = {
3
3
  /** Поверхность отображения */
4
4
  surface?: Surface;
5
- /** Показывать ли сами плейсхолдеры. При `false` карточка занимает место, но остаётся пустой */
6
- loading?: boolean;
7
5
  };
8
6
  /**
9
7
  * Скелетон карточки новости: те же классы сетки и тот же медиа-слот, что и у `NoteItem`,
10
8
  * поэтому переход `loading → data` не меняет геометрию.
11
9
  */
12
- export declare function NoteItemSkeleton({ surface, loading }: NoteItemSkeletonProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function NoteItemSkeleton({ surface }: NoteItemSkeletonProps): import("react/jsx-runtime").JSX.Element;
@@ -9,6 +9,6 @@ const PARAGRAPH_LINES = [2, 3, 3];
9
9
  * Скелетон карточки новости: те же классы сетки и тот же медиа-слот, что и у `NoteItem`,
10
10
  * поэтому переход `loading → data` не меняет геометрию.
11
11
  */
12
- export function NoteItemSkeleton({ surface = SURFACE.Modal, loading = true }) {
13
- return (_jsxs("div", { className: styles.root, "data-surface": surface, "data-loading": loading, "aria-hidden": true, "data-test-id": TEST_IDS.releaseNotesSkeleton, children: [_jsx("div", { className: styles.media, "data-test-id": TEST_IDS.releaseNotesMedia, children: _jsx(Skeleton, { loading: loading, width: '100%', height: '100%' }) }), _jsxs("div", { className: styles.content, children: [_jsx(SkeletonText, { loading: loading, lines: TITLE_LINES, variant: 'headline', size: 's' }), PARAGRAPH_LINES.map((lines, index) => (_jsx(SkeletonText, { loading: loading, lines: lines }, index)))] })] }));
12
+ export function NoteItemSkeleton({ surface = SURFACE.Modal }) {
13
+ return (_jsxs("div", { className: styles.root, "data-surface": surface, "aria-hidden": true, "data-test-id": TEST_IDS.releaseNotesSkeleton, children: [_jsx("div", { className: styles.media, "data-test-id": TEST_IDS.releaseNotesMedia, children: _jsx(Skeleton, { loading: true, width: '100%', height: '100%' }) }), _jsxs("div", { className: styles.content, children: [_jsx(SkeletonText, { loading: true, lines: TITLE_LINES, variant: 'headline', size: 's' }), PARAGRAPH_LINES.map((lines, index) => (_jsx(SkeletonText, { loading: true, lines: lines }, index)))] })] }));
14
14
  }
@@ -4,15 +4,6 @@
4
4
  -moz-column-gap:var(--sn-primitive-dimension-8, 8px);
5
5
  column-gap:var(--sn-primitive-dimension-8, 8px);
6
6
  height:calc(6 * var(--sn-primitive-dimension-64, 64px) - var(--sn-primitive-dimension-4, 4px));
7
- animation:appear 0.2s ease-out;
8
- }
9
- @keyframes appear{
10
- from{
11
- opacity:0;
12
- }
13
- to{
14
- opacity:1;
15
- }
16
7
  }
17
8
  .root[data-surface=bottomSheet]{
18
9
  grid-template-columns:minmax(0, 1fr);
@@ -63,6 +54,10 @@
63
54
  min-height:0;
64
55
  }
65
56
 
57
+ .title{
58
+ color:var(--sn-theme-color-available-version-textMain, var(--sn-brand-color-text-version-main, #41424e));
59
+ }
60
+
66
61
  .description{
67
62
  flex:1;
68
63
  min-height:0;
@@ -75,10 +70,4 @@
75
70
 
76
71
  .root[data-surface=modal] .media{
77
72
  order:2;
78
- }
79
-
80
- @media (prefers-reduced-motion: reduce){
81
- .root{
82
- animation:none;
83
- }
84
73
  }
@@ -5,22 +5,19 @@ import { CrossSVG } from '@cloud-ru/ds-icons/interface/system';
5
5
  import { InfoBlock } from '@cloud-ru/ds-info-block';
6
6
  import cn from 'classnames';
7
7
  import { CONTENT_STATE, TEST_IDS } from '../../constants.js';
8
- import { useDelayedLoading } from '../../hooks.js';
9
8
  import { NoteItem, NoteItemSkeleton } from '../NoteItem/index.js';
10
9
  import styles from './styles.module.css';
11
10
  export function ReleaseNotesContent({ contentState, items, loading, pageIndex, surface, noDataTitle, noDataDescription, errorTitle, errorDescription, retryLabel, onPageChange, onDataErrorRetryClick, }) {
12
11
  const total = items.length;
13
12
  const hasItems = total > 0;
14
- const showSkeleton = useDelayedLoading(loading);
15
- if (!loading && contentState === CONTENT_STATE.Error) {
13
+ if (loading) {
14
+ return (_jsx("div", { className: cn(styles.root, styles[surface]), children: _jsx("div", { className: styles.skeletonSlide, children: _jsx(NoteItemSkeleton, { surface: surface }) }) }));
15
+ }
16
+ if (contentState === CONTENT_STATE.Error) {
16
17
  return (_jsx("div", { className: styles.stateWrapper, "data-surface": surface, children: _jsx(InfoBlock, { className: styles.state, size: 'l', title: errorTitle, content: errorDescription, icon: { icon: CrossSVG, appearance: 'neutral', background: true }, "data-test-id": TEST_IDS.releaseNotesError, footer: _jsx(Button, { view: 'filled', appearance: 'neutral', size: 'l', label: retryLabel, onClick: onDataErrorRetryClick, "data-test-id": TEST_IDS.releaseNotesRetryButton }) }) }));
17
18
  }
18
- if (!loading && (contentState === CONTENT_STATE.NoData || !hasItems)) {
19
+ if (contentState === CONTENT_STATE.NoData || !hasItems) {
19
20
  return (_jsx("div", { className: styles.stateWrapper, "data-surface": surface, children: _jsx(InfoBlock, { className: styles.state, size: 'l', title: noDataTitle, content: noDataDescription, "data-test-id": TEST_IDS.releaseNotesNoData }) }));
20
21
  }
21
- // Скелетон отрисован слайдом той же карусели: у неё есть собственные отступы, и отдельная
22
- // обёртка для состояния загрузки дала бы другую высоту на переходе `loading → data`.
23
- return (_jsx("div", { className: cn(styles.root, styles[surface]), children: _jsx(Carousel, { arrows: false, pagination: false, swipe: !loading, state: { page: pageIndex, onChange: onPageChange }, className: styles.carousel, children: loading
24
- ? [_jsx(NoteItemSkeleton, { surface: surface, loading: showSkeleton }, 'skeleton')]
25
- : items.map(item => _jsx(NoteItem, { ...item, surface: surface }, `${item.title}-${item.image.src}`)) }) }));
22
+ return (_jsx("div", { className: cn(styles.root, styles[surface]), children: _jsx(Carousel, { arrows: false, pagination: false, swipe: true, state: { page: pageIndex, onChange: onPageChange }, className: styles.carousel, children: items.map(item => (_jsx(NoteItem, { ...item, surface: surface }, `${item.title}-${item.image.src}`))) }) }));
26
23
  }
@@ -2,6 +2,8 @@
2
2
  display:flex;
3
3
  flex-direction:column;
4
4
  row-gap:var(--sn-primitive-dimension-16, 16px);
5
+ box-sizing:border-box;
6
+ width:100%;
5
7
  }
6
8
 
7
9
  .modal{
@@ -16,6 +18,12 @@
16
18
  min-width:0;
17
19
  }
18
20
 
21
+ .skeletonSlide{
22
+ box-sizing:border-box;
23
+ width:100%;
24
+ padding:calc(var(--sn-carousel-anatomy-contentWrapper-content-paddingVertical, var(--sn-primitive-dimension-4, 4px)) + var(--sn-primitive-strokeWeight-strokeRegular, 1px)) var(--sn-primitive-strokeWeight-strokeRegular, 1px);
25
+ }
26
+
19
27
  .stateWrapper{
20
28
  display:flex;
21
29
  flex-direction:column;
@@ -23,10 +23,4 @@ export declare function useReleaseNotesNavigation({ onClose, onReadLaterClick, o
23
23
  handleNextPageClick: (totalPages: number) => void;
24
24
  handlePrevPageClick: () => void;
25
25
  };
26
- /**
27
- * Возвращает `true`, только если загрузка длится дольше `delay`. Ответ из кеша приходит за считаные
28
- * кадры, и скелетон в таком сценарии успевает мигнуть — подмена читается как рывок, хотя ждать
29
- * было нечего.
30
- */
31
- export declare function useDelayedLoading(loading?: boolean, delay?: number): boolean;
32
26
  export {};
package/dist/esm/hooks.js CHANGED
@@ -1,5 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
- import { SKELETON_DELAY_MS } from './constants.js';
1
+ import { useCallback, useState } from 'react';
3
2
  export function useTextFieldValidation({ target, errorText }) {
4
3
  const [value, setValue] = useState('');
5
4
  const [error, setError] = useState('');
@@ -54,20 +53,3 @@ export function useReleaseNotesNavigation({ onClose, onReadLaterClick, onSlideCh
54
53
  handlePrevPageClick,
55
54
  };
56
55
  }
57
- /**
58
- * Возвращает `true`, только если загрузка длится дольше `delay`. Ответ из кеша приходит за считаные
59
- * кадры, и скелетон в таком сценарии успевает мигнуть — подмена читается как рывок, хотя ждать
60
- * было нечего.
61
- */
62
- export function useDelayedLoading(loading, delay = SKELETON_DELAY_MS) {
63
- const [delayPassed, setDelayPassed] = useState(false);
64
- useEffect(() => {
65
- if (!loading) {
66
- setDelayPassed(false);
67
- return;
68
- }
69
- const timer = setTimeout(() => setDelayPassed(true), delay);
70
- return () => clearTimeout(timer);
71
- }, [loading, delay]);
72
- return Boolean(loading) && delayPassed;
73
- }
@@ -80,15 +80,6 @@
80
80
  -moz-column-gap:var(--sn-primitive-dimension-8, 8px);
81
81
  column-gap:var(--sn-primitive-dimension-8, 8px);
82
82
  height:calc(6 * var(--sn-primitive-dimension-64, 64px) - var(--sn-primitive-dimension-4, 4px));
83
- animation:appear 0.2s ease-out;
84
- }
85
- @keyframes appear{
86
- from{
87
- opacity:0;
88
- }
89
- to{
90
- opacity:1;
91
- }
92
83
  }
93
84
  .root[data-surface=bottomSheet]{
94
85
  grid-template-columns:minmax(0, 1fr);
@@ -139,6 +130,10 @@
139
130
  min-height:0;
140
131
  }
141
132
 
133
+ .title{
134
+ color:var(--sn-theme-color-available-version-textMain, var(--sn-brand-color-text-version-main, #41424e));
135
+ }
136
+
142
137
  .description{
143
138
  flex:1;
144
139
  min-height:0;
@@ -152,16 +147,12 @@
152
147
  .root[data-surface=modal] .media{
153
148
  order:2;
154
149
  }
155
-
156
- @media (prefers-reduced-motion: reduce){
157
- .root{
158
- animation:none;
159
- }
160
- }
161
150
  .root{
162
151
  display:flex;
163
152
  flex-direction:column;
164
153
  row-gap:var(--sn-primitive-dimension-16, 16px);
154
+ box-sizing:border-box;
155
+ width:100%;
165
156
  }
166
157
 
167
158
  .modal{
@@ -176,6 +167,12 @@
176
167
  min-width:0;
177
168
  }
178
169
 
170
+ .skeletonSlide{
171
+ box-sizing:border-box;
172
+ width:100%;
173
+ padding:calc(var(--sn-carousel-anatomy-contentWrapper-content-paddingVertical, var(--sn-primitive-dimension-4, 4px)) + var(--sn-primitive-strokeWeight-strokeRegular, 1px)) var(--sn-primitive-strokeWeight-strokeRegular, 1px);
174
+ }
175
+
179
176
  .stateWrapper{
180
177
  display:flex;
181
178
  flex-direction:column;