@cloud-ru/ds-uikit-product-modal-predefined 1.0.16 → 1.0.18-preview-177d5565.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.
- package/CHANGELOG.md +4 -0
- package/README.md +4 -0
- package/dist/cjs/constants.d.ts +13 -0
- package/dist/cjs/constants.js +14 -1
- package/dist/cjs/helperComponents/NoteItem/NoteItem.d.ts +2 -2
- package/dist/cjs/helperComponents/NoteItem/NoteItem.js +31 -5
- package/dist/cjs/helperComponents/NoteItem/NoteItemSkeleton.d.ts +10 -0
- package/dist/cjs/helperComponents/NoteItem/NoteItemSkeleton.js +20 -0
- package/dist/cjs/helperComponents/NoteItem/index.d.ts +1 -0
- package/dist/cjs/helperComponents/NoteItem/index.js +1 -0
- package/dist/cjs/helperComponents/NoteItem/styles.module.css +17 -3
- package/dist/cjs/helperComponents/ReleaseNotesContent/ReleaseNotesContent.d.ts +2 -2
- package/dist/cjs/helperComponents/ReleaseNotesContent/ReleaseNotesContent.js +1 -2
- package/dist/cjs/helperComponents/ReleaseNotesContent/styles.module.css +0 -4
- package/dist/cjs/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.d.ts +2 -1
- package/dist/cjs/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.js +1 -1
- package/dist/cjs/style.css +17 -7
- package/dist/cjs/types.d.ts +4 -1
- package/dist/esm/constants.d.ts +13 -0
- package/dist/esm/constants.js +13 -0
- package/dist/esm/helperComponents/NoteItem/NoteItem.d.ts +2 -2
- package/dist/esm/helperComponents/NoteItem/NoteItem.js +34 -8
- package/dist/esm/helperComponents/NoteItem/NoteItemSkeleton.d.ts +10 -0
- package/dist/esm/helperComponents/NoteItem/NoteItemSkeleton.js +14 -0
- package/dist/esm/helperComponents/NoteItem/index.d.ts +1 -0
- package/dist/esm/helperComponents/NoteItem/index.js +1 -0
- package/dist/esm/helperComponents/NoteItem/styles.module.css +17 -3
- package/dist/esm/helperComponents/ReleaseNotesContent/ReleaseNotesContent.d.ts +2 -2
- package/dist/esm/helperComponents/ReleaseNotesContent/ReleaseNotesContent.js +2 -3
- package/dist/esm/helperComponents/ReleaseNotesContent/styles.module.css +0 -4
- package/dist/esm/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.d.ts +2 -1
- package/dist/esm/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.js +2 -2
- package/dist/esm/style.css +17 -7
- package/dist/esm/types.d.ts +4 -1
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/src/constants.ts +15 -0
- package/src/helperComponents/NoteItem/NoteItem.tsx +87 -32
- package/src/helperComponents/NoteItem/NoteItemSkeleton.tsx +36 -0
- package/src/helperComponents/NoteItem/index.ts +1 -0
- package/src/helperComponents/NoteItem/styles.module.scss +23 -3
- package/src/helperComponents/ReleaseNotesContent/ReleaseNotesContent.tsx +5 -6
- package/src/helperComponents/ReleaseNotesContent/styles.module.scss +0 -4
- package/src/helperComponents/ReleaseNotesFooter/ReleaseNotesFooter.tsx +4 -3
- package/src/types.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
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.17 (2026-09-03)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @ds/uikit-product-modal-predefined
|
|
9
|
+
|
|
6
10
|
## 1.0.16 (2026-09-03)
|
|
7
11
|
|
|
8
12
|
**Note:** Version bump only for package @ds/uikit-product-modal-predefined
|
package/README.md
CHANGED
|
@@ -314,6 +314,10 @@ export function ReleaseNotesVideo() {
|
|
|
314
314
|
|
|
315
315
|
- `ReleaseNotesContentState` = `"data"` \| `"error"` \| `"noData"`
|
|
316
316
|
|
|
317
|
+
### Иллюстрации
|
|
318
|
+
|
|
319
|
+
Размеры медиа-слота задаёт раскладка, а не сам файл: на desktop высота слота равна высоте текстовой колонки, на mobile слот сохраняет соотношение 16:10. До загрузки слот занят скелетоном, при ошибке загрузки — заглушкой с иконкой, поэтому геометрия окна не меняется. Изображение вписывается по `object-fit: cover`: всё, что выходит за границы слота, обрезается — оставляйте в иллюстрации запас по краям.
|
|
320
|
+
|
|
317
321
|
### Адаптивность
|
|
318
322
|
|
|
319
323
|
`ReleaseNotes` — адаптивный компонент с переключением поверхности (surface-swap). Раскладку он берёт из `AdaptiveProvider` (контекст `@cloud-ru/ds-adaptive`); публичный API единый для обеих платформ:
|
package/dist/cjs/constants.d.ts
CHANGED
|
@@ -3,6 +3,16 @@ 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
|
+
export declare const MEDIA_STATUS: {
|
|
12
|
+
readonly Pending: "pending";
|
|
13
|
+
readonly Ready: "ready";
|
|
14
|
+
readonly Error: "error";
|
|
15
|
+
};
|
|
6
16
|
export declare const CONFIRM_TEXT_VARIANT: {
|
|
7
17
|
readonly Name: "name";
|
|
8
18
|
readonly Text: "text";
|
|
@@ -17,6 +27,9 @@ export declare const TEST_IDS: {
|
|
|
17
27
|
readonly confirmCopyButton: "modal-predefined__confirm-copy-button";
|
|
18
28
|
readonly releaseNotesItem: "modal-predefined__release-notes-item";
|
|
19
29
|
readonly releaseNotesMedia: "modal-predefined__release-notes-media";
|
|
30
|
+
readonly releaseNotesImage: "modal-predefined__release-notes-image";
|
|
31
|
+
readonly releaseNotesMediaFallback: "modal-predefined__release-notes-media-fallback";
|
|
32
|
+
readonly releaseNotesSkeleton: "modal-predefined__release-notes-skeleton";
|
|
20
33
|
readonly releaseNotesVideo: "modal-predefined__release-notes-video";
|
|
21
34
|
readonly releaseNotesError: "modal-predefined__release-notes-error";
|
|
22
35
|
readonly releaseNotesNoData: "modal-predefined__release-notes-no-data";
|
package/dist/cjs/constants.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
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.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
|
+
exports.MEDIA_STATUS = {
|
|
15
|
+
Pending: 'pending',
|
|
16
|
+
Ready: 'ready',
|
|
17
|
+
Error: 'error',
|
|
18
|
+
};
|
|
9
19
|
exports.CONFIRM_TEXT_VARIANT = {
|
|
10
20
|
Name: 'name',
|
|
11
21
|
Text: 'text',
|
|
@@ -20,6 +30,9 @@ exports.TEST_IDS = {
|
|
|
20
30
|
confirmCopyButton: 'modal-predefined__confirm-copy-button',
|
|
21
31
|
releaseNotesItem: 'modal-predefined__release-notes-item',
|
|
22
32
|
releaseNotesMedia: 'modal-predefined__release-notes-media',
|
|
33
|
+
releaseNotesImage: 'modal-predefined__release-notes-image',
|
|
34
|
+
releaseNotesMediaFallback: 'modal-predefined__release-notes-media-fallback',
|
|
35
|
+
releaseNotesSkeleton: 'modal-predefined__release-notes-skeleton',
|
|
23
36
|
releaseNotesVideo: 'modal-predefined__release-notes-video',
|
|
24
37
|
releaseNotesError: 'modal-predefined__release-notes-error',
|
|
25
38
|
releaseNotesNoData: 'modal-predefined__release-notes-no-data',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { NoteItemProps } from '../../types';
|
|
1
|
+
import { NoteItemProps, Surface } from '../../types';
|
|
2
2
|
type NoteItemComponentProps = NoteItemProps & {
|
|
3
|
-
surface?:
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,10 @@
|
|
|
1
|
+
import { Surface } from '../../types';
|
|
2
|
+
export type NoteItemSkeletonProps = {
|
|
3
|
+
/** Поверхность отображения */
|
|
4
|
+
surface?: Surface;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Скелетон карточки новости: те же классы сетки и тот же медиа-слот, что и у `NoteItem`,
|
|
8
|
+
* поэтому переход `loading → data` не меняет геометрию.
|
|
9
|
+
*/
|
|
10
|
+
export declare function NoteItemSkeleton({ surface }: 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 }) {
|
|
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
|
+
}
|
|
@@ -18,8 +18,15 @@
|
|
|
18
18
|
border-radius:var(--sn-primitive-dimension-16, 16px);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
.
|
|
22
|
-
|
|
21
|
+
.media[data-status=error]{
|
|
22
|
+
background:var(--sn-theme-color-neutral-background, var(--sn-brand-color-neutral-90, var(--sn-primitive-color-gray-90, #eeeff3)));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.root[data-surface=bottomSheet] .media{
|
|
26
|
+
aspect-ratio:16/10;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.file{
|
|
23
30
|
display:block;
|
|
24
31
|
width:100%;
|
|
25
32
|
height:100%;
|
|
@@ -27,9 +34,16 @@
|
|
|
27
34
|
object-fit:cover;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
.
|
|
37
|
+
.mediaOverlay{
|
|
31
38
|
position:absolute;
|
|
32
39
|
inset:0;
|
|
40
|
+
display:flex;
|
|
41
|
+
align-items:center;
|
|
42
|
+
justify-content:center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mediaFallbackIcon{
|
|
46
|
+
color:var(--sn-theme-color-neutral-accent, var(--sn-brand-color-neutral-55, var(--sn-primitive-color-gray-55, #8b8fa7)));
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
.content{
|
|
@@ -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:
|
|
12
|
+
surface: Surface;
|
|
13
13
|
/** Заголовок no-data */
|
|
14
14
|
noDataTitle: string;
|
|
15
15
|
/** Описание no-data */
|
|
@@ -9,7 +9,6 @@ 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");
|
|
15
14
|
const NoteItem_1 = require("../NoteItem");
|
|
@@ -18,7 +17,7 @@ function ReleaseNotesContent({ contentState, items, loading, pageIndex, surface,
|
|
|
18
17
|
const total = items.length;
|
|
19
18
|
const hasItems = total > 0;
|
|
20
19
|
if (loading) {
|
|
21
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.
|
|
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)(NoteItem_1.NoteItemSkeleton, { surface: surface }) }));
|
|
22
21
|
}
|
|
23
22
|
if (contentState === constants_1.CONTENT_STATE.Error) {
|
|
24
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 }) }) }));
|
|
@@ -1,8 +1,9 @@
|
|
|
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:
|
|
6
|
+
surface: Surface;
|
|
6
7
|
/** Количество страниц */
|
|
7
8
|
total: number;
|
|
8
9
|
/** Текущая страница, zero-based */
|
|
@@ -11,7 +11,7 @@ const constants_1 = require("../../constants");
|
|
|
11
11
|
const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
|
|
12
12
|
function ReleaseNotesFooter({ surface, total, pageIndex, readablePageNumber, counterLabel, readLaterLabel, onReadLaterClick, onPrevPageClick, onNextPageClick, readLaterButtonProps, }) {
|
|
13
13
|
const hasPagination = total > 1;
|
|
14
|
-
const isModal = surface ===
|
|
14
|
+
const isModal = surface === constants_1.SURFACE.Modal;
|
|
15
15
|
const buttonSize = isModal ? 'm' : 'l';
|
|
16
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 })] }))] }));
|
|
17
17
|
}
|
package/dist/cjs/style.css
CHANGED
|
@@ -94,8 +94,15 @@
|
|
|
94
94
|
border-radius:var(--sn-primitive-dimension-16, 16px);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
.
|
|
98
|
-
|
|
97
|
+
.media[data-status=error]{
|
|
98
|
+
background:var(--sn-theme-color-neutral-background, var(--sn-brand-color-neutral-90, var(--sn-primitive-color-gray-90, #eeeff3)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.root[data-surface=bottomSheet] .media{
|
|
102
|
+
aspect-ratio:16/10;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.file{
|
|
99
106
|
display:block;
|
|
100
107
|
width:100%;
|
|
101
108
|
height:100%;
|
|
@@ -103,9 +110,16 @@
|
|
|
103
110
|
object-fit:cover;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
.
|
|
113
|
+
.mediaOverlay{
|
|
107
114
|
position:absolute;
|
|
108
115
|
inset:0;
|
|
116
|
+
display:flex;
|
|
117
|
+
align-items:center;
|
|
118
|
+
justify-content:center;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.mediaFallbackIcon{
|
|
122
|
+
color:var(--sn-theme-color-neutral-accent, var(--sn-brand-color-neutral-55, var(--sn-primitive-color-gray-55, #8b8fa7)));
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
.content{
|
|
@@ -163,10 +177,6 @@
|
|
|
163
177
|
width:100%;
|
|
164
178
|
max-width:calc(9 * var(--sn-primitive-dimension-64, 64px) + 3 * var(--sn-primitive-dimension-16, 16px));
|
|
165
179
|
}
|
|
166
|
-
|
|
167
|
-
.loading{
|
|
168
|
-
height:calc(5 * var(--sn-primitive-dimension-64, 64px));
|
|
169
|
-
}
|
|
170
180
|
.root{
|
|
171
181
|
position:relative;
|
|
172
182
|
display:flex;
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -2,8 +2,11 @@ import { ButtonProps } from '@cloud-ru/ds-button';
|
|
|
2
2
|
import { ModalCustomProps } from '@cloud-ru/ds-modal';
|
|
3
3
|
import { ValueOf, WithSupportProps } from '@cloud-ru/ds-utils';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
5
|
-
import { CONFIRM_TEXT_VARIANT, CONTENT_STATE } from './constants';
|
|
5
|
+
import { CONFIRM_TEXT_VARIANT, CONTENT_STATE, MEDIA_STATUS, SURFACE } from './constants';
|
|
6
6
|
export type ReleaseNotesContentState = ValueOf<typeof CONTENT_STATE>;
|
|
7
|
+
/** Поверхность отображения release notes: модальное окно (desktop) или bottom sheet (mobile). */
|
|
8
|
+
export type Surface = ValueOf<typeof SURFACE>;
|
|
9
|
+
export type MediaStatus = ValueOf<typeof MEDIA_STATUS>;
|
|
7
10
|
export type ConfirmTextVariant = ValueOf<typeof CONFIRM_TEXT_VARIANT>;
|
|
8
11
|
export type NoteItemProps = {
|
|
9
12
|
/** Заголовок новости */
|
package/dist/esm/constants.d.ts
CHANGED
|
@@ -3,6 +3,16 @@ 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
|
+
export declare const MEDIA_STATUS: {
|
|
12
|
+
readonly Pending: "pending";
|
|
13
|
+
readonly Ready: "ready";
|
|
14
|
+
readonly Error: "error";
|
|
15
|
+
};
|
|
6
16
|
export declare const CONFIRM_TEXT_VARIANT: {
|
|
7
17
|
readonly Name: "name";
|
|
8
18
|
readonly Text: "text";
|
|
@@ -17,6 +27,9 @@ export declare const TEST_IDS: {
|
|
|
17
27
|
readonly confirmCopyButton: "modal-predefined__confirm-copy-button";
|
|
18
28
|
readonly releaseNotesItem: "modal-predefined__release-notes-item";
|
|
19
29
|
readonly releaseNotesMedia: "modal-predefined__release-notes-media";
|
|
30
|
+
readonly releaseNotesImage: "modal-predefined__release-notes-image";
|
|
31
|
+
readonly releaseNotesMediaFallback: "modal-predefined__release-notes-media-fallback";
|
|
32
|
+
readonly releaseNotesSkeleton: "modal-predefined__release-notes-skeleton";
|
|
20
33
|
readonly releaseNotesVideo: "modal-predefined__release-notes-video";
|
|
21
34
|
readonly releaseNotesError: "modal-predefined__release-notes-error";
|
|
22
35
|
readonly releaseNotesNoData: "modal-predefined__release-notes-no-data";
|
package/dist/esm/constants.js
CHANGED
|
@@ -3,6 +3,16 @@ export const CONTENT_STATE = {
|
|
|
3
3
|
NoData: 'noData',
|
|
4
4
|
Error: 'error',
|
|
5
5
|
};
|
|
6
|
+
export const SURFACE = {
|
|
7
|
+
Modal: 'modal',
|
|
8
|
+
BottomSheet: 'bottomSheet',
|
|
9
|
+
};
|
|
10
|
+
/** Состояние загрузки иллюстрации/видео в карточке новости. */
|
|
11
|
+
export const MEDIA_STATUS = {
|
|
12
|
+
Pending: 'pending',
|
|
13
|
+
Ready: 'ready',
|
|
14
|
+
Error: 'error',
|
|
15
|
+
};
|
|
6
16
|
export const CONFIRM_TEXT_VARIANT = {
|
|
7
17
|
Name: 'name',
|
|
8
18
|
Text: 'text',
|
|
@@ -17,6 +27,9 @@ export const TEST_IDS = {
|
|
|
17
27
|
confirmCopyButton: 'modal-predefined__confirm-copy-button',
|
|
18
28
|
releaseNotesItem: 'modal-predefined__release-notes-item',
|
|
19
29
|
releaseNotesMedia: 'modal-predefined__release-notes-media',
|
|
30
|
+
releaseNotesImage: 'modal-predefined__release-notes-image',
|
|
31
|
+
releaseNotesMediaFallback: 'modal-predefined__release-notes-media-fallback',
|
|
32
|
+
releaseNotesSkeleton: 'modal-predefined__release-notes-skeleton',
|
|
20
33
|
releaseNotesVideo: 'modal-predefined__release-notes-video',
|
|
21
34
|
releaseNotesError: 'modal-predefined__release-notes-error',
|
|
22
35
|
releaseNotesNoData: 'modal-predefined__release-notes-no-data',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { NoteItemProps } from '../../types.js';
|
|
1
|
+
import { NoteItemProps, Surface } from '../../types.js';
|
|
2
2
|
type NoteItemComponentProps = NoteItemProps & {
|
|
3
|
-
surface?:
|
|
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 {};
|
|
@@ -1,14 +1,40 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ImageSVG } from '@cloud-ru/ds-icons/interface/system';
|
|
2
3
|
import { Markdown } from '@cloud-ru/ds-markdown';
|
|
3
4
|
import { BAR_HIDE_STRATEGY, Scroll } from '@cloud-ru/ds-scroll';
|
|
4
5
|
import { Skeleton } from '@cloud-ru/ds-skeleton';
|
|
5
6
|
import { Typography } from '@cloud-ru/ds-typography';
|
|
6
|
-
import { useState } from 'react';
|
|
7
|
-
import { TEST_IDS } from '../../constants.js';
|
|
7
|
+
import { useCallback, useState } from 'react';
|
|
8
|
+
import { MEDIA_STATUS, SURFACE, TEST_IDS } from '../../constants.js';
|
|
8
9
|
import styles from './styles.module.css';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/** Размер иконки-заглушки в медиа-слоте, когда иллюстрация не загрузилась. */
|
|
11
|
+
const FALLBACK_ICON_SIZE = 48;
|
|
12
|
+
/**
|
|
13
|
+
* Медиа-слот новости: иллюстрация или видео. Размеры слота задаёт разметка, а не intrinsic-размер
|
|
14
|
+
* файла, поэтому загрузка не меняет раскладку карточки. До готовности файла слот занят скелетоном,
|
|
15
|
+
* при ошибке загрузки — заглушкой с иконкой.
|
|
16
|
+
*/
|
|
17
|
+
function NoteItemMedia({ image, video }) {
|
|
18
|
+
const [imageStatus, setImageStatus] = useState(MEDIA_STATUS.Pending);
|
|
19
|
+
const [videoStatus, setVideoStatus] = useState(MEDIA_STATUS.Pending);
|
|
20
|
+
// При ошибке видео отображается статичная иллюстрация, и состояние слота определяет уже она.
|
|
21
|
+
const withVideo = Boolean(video) && videoStatus !== MEDIA_STATUS.Error;
|
|
22
|
+
const status = withVideo ? videoStatus : imageStatus;
|
|
23
|
+
// Изображение из кеша может завершить загрузку до подписки на `onLoad` — состояние снимается с узла.
|
|
24
|
+
const handleImageRef = useCallback((node) => {
|
|
25
|
+
if (!node?.complete || !node.getAttribute('src')) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
setImageStatus(node.naturalWidth > 0 ? MEDIA_STATUS.Ready : MEDIA_STATUS.Error);
|
|
29
|
+
}, []);
|
|
30
|
+
const handleImageLoad = useCallback(() => setImageStatus(MEDIA_STATUS.Ready), []);
|
|
31
|
+
const handleImageError = useCallback(() => setImageStatus(MEDIA_STATUS.Error), []);
|
|
32
|
+
const handleVideoLoad = useCallback(() => setVideoStatus(MEDIA_STATUS.Ready), []);
|
|
33
|
+
const handleVideoError = useCallback(() => setVideoStatus(MEDIA_STATUS.Error), []);
|
|
34
|
+
return (_jsxs("div", { className: styles.media, "data-status": status, "data-test-id": TEST_IDS.releaseNotesMedia, children: [withVideo ? (_jsx("video", { className: styles.file, src: video, muted: true, loop: true, playsInline: true, autoPlay: true, "aria-hidden": true, onLoadedData: handleVideoLoad, onError: handleVideoError, "data-test-id": TEST_IDS.releaseNotesVideo })) : (
|
|
35
|
+
// При ошибке `<img>` не рендерится: иначе поверх заглушки остаётся превью браузера с alt-текстом.
|
|
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
|
+
}
|
|
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 }) })] })] }));
|
|
14
40
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Surface } from '../../types.js';
|
|
2
|
+
export type NoteItemSkeletonProps = {
|
|
3
|
+
/** Поверхность отображения */
|
|
4
|
+
surface?: Surface;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Скелетон карточки новости: те же классы сетки и тот же медиа-слот, что и у `NoteItem`,
|
|
8
|
+
* поэтому переход `loading → data` не меняет геометрию.
|
|
9
|
+
*/
|
|
10
|
+
export declare function NoteItemSkeleton({ surface }: NoteItemSkeletonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Skeleton, SkeletonText } from '@cloud-ru/ds-skeleton';
|
|
3
|
+
import { SURFACE, TEST_IDS } from '../../constants.js';
|
|
4
|
+
import styles from './styles.module.css';
|
|
5
|
+
/** Строки скелетона текста: заголовок и абзацы описания — по структуре типовой новости. */
|
|
6
|
+
const TITLE_LINES = 1;
|
|
7
|
+
const PARAGRAPH_LINES = [2, 3, 3];
|
|
8
|
+
/**
|
|
9
|
+
* Скелетон карточки новости: те же классы сетки и тот же медиа-слот, что и у `NoteItem`,
|
|
10
|
+
* поэтому переход `loading → data` не меняет геометрию.
|
|
11
|
+
*/
|
|
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
|
+
}
|
|
@@ -18,8 +18,15 @@
|
|
|
18
18
|
border-radius:var(--sn-primitive-dimension-16, 16px);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
.
|
|
22
|
-
|
|
21
|
+
.media[data-status=error]{
|
|
22
|
+
background:var(--sn-theme-color-neutral-background, var(--sn-brand-color-neutral-90, var(--sn-primitive-color-gray-90, #eeeff3)));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.root[data-surface=bottomSheet] .media{
|
|
26
|
+
aspect-ratio:16/10;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.file{
|
|
23
30
|
display:block;
|
|
24
31
|
width:100%;
|
|
25
32
|
height:100%;
|
|
@@ -27,9 +34,16 @@
|
|
|
27
34
|
object-fit:cover;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
.
|
|
37
|
+
.mediaOverlay{
|
|
31
38
|
position:absolute;
|
|
32
39
|
inset:0;
|
|
40
|
+
display:flex;
|
|
41
|
+
align-items:center;
|
|
42
|
+
justify-content:center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mediaFallbackIcon{
|
|
46
|
+
color:var(--sn-theme-color-neutral-accent, var(--sn-brand-color-neutral-55, var(--sn-primitive-color-gray-55, #8b8fa7)));
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
.content{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NoteItemProps, ReleaseNotesContentState } from '../../types.js';
|
|
1
|
+
import { NoteItemProps, ReleaseNotesContentState, Surface } from '../../types.js';
|
|
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:
|
|
12
|
+
surface: Surface;
|
|
13
13
|
/** Заголовок no-data */
|
|
14
14
|
noDataTitle: string;
|
|
15
15
|
/** Описание no-data */
|
|
@@ -3,16 +3,15 @@ import { Button } from '@cloud-ru/ds-button';
|
|
|
3
3
|
import { Carousel } from '@cloud-ru/ds-carousel';
|
|
4
4
|
import { CrossSVG } from '@cloud-ru/ds-icons/interface/system';
|
|
5
5
|
import { InfoBlock } from '@cloud-ru/ds-info-block';
|
|
6
|
-
import { Skeleton } from '@cloud-ru/ds-skeleton';
|
|
7
6
|
import cn from 'classnames';
|
|
8
7
|
import { CONTENT_STATE, TEST_IDS } from '../../constants.js';
|
|
9
|
-
import { NoteItem } from '../NoteItem/index.js';
|
|
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
13
|
if (loading) {
|
|
15
|
-
return (_jsx("div", { className: styles.
|
|
14
|
+
return (_jsx("div", { className: cn(styles.root, styles[surface]), children: _jsx(NoteItemSkeleton, { surface: surface }) }));
|
|
16
15
|
}
|
|
17
16
|
if (contentState === CONTENT_STATE.Error) {
|
|
18
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 }) }) }));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ButtonProps } from '@cloud-ru/ds-button';
|
|
2
2
|
import { MouseEvent } from 'react';
|
|
3
|
+
import { Surface } from '../../types.js';
|
|
3
4
|
export type ReleaseNotesFooterProps = {
|
|
4
5
|
/** Поверхность отображения */
|
|
5
|
-
surface:
|
|
6
|
+
surface: Surface;
|
|
6
7
|
/** Количество страниц */
|
|
7
8
|
total: number;
|
|
8
9
|
/** Текущая страница, zero-based */
|