@cloud-ru/ds-uikit-product-modal-predefined 1.0.17 → 1.0.18-preview-424ddd29.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 (66) hide show
  1. package/README.md +8 -1
  2. package/dist/cjs/constants.d.ts +22 -0
  3. package/dist/cjs/constants.js +23 -1
  4. package/dist/cjs/helperComponents/DesktopReleaseNotes/DesktopReleaseNotes.js +1 -1
  5. package/dist/cjs/helperComponents/MobileReleaseNotes/MobileReleaseNotes.d.ts +1 -1
  6. package/dist/cjs/helperComponents/MobileReleaseNotes/MobileReleaseNotes.js +2 -6
  7. package/dist/cjs/helperComponents/NoteItem/NoteItem.d.ts +2 -2
  8. package/dist/cjs/helperComponents/NoteItem/NoteItem.js +31 -5
  9. package/dist/cjs/helperComponents/NoteItem/NoteItemSkeleton.d.ts +12 -0
  10. package/dist/cjs/helperComponents/NoteItem/NoteItemSkeleton.js +20 -0
  11. package/dist/cjs/helperComponents/NoteItem/index.d.ts +1 -0
  12. package/dist/cjs/helperComponents/NoteItem/index.js +1 -0
  13. package/dist/cjs/helperComponents/NoteItem/styles.module.css +40 -9
  14. package/dist/cjs/helperComponents/ReleaseNotesContent/ReleaseNotesContent.d.ts +2 -2
  15. package/dist/cjs/helperComponents/ReleaseNotesContent/ReleaseNotesContent.js +9 -7
  16. package/dist/cjs/helperComponents/ReleaseNotesContent/styles.module.css +0 -4
  17. package/dist/cjs/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.d.ts +7 -4
  18. package/dist/cjs/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.js +9 -4
  19. package/dist/cjs/helperComponents/ReleaseNotesFooter/styles.module.css +10 -9
  20. package/dist/cjs/hooks.d.ts +6 -0
  21. package/dist/cjs/hooks.js +19 -0
  22. package/dist/cjs/locale/index.d.ts +0 -4
  23. package/dist/cjs/locale/index.js +0 -2
  24. package/dist/cjs/style.css +50 -22
  25. package/dist/cjs/types.d.ts +11 -1
  26. package/dist/esm/constants.d.ts +22 -0
  27. package/dist/esm/constants.js +22 -0
  28. package/dist/esm/helperComponents/DesktopReleaseNotes/DesktopReleaseNotes.js +1 -1
  29. package/dist/esm/helperComponents/MobileReleaseNotes/MobileReleaseNotes.d.ts +1 -1
  30. package/dist/esm/helperComponents/MobileReleaseNotes/MobileReleaseNotes.js +3 -7
  31. package/dist/esm/helperComponents/NoteItem/NoteItem.d.ts +2 -2
  32. package/dist/esm/helperComponents/NoteItem/NoteItem.js +34 -8
  33. package/dist/esm/helperComponents/NoteItem/NoteItemSkeleton.d.ts +12 -0
  34. package/dist/esm/helperComponents/NoteItem/NoteItemSkeleton.js +14 -0
  35. package/dist/esm/helperComponents/NoteItem/index.d.ts +1 -0
  36. package/dist/esm/helperComponents/NoteItem/index.js +1 -0
  37. package/dist/esm/helperComponents/NoteItem/styles.module.css +40 -9
  38. package/dist/esm/helperComponents/ReleaseNotesContent/ReleaseNotesContent.d.ts +2 -2
  39. package/dist/esm/helperComponents/ReleaseNotesContent/ReleaseNotesContent.js +10 -8
  40. package/dist/esm/helperComponents/ReleaseNotesContent/styles.module.css +0 -4
  41. package/dist/esm/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.d.ts +7 -4
  42. package/dist/esm/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.js +10 -5
  43. package/dist/esm/helperComponents/ReleaseNotesFooter/styles.module.css +10 -9
  44. package/dist/esm/hooks.d.ts +6 -0
  45. package/dist/esm/hooks.js +19 -1
  46. package/dist/esm/locale/index.d.ts +0 -4
  47. package/dist/esm/locale/index.js +0 -2
  48. package/dist/esm/style.css +50 -22
  49. package/dist/esm/types.d.ts +11 -1
  50. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  51. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  52. package/package.json +19 -18
  53. package/src/constants.ts +26 -0
  54. package/src/helperComponents/DesktopReleaseNotes/DesktopReleaseNotes.tsx +2 -1
  55. package/src/helperComponents/MobileReleaseNotes/MobileReleaseNotes.tsx +5 -4
  56. package/src/helperComponents/NoteItem/NoteItem.tsx +87 -32
  57. package/src/helperComponents/NoteItem/NoteItemSkeleton.tsx +44 -0
  58. package/src/helperComponents/NoteItem/index.ts +1 -0
  59. package/src/helperComponents/NoteItem/styles.module.scss +60 -8
  60. package/src/helperComponents/ReleaseNotesContent/ReleaseNotesContent.tsx +13 -18
  61. package/src/helperComponents/ReleaseNotesContent/styles.module.scss +0 -4
  62. package/src/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.tsx +33 -10
  63. package/src/helperComponents/ReleaseNotesFooter/styles.module.scss +11 -4
  64. package/src/hooks.ts +25 -1
  65. package/src/locale/index.ts +0 -2
  66. package/src/types.ts +13 -1
package/README.md CHANGED
@@ -300,6 +300,7 @@ export function ReleaseNotesVideo() {
300
300
  | `onSlideChange` | `((slide: number) => void)` | — | Действие при смене слайда |
301
301
  | `open` | `boolean` | — | Управление состоянием показан/не показан |
302
302
  | `readLaterButtonProps` | `ButtonProps` | — | Только mobile: дополнительные пропсы action-кнопки "Ознакомиться позже" (на desktop игнорируется). |
303
+ | `snapPoint` | `SnapPoint` | `0.85` | Только mobile: фиксированная позиция bottom sheet по высоте (на desktop игнорируется). <br/> Доля вьюпорта, пиксели или viewport-единицы — см. `SnapPoint` из `@cloud-ru/ds-bottom-sheet`. |
303
304
 
304
305
  ##### Related types
305
306
 
@@ -314,6 +315,12 @@ export function ReleaseNotesVideo() {
314
315
 
315
316
  - `ReleaseNotesContentState` = `"data"` \| `"error"` \| `"noData"`
316
317
 
318
+ ### Размеры и иллюстрации
319
+
320
+ На desktop карточка новости — квадрат 380×380: иллюстрация занимает правую половину целиком, текст — остаток ширины. Размер карточки фиксирован, поэтому окно одного размера во всех состояниях и не зависит ни от длины описания, ни от того, загрузились ли данные; описание, которое не помещается, прокручивается внутри карточки. На mobile карточка растёт по контенту, а прокручивается тело sheet'а целиком — размер держит сам sheet. Его позицию задаёт проп `snapPoint` — доля вьюпорта, пиксели или viewport-единицы; по умолчанию `1`, весь экран.
321
+
322
+ Размеры медиа-слота задаёт раскладка, а не сам файл: на desktop это те же 380×380, на mobile — квадрат во всю ширину карточки. До загрузки слот занят скелетоном, при ошибке загрузки — заглушкой с иконкой. Изображение вписывается по `object-fit: cover`: всё, что выходит за границы слота, обрезается — оставляйте в иллюстрации запас по краям.
323
+
317
324
  ### Адаптивность
318
325
 
319
326
  `ReleaseNotes` — адаптивный компонент с переключением поверхности (surface-swap). Раскладку он берёт из `AdaptiveProvider` (контекст `@cloud-ru/ds-adaptive`); публичный API единый для обеих платформ:
@@ -349,7 +356,7 @@ export function ReleaseNotesVideo() {
349
356
 
350
357
  | Пропы | desktop | mobile |
351
358
  |-------|---------|--------|
352
- | `readLaterButtonProps` | игнорируется | используется |
359
+ | `readLaterButtonProps`, `snapPoint` | игнорируются | используются |
353
360
  | `open`, `onClose`, `container`, `closeOnPopstate`, `contentState`, `items`, `loading`, `onReadLaterClick`, `onDataErrorRetryClick`, `onSlideChange` | используется | используется |
354
361
 
355
362
  Подробнее о модели адаптивности — **Adaptive**.
@@ -3,6 +3,24 @@ export declare const CONTENT_STATE: {
3
3
  readonly NoData: "noData";
4
4
  readonly Error: "error";
5
5
  };
6
+ export declare const SURFACE: {
7
+ readonly Modal: "modal";
8
+ readonly BottomSheet: "bottomSheet";
9
+ };
10
+ /**
11
+ * Дефолтная позиция bottom sheet с новостями — по макету весь вьюпорт. Фиксированная позиция вместо
12
+ * высоты по контенту: иначе sheet меняет размер и на переходе `loading → data`, и при переключении
13
+ * слайдов разной длины. Переопределяется пропом `snapPoint`.
14
+ */
15
+ export declare const DEFAULT_RELEASE_NOTES_SNAP_POINT = 1;
16
+ /** Задержка перед показом скелетона, мс. Ниже неё загрузка не успевает стать заметной. */
17
+ export declare const SKELETON_DELAY_MS = 200;
18
+ /** Состояние загрузки иллюстрации/видео в карточке новости. */
19
+ export declare const MEDIA_STATUS: {
20
+ readonly Pending: "pending";
21
+ readonly Ready: "ready";
22
+ readonly Error: "error";
23
+ };
6
24
  export declare const CONFIRM_TEXT_VARIANT: {
7
25
  readonly Name: "name";
8
26
  readonly Text: "text";
@@ -17,11 +35,15 @@ export declare const TEST_IDS: {
17
35
  readonly confirmCopyButton: "modal-predefined__confirm-copy-button";
18
36
  readonly releaseNotesItem: "modal-predefined__release-notes-item";
19
37
  readonly releaseNotesMedia: "modal-predefined__release-notes-media";
38
+ readonly releaseNotesImage: "modal-predefined__release-notes-image";
39
+ readonly releaseNotesMediaFallback: "modal-predefined__release-notes-media-fallback";
40
+ readonly releaseNotesSkeleton: "modal-predefined__release-notes-skeleton";
20
41
  readonly releaseNotesVideo: "modal-predefined__release-notes-video";
21
42
  readonly releaseNotesError: "modal-predefined__release-notes-error";
22
43
  readonly releaseNotesNoData: "modal-predefined__release-notes-no-data";
23
44
  readonly releaseNotesRetryButton: "modal-predefined__release-notes-retry-button";
24
45
  readonly releaseNotesReadLaterButton: "modal-predefined__release-notes-read-later-button";
46
+ readonly releaseNotesPaginationSlider: "modal-predefined__release-notes-pagination-slider";
25
47
  readonly releaseNotesPrevButton: "modal-predefined__release-notes-prev-button";
26
48
  readonly releaseNotesNextButton: "modal-predefined__release-notes-next-button";
27
49
  };
@@ -1,11 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TEST_IDS = exports.CONFIRM_TEXT_VARIANT = exports.CONTENT_STATE = void 0;
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;
4
4
  exports.CONTENT_STATE = {
5
5
  Data: 'data',
6
6
  NoData: 'noData',
7
7
  Error: 'error',
8
8
  };
9
+ exports.SURFACE = {
10
+ Modal: 'modal',
11
+ BottomSheet: 'bottomSheet',
12
+ };
13
+ /**
14
+ * Дефолтная позиция bottom sheet с новостями — по макету весь вьюпорт. Фиксированная позиция вместо
15
+ * высоты по контенту: иначе sheet меняет размер и на переходе `loading → data`, и при переключении
16
+ * слайдов разной длины. Переопределяется пропом `snapPoint`.
17
+ */
18
+ exports.DEFAULT_RELEASE_NOTES_SNAP_POINT = 1;
19
+ /** Задержка перед показом скелетона, мс. Ниже неё загрузка не успевает стать заметной. */
20
+ exports.SKELETON_DELAY_MS = 200;
21
+ /** Состояние загрузки иллюстрации/видео в карточке новости. */
22
+ exports.MEDIA_STATUS = {
23
+ Pending: 'pending',
24
+ Ready: 'ready',
25
+ Error: 'error',
26
+ };
9
27
  exports.CONFIRM_TEXT_VARIANT = {
10
28
  Name: 'name',
11
29
  Text: 'text',
@@ -20,11 +38,15 @@ exports.TEST_IDS = {
20
38
  confirmCopyButton: 'modal-predefined__confirm-copy-button',
21
39
  releaseNotesItem: 'modal-predefined__release-notes-item',
22
40
  releaseNotesMedia: 'modal-predefined__release-notes-media',
41
+ releaseNotesImage: 'modal-predefined__release-notes-image',
42
+ releaseNotesMediaFallback: 'modal-predefined__release-notes-media-fallback',
43
+ releaseNotesSkeleton: 'modal-predefined__release-notes-skeleton',
23
44
  releaseNotesVideo: 'modal-predefined__release-notes-video',
24
45
  releaseNotesError: 'modal-predefined__release-notes-error',
25
46
  releaseNotesNoData: 'modal-predefined__release-notes-no-data',
26
47
  releaseNotesRetryButton: 'modal-predefined__release-notes-retry-button',
27
48
  releaseNotesReadLaterButton: 'modal-predefined__release-notes-read-later-button',
49
+ releaseNotesPaginationSlider: 'modal-predefined__release-notes-pagination-slider',
28
50
  releaseNotesPrevButton: 'modal-predefined__release-notes-prev-button',
29
51
  releaseNotesNextButton: 'modal-predefined__release-notes-next-button',
30
52
  };
@@ -17,5 +17,5 @@ function DesktopReleaseNotes({ open, onClose, closeOnPopstate, contentState = co
17
17
  const { t } = locale_1.modalPredefinedLocale.useTranslations();
18
18
  const { pageIndex, readablePageNumber, setPage, handleClose, handleReadLaterClick, handleNextPageClick, handlePrevPageClick, } = (0, hooks_1.useReleaseNotesNavigation)({ onClose, onReadLaterClick, onSlideChange });
19
19
  const shouldShowFooter = contentState === constants_1.CONTENT_STATE.Data && (loading || items.length > 0);
20
- return ((0, jsx_runtime_1.jsxs)(modal_1.ModalCustom, { open: open, onClose: handleClose, closeOnPopstate: closeOnPopstate, width: modal_1.WIDTH.M, "data-content-state": contentState, "data-surface": 'modal', ...(0, utils_1.extractSupportProps)(rest), children: [(0, jsx_runtime_1.jsx)(modal_1.ModalCustom.Header, { title: t('releaseNotes.title') }), (0, jsx_runtime_1.jsx)(modal_1.ModalCustom.Body, { className: styles_module_scss_1.default.body, content: (0, jsx_runtime_1.jsx)(ReleaseNotesContent_1.ReleaseNotesContent, { contentState: contentState, items: items, loading: loading, pageIndex: pageIndex, surface: 'modal', noDataTitle: t('releaseNotes.noDataTitle'), noDataDescription: t('releaseNotes.noDataDescription'), errorTitle: t('releaseNotes.errorTitle'), errorDescription: t('releaseNotes.errorDescription'), retryLabel: t('releaseNotes.retry'), onPageChange: setPage, onDataErrorRetryClick: onDataErrorRetryClick }) }), shouldShowFooter && ((0, jsx_runtime_1.jsx)(modal_1.ModalCustom.Footer, { children: (0, jsx_runtime_1.jsx)(ReleaseNotesFooter_1.ReleaseNotesFooter, { surface: 'modal', total: items.length, pageIndex: pageIndex, readablePageNumber: readablePageNumber, counterLabel: t('releaseNotes.counter', { page: readablePageNumber, total: items.length }), readLaterLabel: t('releaseNotes.readLater'), onReadLaterClick: handleReadLaterClick, onPrevPageClick: handlePrevPageClick, onNextPageClick: () => handleNextPageClick(items.length) }) }))] }));
20
+ return ((0, jsx_runtime_1.jsxs)(modal_1.ModalCustom, { open: open, onClose: handleClose, closeOnPopstate: closeOnPopstate, width: modal_1.WIDTH.M, "data-content-state": contentState, "data-surface": 'modal', ...(0, utils_1.extractSupportProps)(rest), children: [(0, jsx_runtime_1.jsx)(modal_1.ModalCustom.Header, { title: t('releaseNotes.title') }), (0, jsx_runtime_1.jsx)(modal_1.ModalCustom.Body, { className: styles_module_scss_1.default.body, content: (0, jsx_runtime_1.jsx)(ReleaseNotesContent_1.ReleaseNotesContent, { contentState: contentState, items: items, loading: loading, pageIndex: pageIndex, surface: 'modal', noDataTitle: t('releaseNotes.noDataTitle'), noDataDescription: t('releaseNotes.noDataDescription'), errorTitle: t('releaseNotes.errorTitle'), errorDescription: t('releaseNotes.errorDescription'), retryLabel: t('releaseNotes.retry'), onPageChange: setPage, onDataErrorRetryClick: onDataErrorRetryClick }) }), shouldShowFooter && ((0, jsx_runtime_1.jsx)(modal_1.ModalCustom.Footer, { children: (0, jsx_runtime_1.jsx)(ReleaseNotesFooter_1.ReleaseNotesFooter, { surface: 'modal', loading: loading, total: items.length, pageIndex: pageIndex, readablePageNumber: readablePageNumber, readLaterLabel: t('releaseNotes.readLater'), onPageChange: setPage, onReadLaterClick: handleReadLaterClick, onPrevPageClick: handlePrevPageClick, onNextPageClick: () => handleNextPageClick(items.length) }) }))] }));
21
21
  }
@@ -1,2 +1,2 @@
1
1
  import { ReleaseNotesProps } from '../../types';
2
- export declare function MobileReleaseNotes({ open, onClose, closeOnPopstate, contentState, items, loading, onReadLaterClick, onDataErrorRetryClick, onSlideChange, readLaterButtonProps, ...rest }: ReleaseNotesProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function MobileReleaseNotes({ open, onClose, closeOnPopstate, contentState, items, loading, onReadLaterClick, onDataErrorRetryClick, onSlideChange, readLaterButtonProps, snapPoint, ...rest }: ReleaseNotesProps): import("react/jsx-runtime").JSX.Element;
@@ -9,7 +9,7 @@ const hooks_1 = require("../../hooks");
9
9
  const locale_1 = require("../../locale");
10
10
  const ReleaseNotesContent_1 = require("../ReleaseNotesContent");
11
11
  const ReleaseNotesFooter_1 = require("../ReleaseNotesFooter");
12
- function MobileReleaseNotes({ open, onClose, closeOnPopstate, contentState = constants_1.CONTENT_STATE.Data, items, loading, onReadLaterClick, onDataErrorRetryClick, onSlideChange, readLaterButtonProps, ...rest }) {
12
+ function MobileReleaseNotes({ open, onClose, closeOnPopstate, contentState = constants_1.CONTENT_STATE.Data, items, loading, onReadLaterClick, onDataErrorRetryClick, onSlideChange, readLaterButtonProps, snapPoint = constants_1.DEFAULT_RELEASE_NOTES_SNAP_POINT, ...rest }) {
13
13
  const { t } = locale_1.modalPredefinedLocale.useTranslations();
14
14
  const { pageIndex, readablePageNumber, setPage, handleClose, handleReadLaterClick, handleNextPageClick, handlePrevPageClick, } = (0, hooks_1.useReleaseNotesNavigation)({ onClose, onReadLaterClick, onSlideChange });
15
15
  const { onClick: onReadLaterButtonClick, ...readLaterButtonRest } = readLaterButtonProps ?? {};
@@ -18,9 +18,5 @@ function MobileReleaseNotes({ open, onClose, closeOnPopstate, contentState = con
18
18
  onReadLaterButtonClick?.(event);
19
19
  handleReadLaterClick();
20
20
  };
21
- return ((0, jsx_runtime_1.jsx)(bottom_sheet_1.BottomSheet
22
- // Дефолт `withDividers` сменился на `true` — здесь вид сохраняем прежним.
23
- , {
24
- // Дефолт `withDividers` сменился на `true` — здесь вид сохраняем прежним.
25
- withDividers: false, open: open, onClose: handleClose, closeOnPopstate: closeOnPopstate, title: t('releaseNotes.title'), content: (0, jsx_runtime_1.jsx)(ReleaseNotesContent_1.ReleaseNotesContent, { contentState: contentState, items: items, loading: loading, pageIndex: pageIndex, surface: 'bottomSheet', noDataTitle: t('releaseNotes.noDataTitle'), noDataDescription: t('releaseNotes.noDataDescription'), errorTitle: t('releaseNotes.errorTitle'), errorDescription: t('releaseNotes.errorDescription'), retryLabel: t('releaseNotes.retry'), onPageChange: setPage, onDataErrorRetryClick: onDataErrorRetryClick }), footer: shouldShowFooter ? ((0, jsx_runtime_1.jsx)(ReleaseNotesFooter_1.ReleaseNotesFooter, { surface: 'bottomSheet', total: items.length, pageIndex: pageIndex, readablePageNumber: readablePageNumber, counterLabel: t('releaseNotes.counter', { page: readablePageNumber, total: items.length }), readLaterLabel: t('releaseNotes.readLater'), onReadLaterClick: handleReadLaterButtonClick, onPrevPageClick: handlePrevPageClick, onNextPageClick: () => handleNextPageClick(items.length), readLaterButtonProps: readLaterButtonRest })) : undefined, "data-content-state": contentState, "data-surface": 'bottomSheet', ...(0, utils_1.extractSupportProps)(rest) }));
21
+ return ((0, jsx_runtime_1.jsx)(bottom_sheet_1.BottomSheet, { open: open, onClose: handleClose, closeOnPopstate: closeOnPopstate, snapPoints: [snapPoint], title: t('releaseNotes.title'), content: (0, jsx_runtime_1.jsx)(ReleaseNotesContent_1.ReleaseNotesContent, { contentState: contentState, items: items, loading: loading, pageIndex: pageIndex, surface: 'bottomSheet', noDataTitle: t('releaseNotes.noDataTitle'), noDataDescription: t('releaseNotes.noDataDescription'), errorTitle: t('releaseNotes.errorTitle'), errorDescription: t('releaseNotes.errorDescription'), retryLabel: t('releaseNotes.retry'), onPageChange: setPage, onDataErrorRetryClick: onDataErrorRetryClick }), footer: shouldShowFooter ? ((0, jsx_runtime_1.jsx)(ReleaseNotesFooter_1.ReleaseNotesFooter, { surface: 'bottomSheet', loading: loading, total: items.length, pageIndex: pageIndex, readablePageNumber: readablePageNumber, readLaterLabel: t('releaseNotes.readLater'), onPageChange: setPage, onReadLaterClick: handleReadLaterButtonClick, onPrevPageClick: handlePrevPageClick, onNextPageClick: () => handleNextPageClick(items.length), readLaterButtonProps: readLaterButtonRest })) : undefined, "data-content-state": contentState, "data-surface": 'bottomSheet', ...(0, utils_1.extractSupportProps)(rest) }));
26
22
  }
@@ -1,6 +1,6 @@
1
- import { NoteItemProps } from '../../types';
1
+ import { NoteItemProps, Surface } from '../../types';
2
2
  type NoteItemComponentProps = NoteItemProps & {
3
- surface?: 'modal' | 'bottomSheet';
3
+ surface?: Surface;
4
4
  };
5
5
  export declare function NoteItem({ title, description, image, video, surface }: NoteItemComponentProps): import("react/jsx-runtime").JSX.Element;
6
6
  export {};
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.NoteItem = NoteItem;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const system_1 = require("@cloud-ru/ds-icons/interface/system");
8
9
  const markdown_1 = require("@cloud-ru/ds-markdown");
9
10
  const scroll_1 = require("@cloud-ru/ds-scroll");
10
11
  const skeleton_1 = require("@cloud-ru/ds-skeleton");
@@ -12,9 +13,34 @@ const typography_1 = require("@cloud-ru/ds-typography");
12
13
  const react_1 = require("react");
13
14
  const constants_1 = require("../../constants");
14
15
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
15
- function NoteItem({ title, description, image, video, surface = 'modal' }) {
16
- const [isVideoReady, setIsVideoReady] = (0, react_1.useState)(false);
17
- const [hasVideoError, setHasVideoError] = (0, react_1.useState)(false);
18
- const shouldShowVideo = Boolean(video && !hasVideoError);
19
- 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.jsxs)("div", { className: styles_module_scss_1.default.media, "data-test-id": constants_1.TEST_IDS.releaseNotesMedia, children: [shouldShowVideo && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("video", { className: styles_module_scss_1.default.video, src: video, muted: true, loop: true, playsInline: true, autoPlay: true, onLoadedData: () => setIsVideoReady(true), onError: () => setHasVideoError(true), "data-test-id": constants_1.TEST_IDS.releaseNotesVideo }), !isVideoReady && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.videoSkeleton, "aria-hidden": true, children: (0, jsx_runtime_1.jsx)(skeleton_1.Skeleton, { loading: true, width: '100%', height: '100%' }) }))] })), !shouldShowVideo && (0, jsx_runtime_1.jsx)("img", { className: styles_module_scss_1.default.image, src: image.src, alt: image.alt })] }), (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 }) })] })] }));
16
+ /** Размер иконки-заглушки в медиа-слоте, когда иллюстрация не загрузилась. */
17
+ const FALLBACK_ICON_SIZE = 48;
18
+ /**
19
+ * Медиа-слот новости: иллюстрация или видео. Размеры слота задаёт разметка, а не intrinsic-размер
20
+ * файла, поэтому загрузка не меняет раскладку карточки. До готовности файла слот занят скелетоном,
21
+ * при ошибке загрузки — заглушкой с иконкой.
22
+ */
23
+ function NoteItemMedia({ image, video }) {
24
+ const [imageStatus, setImageStatus] = (0, react_1.useState)(constants_1.MEDIA_STATUS.Pending);
25
+ const [videoStatus, setVideoStatus] = (0, react_1.useState)(constants_1.MEDIA_STATUS.Pending);
26
+ // При ошибке видео отображается статичная иллюстрация, и состояние слота определяет уже она.
27
+ const withVideo = Boolean(video) && videoStatus !== constants_1.MEDIA_STATUS.Error;
28
+ const status = withVideo ? videoStatus : imageStatus;
29
+ // Изображение из кеша может завершить загрузку до подписки на `onLoad` — состояние снимается с узла.
30
+ const handleImageRef = (0, react_1.useCallback)((node) => {
31
+ if (!node?.complete || !node.getAttribute('src')) {
32
+ return;
33
+ }
34
+ setImageStatus(node.naturalWidth > 0 ? constants_1.MEDIA_STATUS.Ready : constants_1.MEDIA_STATUS.Error);
35
+ }, []);
36
+ const handleImageLoad = (0, react_1.useCallback)(() => setImageStatus(constants_1.MEDIA_STATUS.Ready), []);
37
+ const handleImageError = (0, react_1.useCallback)(() => setImageStatus(constants_1.MEDIA_STATUS.Error), []);
38
+ const handleVideoLoad = (0, react_1.useCallback)(() => setVideoStatus(constants_1.MEDIA_STATUS.Ready), []);
39
+ const handleVideoError = (0, react_1.useCallback)(() => setVideoStatus(constants_1.MEDIA_STATUS.Error), []);
40
+ return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.media, "data-status": status, "data-test-id": constants_1.TEST_IDS.releaseNotesMedia, children: [withVideo ? ((0, jsx_runtime_1.jsx)("video", { className: styles_module_scss_1.default.file, src: video, muted: true, loop: true, playsInline: true, autoPlay: true, "aria-hidden": true, onLoadedData: handleVideoLoad, onError: handleVideoError, "data-test-id": constants_1.TEST_IDS.releaseNotesVideo })) : (
41
+ // При ошибке `<img>` не рендерится: иначе поверх заглушки остаётся превью браузера с alt-текстом.
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
+ }
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 }) })] })] }));
20
46
  }
@@ -0,0 +1,12 @@
1
+ import { Surface } from '../../types';
2
+ export type NoteItemSkeletonProps = {
3
+ /** Поверхность отображения */
4
+ surface?: Surface;
5
+ /** Показывать ли сами плейсхолдеры. При `false` карточка занимает место, но остаётся пустой */
6
+ loading?: boolean;
7
+ };
8
+ /**
9
+ * Скелетон карточки новости: те же классы сетки и тот же медиа-слот, что и у `NoteItem`,
10
+ * поэтому переход `loading → data` не меняет геометрию.
11
+ */
12
+ export declare function NoteItemSkeleton({ surface, loading }: NoteItemSkeletonProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NoteItemSkeleton = NoteItemSkeleton;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const skeleton_1 = require("@cloud-ru/ds-skeleton");
9
+ const constants_1 = require("../../constants");
10
+ const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
11
+ /** Строки скелетона текста: заголовок и абзацы описания — по структуре типовой новости. */
12
+ const TITLE_LINES = 1;
13
+ const PARAGRAPH_LINES = [2, 3, 3];
14
+ /**
15
+ * Скелетон карточки новости: те же классы сетки и тот же медиа-слот, что и у `NoteItem`,
16
+ * поэтому переход `loading → data` не меняет геометрию.
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)))] })] }));
20
+ }
@@ -1 +1,2 @@
1
1
  export * from './NoteItem';
2
+ export * from './NoteItemSkeleton';
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./NoteItem"), exports);
18
+ __exportStar(require("./NoteItemSkeleton"), exports);
@@ -1,25 +1,41 @@
1
1
  .root{
2
2
  display:grid;
3
- grid-template-columns:minmax(0, 1fr) minmax(0, 1fr);
4
- -moz-column-gap:var(--sn-primitive-dimension-24, 24px);
5
- column-gap:var(--sn-primitive-dimension-24, 24px);
6
- min-height:calc(5 * var(--sn-primitive-dimension-64, 64px));
3
+ grid-template-columns:minmax(0, 1fr) calc(6 * var(--sn-primitive-dimension-64, 64px) - var(--sn-primitive-dimension-4, 4px));
4
+ -moz-column-gap:var(--sn-primitive-dimension-8, 8px);
5
+ column-gap:var(--sn-primitive-dimension-8, 8px);
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
+ }
7
16
  }
8
17
  .root[data-surface=bottomSheet]{
9
18
  grid-template-columns:minmax(0, 1fr);
10
19
  row-gap:var(--sn-primitive-dimension-16, 16px);
11
- min-height:0;
20
+ height:auto;
12
21
  }
13
22
 
14
23
  .media{
15
24
  position:relative;
16
25
  overflow:hidden;
17
26
  min-width:0;
18
- border-radius:var(--sn-primitive-dimension-16, 16px);
27
+ border-radius:var(--sn-density-radius-2xs, var(--sn-primitive-dimension-4, 4px));
28
+ }
29
+
30
+ .media[data-status=error]{
31
+ background:var(--sn-theme-color-neutral-background, var(--sn-brand-color-neutral-90, var(--sn-primitive-color-gray-90, #eeeff3)));
32
+ }
33
+
34
+ .root[data-surface=bottomSheet] .media{
35
+ aspect-ratio:1;
19
36
  }
20
37
 
21
- .image,
22
- .video{
38
+ .file{
23
39
  display:block;
24
40
  width:100%;
25
41
  height:100%;
@@ -27,9 +43,16 @@
27
43
  object-fit:cover;
28
44
  }
29
45
 
30
- .videoSkeleton{
46
+ .mediaOverlay{
31
47
  position:absolute;
32
48
  inset:0;
49
+ display:flex;
50
+ align-items:center;
51
+ justify-content:center;
52
+ }
53
+
54
+ .mediaFallbackIcon{
55
+ color:var(--sn-theme-color-neutral-accent, var(--sn-brand-color-neutral-55, var(--sn-primitive-color-gray-55, #8b8fa7)));
33
56
  }
34
57
 
35
58
  .content{
@@ -37,9 +60,11 @@
37
60
  flex-direction:column;
38
61
  row-gap:var(--sn-primitive-dimension-12, 12px);
39
62
  min-width:0;
63
+ min-height:0;
40
64
  }
41
65
 
42
66
  .description{
67
+ flex:1;
43
68
  min-height:0;
44
69
  }
45
70
 
@@ -50,4 +75,10 @@
50
75
 
51
76
  .root[data-surface=modal] .media{
52
77
  order:2;
78
+ }
79
+
80
+ @media (prefers-reduced-motion: reduce){
81
+ .root{
82
+ animation:none;
83
+ }
53
84
  }
@@ -1,4 +1,4 @@
1
- import { NoteItemProps, ReleaseNotesContentState } from '../../types';
1
+ import { NoteItemProps, ReleaseNotesContentState, Surface } from '../../types';
2
2
  export type ReleaseNotesContentProps = {
3
3
  /** Визуальное состояние */
4
4
  contentState: ReleaseNotesContentState;
@@ -9,7 +9,7 @@ export type ReleaseNotesContentProps = {
9
9
  /** Текущая страница, zero-based */
10
10
  pageIndex: number;
11
11
  /** Поверхность */
12
- surface: 'modal' | 'bottomSheet';
12
+ surface: Surface;
13
13
  /** Заголовок no-data */
14
14
  noDataTitle: string;
15
15
  /** Описание no-data */
@@ -9,22 +9,24 @@ const button_1 = require("@cloud-ru/ds-button");
9
9
  const carousel_1 = require("@cloud-ru/ds-carousel");
10
10
  const system_1 = require("@cloud-ru/ds-icons/interface/system");
11
11
  const info_block_1 = require("@cloud-ru/ds-info-block");
12
- const skeleton_1 = require("@cloud-ru/ds-skeleton");
13
12
  const classnames_1 = __importDefault(require("classnames"));
14
13
  const constants_1 = require("../../constants");
14
+ const hooks_1 = require("../../hooks");
15
15
  const NoteItem_1 = require("../NoteItem");
16
16
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
17
17
  function ReleaseNotesContent({ contentState, items, loading, pageIndex, surface, noDataTitle, noDataDescription, errorTitle, errorDescription, retryLabel, onPageChange, onDataErrorRetryClick, }) {
18
18
  const total = items.length;
19
19
  const hasItems = total > 0;
20
- if (loading) {
21
- return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.loading, "data-surface": surface, children: (0, jsx_runtime_1.jsx)(skeleton_1.Skeleton, { loading: true, width: '100%', height: '100%', borderRadius: 'var(--border-radius-l)' }) }));
22
- }
23
- if (contentState === constants_1.CONTENT_STATE.Error) {
20
+ const showSkeleton = (0, hooks_1.useDelayedLoading)(loading);
21
+ if (!loading && contentState === constants_1.CONTENT_STATE.Error) {
24
22
  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 }) }) }));
25
23
  }
26
- if (contentState === constants_1.CONTENT_STATE.NoData || !hasItems) {
24
+ if (!loading && (contentState === constants_1.CONTENT_STATE.NoData || !hasItems)) {
27
25
  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 }) }));
28
26
  }
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: 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}`))) }) }));
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}`)) }) }));
30
32
  }
@@ -33,8 +33,4 @@
33
33
  .state{
34
34
  width:100%;
35
35
  max-width:calc(9 * var(--sn-primitive-dimension-64, 64px) + 3 * var(--sn-primitive-dimension-16, 16px));
36
- }
37
-
38
- .loading{
39
- height:calc(5 * var(--sn-primitive-dimension-64, 64px));
40
36
  }
@@ -1,16 +1,19 @@
1
1
  import { ButtonProps } from '@cloud-ru/ds-button';
2
2
  import { MouseEvent } from 'react';
3
+ import { Surface } from '../../types';
3
4
  export type ReleaseNotesFooterProps = {
4
5
  /** Поверхность отображения */
5
- surface: 'modal' | 'bottomSheet';
6
+ surface: Surface;
6
7
  /** Количество страниц */
7
8
  total: number;
9
+ /** Состояние загрузки: количество страниц ещё неизвестно */
10
+ loading?: boolean;
8
11
  /** Текущая страница, zero-based */
9
12
  pageIndex: number;
10
13
  /** Номер страницы для отображения */
11
14
  readablePageNumber: number;
12
- /** Текст счётчика «1 из N» (используется на bottomSheet) */
13
- counterLabel: string;
15
+ /** Переход на страницу по клику в слайдере (используется на modal) */
16
+ onPageChange(pageIndex: number): void;
14
17
  /** Label кнопки «Ознакомиться позже» */
15
18
  readLaterLabel: string;
16
19
  /** Действие «Ознакомиться позже» */
@@ -22,4 +25,4 @@ export type ReleaseNotesFooterProps = {
22
25
  /** Дополнительные пропсы кнопки «Ознакомиться позже» */
23
26
  readLaterButtonProps?: Partial<ButtonProps>;
24
27
  };
25
- export declare function ReleaseNotesFooter({ surface, total, pageIndex, readablePageNumber, counterLabel, readLaterLabel, onReadLaterClick, onPrevPageClick, onNextPageClick, readLaterButtonProps, }: ReleaseNotesFooterProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function ReleaseNotesFooter({ surface, total, loading, pageIndex, readablePageNumber, readLaterLabel, onReadLaterClick, onPrevPageClick, onNextPageClick, onPageChange, readLaterButtonProps, }: ReleaseNotesFooterProps): import("react/jsx-runtime").JSX.Element;
@@ -7,11 +7,16 @@ exports.ReleaseNotesFooter = ReleaseNotesFooter;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const button_1 = require("@cloud-ru/ds-button");
9
9
  const system_1 = require("@cloud-ru/ds-icons/interface/system");
10
+ const pagination_1 = require("@cloud-ru/ds-pagination");
11
+ const skeleton_1 = require("@cloud-ru/ds-skeleton");
10
12
  const constants_1 = require("../../constants");
11
13
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
12
- function ReleaseNotesFooter({ surface, total, pageIndex, readablePageNumber, counterLabel, readLaterLabel, onReadLaterClick, onPrevPageClick, onNextPageClick, readLaterButtonProps, }) {
13
- const hasPagination = total > 1;
14
- const isModal = surface === 'modal';
14
+ function ReleaseNotesFooter({ surface, total, loading, pageIndex, readablePageNumber, readLaterLabel, onReadLaterClick, onPrevPageClick, onNextPageClick, onPageChange, readLaterButtonProps, }) {
15
+ // Пока новости не загрузились, страниц ещё нет, но место под навигацию занято: иначе кнопки
16
+ // появляются рывком поверх уже отрисованного контента.
17
+ const hasPagination = loading || total > 1;
18
+ const isModal = surface === constants_1.SURFACE.Modal;
15
19
  const buttonSize = isModal ? 'm' : 'l';
16
- return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.root, "data-surface": surface, children: [(0, jsx_runtime_1.jsx)(button_1.Button, { view: 'function', appearance: 'neutral', size: buttonSize, label: readLaterLabel, "data-test-id": constants_1.TEST_IDS.releaseNotesReadLaterButton, ...readLaterButtonProps, onClick: onReadLaterClick }), hasPagination && ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.controls, children: [!isModal && (0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.counter, children: counterLabel }), (0, jsx_runtime_1.jsx)(button_1.Button, { view: 'outline', appearance: 'neutral', size: buttonSize, icon: (0, jsx_runtime_1.jsx)(system_1.ChevronLeftSVG, {}), disabled: pageIndex === 0, onClick: onPrevPageClick, "data-test-id": constants_1.TEST_IDS.releaseNotesPrevButton }), (0, jsx_runtime_1.jsx)(button_1.Button, { view: 'outline', appearance: 'neutral', size: buttonSize, icon: (0, jsx_runtime_1.jsx)(system_1.ChevronRightSVG, {}), disabled: readablePageNumber === total, onClick: onNextPageClick, "data-test-id": constants_1.TEST_IDS.releaseNotesNextButton })] }))] }));
20
+ const handleSliderChange = (page) => onPageChange(page - 1);
21
+ return ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.root, "data-surface": surface, children: [(0, jsx_runtime_1.jsx)(button_1.Button, { view: 'function', appearance: 'neutral', size: buttonSize, label: readLaterLabel, "data-test-id": constants_1.TEST_IDS.releaseNotesReadLaterButton, ...readLaterButtonProps, onClick: onReadLaterClick }), isModal && hasPagination && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.slider, children: (0, jsx_runtime_1.jsx)(skeleton_1.Skeleton, { loading: loading, className: styles_module_scss_1.default.sliderSkeleton, children: (0, jsx_runtime_1.jsx)(pagination_1.PaginationSlider, { size: pagination_1.PAGINATION_SLIDER_SIZE.Xs, total: total, page: readablePageNumber, onChange: handleSliderChange, "data-test-id": constants_1.TEST_IDS.releaseNotesPaginationSlider }) }) })), hasPagination && ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.controls, children: [(0, jsx_runtime_1.jsx)(button_1.Button, { view: 'outline', appearance: 'neutral', size: buttonSize, icon: (0, jsx_runtime_1.jsx)(system_1.ChevronLeftSVG, {}), disabled: loading || pageIndex === 0, onClick: onPrevPageClick, "data-test-id": constants_1.TEST_IDS.releaseNotesPrevButton }), (0, jsx_runtime_1.jsx)(button_1.Button, { view: 'outline', appearance: 'neutral', size: buttonSize, icon: (0, jsx_runtime_1.jsx)(system_1.ChevronRightSVG, {}), disabled: loading || readablePageNumber === total, onClick: onNextPageClick, "data-test-id": constants_1.TEST_IDS.releaseNotesNextButton })] }))] }));
17
22
  }
@@ -15,13 +15,14 @@
15
15
  align-items:center;
16
16
  }
17
17
 
18
- .counter{
19
- font-family:var(--sn-regular-body-m-fontFamily, var(--sn-brand-font-fontFamily-regular-body, var(--sn-primitive-fontFamily-brandA-sans, SB Sans Interface)));
20
- font-weight:var(--sn-regular-body-m-fontWeight, var(--sn-theme-typography-fontWeight-regular-body-m, var(--sn-brand-font-fontWeight-light-regular-body-m, var(--sn-primitive-font-fontWeightValue-400, 400))));
21
- line-height:var(--sn-regular-body-m-lineHeight, var(--sn-density-typography-roleBody-lineHeightM, var(--sn-primitive-font-lineHeight-by16, 24px)));
22
- font-size:var(--sn-regular-body-m-fontSize, var(--sn-density-typography-roleBody-fontSizeM, var(--sn-primitive-font-fontSize-16, 16px)));
23
- letter-spacing:var(--sn-regular-body-m-letterSpacing, var(--sn-primitive-font-letterSpacing-m, 0.1px));
24
- paragraph-spacing:var(--sn-regular-body-m-paragraphSpacing, var(--sn-primitive-font-paragraphSpacing-byFontSize14, 7px));
25
- color:var(--sn-theme-color-available-version-textTertiary, var(--sn-brand-color-text-version-tertiary, rgba(65, 66, 78, 0.5490196078)));
26
- white-space:nowrap;
18
+ .slider{
19
+ position:absolute;
20
+ left:50%;
21
+ transform:translateX(-50%);
22
+ }
23
+
24
+ .sliderSkeleton{
25
+ width:calc(3 * var(--sn-primitive-dimension-16, 16px));
26
+ height:var(--sn-primitive-dimension-16, 16px);
27
+ border-radius:var(--sn-density-radius-2xs, var(--sn-primitive-dimension-4, 4px));
27
28
  }
@@ -23,4 +23,10 @@ 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;
26
32
  export {};
package/dist/cjs/hooks.js CHANGED
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useTextFieldValidation = useTextFieldValidation;
4
4
  exports.useReleaseNotesNavigation = useReleaseNotesNavigation;
5
+ exports.useDelayedLoading = useDelayedLoading;
5
6
  const react_1 = require("react");
7
+ const constants_1 = require("./constants");
6
8
  function useTextFieldValidation({ target, errorText }) {
7
9
  const [value, setValue] = (0, react_1.useState)('');
8
10
  const [error, setError] = (0, react_1.useState)('');
@@ -57,3 +59,20 @@ function useReleaseNotesNavigation({ onClose, onReadLaterClick, onSlideChange })
57
59
  handlePrevPageClick,
58
60
  };
59
61
  }
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
+ }
@@ -25,7 +25,6 @@ declare const MODAL_PREDEFINED_MESSAGES: {
25
25
  readonly releaseNotes: {
26
26
  readonly title: "What is new";
27
27
  readonly readLater: "Read later";
28
- readonly counter: "{{page}} of {{total}}";
29
28
  readonly noDataTitle: "No news yet";
30
29
  readonly noDataDescription: "Platform news will appear here soon";
31
30
  readonly errorTitle: "Could not get data";
@@ -59,7 +58,6 @@ declare const MODAL_PREDEFINED_MESSAGES: {
59
58
  readonly releaseNotes: {
60
59
  readonly title: "Что нового";
61
60
  readonly readLater: "Ознакомиться позже";
62
- readonly counter: "{{page}} из {{total}}";
63
61
  readonly noDataTitle: "Новостей пока нет";
64
62
  readonly noDataDescription: "Скоро здесь появятся новости платформы";
65
63
  readonly errorTitle: "Не удалось получить данные";
@@ -97,7 +95,6 @@ export declare const modalPredefinedLocale: import("@cloud-ru/ds-locale").Define
97
95
  readonly releaseNotes: {
98
96
  readonly title: "What is new";
99
97
  readonly readLater: "Read later";
100
- readonly counter: "{{page}} of {{total}}";
101
98
  readonly noDataTitle: "No news yet";
102
99
  readonly noDataDescription: "Platform news will appear here soon";
103
100
  readonly errorTitle: "Could not get data";
@@ -131,7 +128,6 @@ export declare const modalPredefinedLocale: import("@cloud-ru/ds-locale").Define
131
128
  readonly releaseNotes: {
132
129
  readonly title: "Что нового";
133
130
  readonly readLater: "Ознакомиться позже";
134
- readonly counter: "{{page}} из {{total}}";
135
131
  readonly noDataTitle: "Новостей пока нет";
136
132
  readonly noDataDescription: "Скоро здесь появятся новости платформы";
137
133
  readonly errorTitle: "Не удалось получить данные";
@@ -29,7 +29,6 @@ const MODAL_PREDEFINED_MESSAGES = (0, locale_1.defineMessages)({
29
29
  releaseNotes: {
30
30
  title: 'What is new',
31
31
  readLater: 'Read later',
32
- counter: '{{page}} of {{total}}',
33
32
  noDataTitle: 'No news yet',
34
33
  noDataDescription: 'Platform news will appear here soon',
35
34
  errorTitle: 'Could not get data',
@@ -63,7 +62,6 @@ const MODAL_PREDEFINED_MESSAGES = (0, locale_1.defineMessages)({
63
62
  releaseNotes: {
64
63
  title: 'Что нового',
65
64
  readLater: 'Ознакомиться позже',
66
- counter: '{{page}} из {{total}}',
67
65
  noDataTitle: 'Новостей пока нет',
68
66
  noDataDescription: 'Скоро здесь появятся новости платформы',
69
67
  errorTitle: 'Не удалось получить данные',