@pushwoosh/dumb-components 1.1.221 → 1.1.223

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 (87) hide show
  1. package/EmailTemplatePreview/__tests__/clickMap.test.js +33 -2
  2. package/EmailTemplatePreview/clickMap.d.ts +3 -1
  3. package/EmailTemplatePreview/clickMap.js +14 -3
  4. package/EmailTemplatePreview/types.d.ts +2 -0
  5. package/InboxPreview/InboxPreview.d.ts +13 -0
  6. package/InboxPreview/InboxPreview.js +82 -0
  7. package/InboxPreview/__tests__/helpers.test.d.ts +1 -0
  8. package/InboxPreview/__tests__/helpers.test.js +201 -0
  9. package/InboxPreview/components/InboxAndroidCell/InboxAndroidCell.d.ts +4 -0
  10. package/InboxPreview/components/InboxAndroidCell/InboxAndroidCell.js +70 -0
  11. package/InboxPreview/components/InboxAndroidCell/index.d.ts +1 -0
  12. package/InboxPreview/components/InboxAndroidCell/index.js +1 -0
  13. package/InboxPreview/components/InboxCellAvatar/InboxCellAvatar.d.ts +4 -0
  14. package/InboxPreview/components/InboxCellAvatar/InboxCellAvatar.js +48 -0
  15. package/InboxPreview/components/InboxCellAvatar/index.d.ts +1 -0
  16. package/InboxPreview/components/InboxCellAvatar/index.js +1 -0
  17. package/InboxPreview/components/InboxCellAvatar/styles.d.ts +1 -0
  18. package/InboxPreview/components/InboxCellAvatar/styles.js +5 -0
  19. package/InboxPreview/components/InboxCellAvatar/types.d.ts +10 -0
  20. package/InboxPreview/components/InboxCellAvatar/types.js +1 -0
  21. package/InboxPreview/components/InboxCellCarousel/InboxCellCarousel.d.ts +4 -0
  22. package/InboxPreview/components/InboxCellCarousel/InboxCellCarousel.js +53 -0
  23. package/InboxPreview/components/InboxCellCarousel/index.d.ts +1 -0
  24. package/InboxPreview/components/InboxCellCarousel/index.js +1 -0
  25. package/InboxPreview/components/InboxCellCarousel/styles.d.ts +29 -0
  26. package/InboxPreview/components/InboxCellCarousel/styles.js +24 -0
  27. package/InboxPreview/components/InboxCellCarousel/types.d.ts +8 -0
  28. package/InboxPreview/components/InboxCellCarousel/types.js +1 -0
  29. package/InboxPreview/components/InboxCellMedia/InboxCellMedia.d.ts +4 -0
  30. package/InboxPreview/components/InboxCellMedia/InboxCellMedia.js +26 -0
  31. package/InboxPreview/components/InboxCellMedia/index.d.ts +1 -0
  32. package/InboxPreview/components/InboxCellMedia/index.js +1 -0
  33. package/InboxPreview/components/InboxCellMedia/types.d.ts +12 -0
  34. package/InboxPreview/components/InboxCellMedia/types.js +1 -0
  35. package/InboxPreview/components/InboxIOSCell/InboxIOSCell.d.ts +4 -0
  36. package/InboxPreview/components/InboxIOSCell/InboxIOSCell.js +125 -0
  37. package/InboxPreview/components/InboxIOSCell/index.d.ts +1 -0
  38. package/InboxPreview/components/InboxIOSCell/index.js +1 -0
  39. package/InboxPreview/components/InboxWebCell/InboxWebCell.d.ts +4 -0
  40. package/InboxPreview/components/InboxWebCell/InboxWebCell.js +83 -0
  41. package/InboxPreview/components/InboxWebCell/index.d.ts +1 -0
  42. package/InboxPreview/components/InboxWebCell/index.js +1 -0
  43. package/InboxPreview/constants.d.ts +14 -0
  44. package/InboxPreview/constants.js +30 -0
  45. package/InboxPreview/helpers.d.ts +29 -0
  46. package/InboxPreview/helpers.js +75 -0
  47. package/InboxPreview/index.d.ts +4 -0
  48. package/InboxPreview/index.js +3 -0
  49. package/InboxPreview/polyglot/index.d.ts +2 -0
  50. package/InboxPreview/polyglot/index.js +34 -0
  51. package/InboxPreview/polyglot/messages_ar.d.ts +3 -0
  52. package/InboxPreview/polyglot/messages_ar.js +4 -0
  53. package/InboxPreview/polyglot/messages_bn.d.ts +3 -0
  54. package/InboxPreview/polyglot/messages_bn.js +4 -0
  55. package/InboxPreview/polyglot/messages_de.d.ts +3 -0
  56. package/InboxPreview/polyglot/messages_de.js +4 -0
  57. package/InboxPreview/polyglot/messages_en.d.ts +3 -0
  58. package/InboxPreview/polyglot/messages_en.js +4 -0
  59. package/InboxPreview/polyglot/messages_es.d.ts +3 -0
  60. package/InboxPreview/polyglot/messages_es.js +4 -0
  61. package/InboxPreview/polyglot/messages_fr.d.ts +3 -0
  62. package/InboxPreview/polyglot/messages_fr.js +4 -0
  63. package/InboxPreview/polyglot/messages_he.d.ts +3 -0
  64. package/InboxPreview/polyglot/messages_he.js +4 -0
  65. package/InboxPreview/polyglot/messages_hi.d.ts +3 -0
  66. package/InboxPreview/polyglot/messages_hi.js +4 -0
  67. package/InboxPreview/polyglot/messages_ja.d.ts +3 -0
  68. package/InboxPreview/polyglot/messages_ja.js +4 -0
  69. package/InboxPreview/polyglot/messages_ko.d.ts +3 -0
  70. package/InboxPreview/polyglot/messages_ko.js +4 -0
  71. package/InboxPreview/polyglot/messages_pt.d.ts +3 -0
  72. package/InboxPreview/polyglot/messages_pt.js +4 -0
  73. package/InboxPreview/polyglot/messages_ru.d.ts +3 -0
  74. package/InboxPreview/polyglot/messages_ru.js +4 -0
  75. package/InboxPreview/polyglot/messages_th.d.ts +3 -0
  76. package/InboxPreview/polyglot/messages_th.js +4 -0
  77. package/InboxPreview/polyglot/messages_zh.d.ts +3 -0
  78. package/InboxPreview/polyglot/messages_zh.js +4 -0
  79. package/InboxPreview/polyglot/types.d.ts +5 -0
  80. package/InboxPreview/polyglot/types.js +1 -0
  81. package/InboxPreview/styles.d.ts +25 -0
  82. package/InboxPreview/styles.js +21 -0
  83. package/InboxPreview/types.d.ts +74 -0
  84. package/InboxPreview/types.js +1 -0
  85. package/index.d.ts +1 -0
  86. package/index.js +1 -0
  87. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { CLICK_MAP_BADGE_ATTRIBUTE, decorateClickMap, matchClickMapItems } from '../clickMap';
2
2
  const PROMO = 'http://spartak.com/matchtowin';
3
3
  const TICKETS = 'https://tickets.spartak.com/zones/18956';
4
+ const SHOP = 'https://shop.spartak.com/';
4
5
  const HTML = `
5
6
  <a href="${PROMO}?utm_source=email&amp;utm_content=img"><img src="banner.png" alt=""></a>
6
7
  <p>Подробности на <a href="${PROMO}?utm_source=email">странице акции</a>.</p>
@@ -8,12 +9,15 @@ const HTML = `
8
9
  <a href="${TICKETS}?utm_source=email">Купить билеты</a>
9
10
  <a href="${TICKETS}?utm_source=email">Купить билеты</a>
10
11
  <a href="[% param.url_unsub %]">Отписаться</a>
12
+ <a data-pw-lid="aaaa11112222" href="${SHOP}">В магазин</a>
13
+ <a data-pw-lid="bbbb33334444" href="${SHOP}">В магазин</a>
11
14
  `;
12
- const item = (key, link, title, clicks) => ({
15
+ const item = (key, link, title, clicks, linkId) => ({
13
16
  key,
14
17
  link,
15
18
  title,
16
- clicks
19
+ clicks,
20
+ linkId
17
21
  });
18
22
  function createDocument() {
19
23
  const frameDocument = document.implementation.createHTMLDocument('preview');
@@ -64,6 +68,33 @@ describe('matchClickMapItems', () => {
64
68
  expect(placement.pooled).toEqual(['promo']);
65
69
  expect(matches).toHaveLength(3);
66
70
  });
71
+ it('tells identical anchors apart by the stamped link id', () => {
72
+ const {
73
+ matches,
74
+ placement
75
+ } = matchClickMapItems(createDocument(), [item('top', SHOP, 'В магазин', 40, 'aaaa11112222'), item('bottom', SHOP, 'В магазин', 9, 'bbbb33334444')]);
76
+ expect(placement).toEqual({
77
+ placed: ['top', 'bottom'],
78
+ pooled: [],
79
+ unplaced: []
80
+ });
81
+ expect(matches.map(({
82
+ anchor,
83
+ items: matched
84
+ }) => [anchor.getAttribute('data-pw-lid'), matched.map(row => row.key)])).toEqual([['aaaa11112222', ['top']], ['bbbb33334444', ['bottom']]]);
85
+ });
86
+ it('falls back to link and text when the template no longer carries the recorded link id', () => {
87
+ const {
88
+ matches,
89
+ placement
90
+ } = matchClickMapItems(createDocument(), [item('old', SHOP, 'В магазин', 3, 'cccc55556666'), item('unstamped', PROMO, 'странице акции', 1)]);
91
+ expect(placement).toEqual({
92
+ placed: ['unstamped'],
93
+ pooled: ['old'],
94
+ unplaced: []
95
+ });
96
+ expect(matches).toHaveLength(3);
97
+ });
67
98
  it('reports rows whose link is not in the template as unplaced', () => {
68
99
  const {
69
100
  matches,
@@ -19,7 +19,9 @@ export type EmailClickMapDecorateOptions = {
19
19
  };
20
20
  /** Attribute every badge carries, so tests and consumers can find them. */
21
21
  export declare const CLICK_MAP_BADGE_ATTRIBUTE = "data-pw-click-map-badge";
22
- /** Lands click-tracking rows on template anchors by (link, text), falling back to every anchor of the link. */
22
+ /** Stable anchor id stamped into the template html on save; the click carries it as `linkId`. */
23
+ export declare const CLICK_MAP_LINK_ID_ATTRIBUTE = "data-pw-lid";
24
+ /** Lands click-tracking rows on template anchors by stamped `linkId`, else by (link, text), else every anchor of the link. */
23
25
  export declare function matchClickMapItems(document: Document, items: EmailClickMapItem[]): EmailClickMapMatchResult;
24
26
  /** Draws click-count badges over matched anchors, outlines `activeKey`; returns a cleanup. */
25
27
  export declare function decorateClickMap(document: Document, options: EmailClickMapDecorateOptions): () => void;
@@ -1,5 +1,7 @@
1
1
  /** Attribute every badge carries, so tests and consumers can find them. */
2
2
  export const CLICK_MAP_BADGE_ATTRIBUTE = 'data-pw-click-map-badge';
3
+ /** Stable anchor id stamped into the template html on save; the click carries it as `linkId`. */
4
+ export const CLICK_MAP_LINK_ID_ATTRIBUTE = 'data-pw-lid';
3
5
  const ACTIVE_COLOR = '#00b2ff';
4
6
  const IDLE_COLOR = 'rgba(36, 48, 66, 0.82)';
5
7
  const compactNumber = new Intl.NumberFormat('en', {
@@ -10,14 +12,19 @@ const stripQuery = url => url.split('?')[0].trim();
10
12
  const normalizeText = text => text.replace(/\s+/g, ' ').trim();
11
13
  const anchorText = anchor => normalizeText(anchor.textContent ?? '');
12
14
  const anchorLink = anchor => stripQuery(anchor.getAttribute('href') ?? '');
13
- /** Lands click-tracking rows on template anchors by (link, text), falling back to every anchor of the link. */
15
+ /** Lands click-tracking rows on template anchors by stamped `linkId`, else by (link, text), else every anchor of the link. */
14
16
  export function matchClickMapItems(document, items) {
15
17
  const anchorsByLink = new Map();
18
+ const anchorsByLinkId = new Map();
16
19
  document.querySelectorAll('a[href]').forEach(anchor => {
17
20
  const link = anchorLink(anchor);
21
+ const linkId = anchor.getAttribute(CLICK_MAP_LINK_ID_ATTRIBUTE);
18
22
  if (link) {
19
23
  anchorsByLink.set(link, [...(anchorsByLink.get(link) ?? []), anchor]);
20
24
  }
25
+ if (linkId) {
26
+ anchorsByLinkId.set(linkId, [...(anchorsByLinkId.get(linkId) ?? []), anchor]);
27
+ }
21
28
  });
22
29
  const itemsByAnchor = new Map();
23
30
  const placement = {
@@ -25,11 +32,15 @@ export function matchClickMapItems(document, items) {
25
32
  pooled: [],
26
33
  unplaced: []
27
34
  };
28
- items.forEach(item => {
35
+ const targetsByLinkAndText = item => {
29
36
  const candidates = anchorsByLink.get(stripQuery(item.link)) ?? [];
30
37
  const title = normalizeText(item.title);
31
38
  const exact = candidates.filter(anchor => anchorText(anchor) === title);
32
- const targets = exact.length > 0 ? exact : candidates;
39
+ return exact.length > 0 ? exact : candidates;
40
+ };
41
+ items.forEach(item => {
42
+ const stamped = item.linkId ? anchorsByLinkId.get(item.linkId) : undefined;
43
+ const targets = stamped ?? targetsByLinkAndText(item);
33
44
  if (targets.length === 0) {
34
45
  placement.unplaced.push(item.key);
35
46
  return;
@@ -6,6 +6,8 @@ export type EmailClickMapItem = {
6
6
  link: string;
7
7
  /** Anchor text recorded with the click; empty for image links. */
8
8
  title: string;
9
+ /** Stable anchor id (`data-pw-lid`) recorded with the click; matched before link and text when set. */
10
+ linkId?: string;
9
11
  clicks: number;
10
12
  };
11
13
  /** How the rows of {@link EmailClickMapProps.items} landed on the rendered template. */
@@ -0,0 +1,13 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { InboxPreviewProps } from './types';
3
+ /**
4
+ * Renders an Inbox cell the way the SDK of the chosen platform draws it
5
+ * (`platform`: iOS card, Android flat list row, or Web widget card), with the
6
+ * SDK degradation rules: a layout that cannot render falls back to classic and
7
+ * a carousel shows at most five valid slides. Content without a single filled
8
+ * field renders the dashed placeholder of the requested layout.
9
+ *
10
+ * @example
11
+ * <InboxPreview platform="ios" localizedContent={preset.localizedContent} language="en" />
12
+ */
13
+ export declare function InboxPreview(props: InboxPreviewProps): ReactElement;
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Color, ShapeRadius } from '@pushwoosh/kit-constants';
3
+ import { Vertical } from '@pushwoosh/kit-helpers';
4
+ import { InboxAndroidCell } from './components/InboxAndroidCell';
5
+ import { InboxIOSCell } from './components/InboxIOSCell';
6
+ import { InboxWebCell } from './components/InboxWebCell';
7
+ import { INBOX_PREVIEW_MAX_WIDTH, INBOX_PREVIEW_MIN_WIDTH, INBOX_PREVIEW_SKELETON_HEIGHT } from './constants';
8
+ import { getEmptyLayout, getPreviewContent, getPreviewTexts, isEmptyContent, resolveInboxLayout } from './helpers';
9
+ import { Lang } from './polyglot';
10
+ import { RichMessageViewer } from '../RichMessageEditor';
11
+ import { LIGHT_ISLAND_PROPS } from '../shared/lightIsland';
12
+ const CELL_BY_PLATFORM = {
13
+ ios: InboxIOSCell,
14
+ android: InboxAndroidCell,
15
+ web: InboxWebCell
16
+ };
17
+ /**
18
+ * Renders an Inbox cell the way the SDK of the chosen platform draws it
19
+ * (`platform`: iOS card, Android flat list row, or Web widget card), with the
20
+ * SDK degradation rules: a layout that cannot render falls back to classic and
21
+ * a carousel shows at most five valid slides. Content without a single filled
22
+ * field renders the dashed placeholder of the requested layout.
23
+ *
24
+ * @example
25
+ * <InboxPreview platform="ios" localizedContent={preset.localizedContent} language="en" />
26
+ */
27
+ export function InboxPreview(props) {
28
+ const {
29
+ platform = 'ios',
30
+ content,
31
+ localizedContent,
32
+ language,
33
+ dateLabel = '12:12',
34
+ unread = true,
35
+ isLoading = false,
36
+ emptyTitle = _jsx(Lang, {
37
+ message: "DUMB.INBOX_PREVIEW.EMPTY_TITLE"
38
+ }),
39
+ emptyDescription = _jsx(Lang, {
40
+ message: "DUMB.INBOX_PREVIEW.EMPTY_DESCRIPTION"
41
+ }),
42
+ width = '100%'
43
+ } = props;
44
+ if (isLoading) {
45
+ return _jsx(Vertical, {
46
+ ...LIGHT_ISLAND_PROPS,
47
+ "$bgColor": Color.SOFT_LIGHT,
48
+ "$borderRadius": ShapeRadius.SECTION,
49
+ "$maxWidth": INBOX_PREVIEW_MAX_WIDTH,
50
+ "$minHeight": INBOX_PREVIEW_SKELETON_HEIGHT,
51
+ "$minWidth": INBOX_PREVIEW_MIN_WIDTH,
52
+ "$width": width
53
+ });
54
+ }
55
+ const picked = getPreviewContent(content, localizedContent, language) || {};
56
+ const isEmpty = isEmptyContent(picked);
57
+ const layout = isEmpty ? getEmptyLayout(picked) : resolveInboxLayout(picked);
58
+ const texts = getPreviewTexts(picked, layout);
59
+ const Cell = CELL_BY_PLATFORM[platform];
60
+ return _jsx(Vertical, {
61
+ ...LIGHT_ISLAND_PROPS,
62
+ "$maxWidth": INBOX_PREVIEW_MAX_WIDTH,
63
+ "$minWidth": INBOX_PREVIEW_MIN_WIDTH,
64
+ "$position": "relative",
65
+ "$width": width,
66
+ children: _jsx(Cell, {
67
+ body: isEmpty ? emptyDescription : texts.body && _jsx(RichMessageViewer, {
68
+ flat: true,
69
+ value: texts.body
70
+ }),
71
+ content: picked,
72
+ dateLabel: dateLabel,
73
+ isEmpty: isEmpty,
74
+ layout: layout,
75
+ title: isEmpty ? emptyTitle : _jsx(RichMessageViewer, {
76
+ flat: true,
77
+ value: texts.title
78
+ }),
79
+ unread: unread
80
+ })
81
+ });
82
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,201 @@
1
+ import { MAX_CAROUSEL_SLIDES } from '../constants';
2
+ import { getEmptyLayout, getPreviewContent, getPreviewTexts, getValidSlides, isEmptyContent, resolveInboxLayout } from '../helpers';
3
+ const HERO = 'https://cdn.example.com/hero.jpg';
4
+ const SLIDE = {
5
+ imageUrl: 'https://cdn.example.com/slide.jpg'
6
+ };
7
+ describe('resolveInboxLayout', () => {
8
+ it('renders the requested layout when it can', () => {
9
+ expect(resolveInboxLayout({
10
+ layoutType: 'classic',
11
+ title: 't',
12
+ body: 'b'
13
+ })).toBe('classic');
14
+ expect(resolveInboxLayout({
15
+ layoutType: 'banner',
16
+ bannerUrl: HERO
17
+ })).toBe('banner');
18
+ expect(resolveInboxLayout({
19
+ layoutType: 'captioned',
20
+ title: 't',
21
+ body: 'b',
22
+ bannerUrl: HERO
23
+ })).toBe('captioned');
24
+ expect(resolveInboxLayout({
25
+ layoutType: 'carousel',
26
+ title: 't',
27
+ body: 'b',
28
+ carousel: [SLIDE]
29
+ })).toBe('carousel');
30
+ });
31
+ it('degrades banner and captioned without a hero image into classic', () => {
32
+ expect(resolveInboxLayout({
33
+ layoutType: 'banner'
34
+ })).toBe('classic');
35
+ expect(resolveInboxLayout({
36
+ layoutType: 'captioned',
37
+ title: 't',
38
+ body: 'b'
39
+ })).toBe('classic');
40
+ });
41
+ it('degrades captioned and carousel without title or body into classic', () => {
42
+ expect(resolveInboxLayout({
43
+ layoutType: 'captioned',
44
+ body: 'b',
45
+ bannerUrl: HERO
46
+ })).toBe('classic');
47
+ expect(resolveInboxLayout({
48
+ layoutType: 'captioned',
49
+ title: 't',
50
+ bannerUrl: HERO
51
+ })).toBe('classic');
52
+ expect(resolveInboxLayout({
53
+ layoutType: 'carousel',
54
+ body: 'b',
55
+ carousel: [SLIDE]
56
+ })).toBe('classic');
57
+ expect(resolveInboxLayout({
58
+ layoutType: 'carousel',
59
+ title: 't',
60
+ carousel: [SLIDE]
61
+ })).toBe('classic');
62
+ });
63
+ it('degrades a carousel without valid slides into classic', () => {
64
+ expect(resolveInboxLayout({
65
+ layoutType: 'carousel',
66
+ title: 't',
67
+ body: 'b'
68
+ })).toBe('classic');
69
+ expect(resolveInboxLayout({
70
+ layoutType: 'carousel',
71
+ title: 't',
72
+ body: 'b',
73
+ carousel: [{
74
+ caption: 'no image'
75
+ }]
76
+ })).toBe('classic');
77
+ });
78
+ it('uses the icon as the hero fallback', () => {
79
+ expect(resolveInboxLayout({
80
+ layoutType: 'banner',
81
+ iconUrl: HERO
82
+ })).toBe('banner');
83
+ });
84
+ it('applies the SDK heuristic to unknown or absent layouts', () => {
85
+ expect(resolveInboxLayout({
86
+ bannerUrl: HERO
87
+ })).toBe('banner');
88
+ expect(resolveInboxLayout({
89
+ title: 't',
90
+ body: 'b',
91
+ bannerUrl: HERO
92
+ })).toBe('captioned');
93
+ expect(resolveInboxLayout({
94
+ title: 't',
95
+ body: 'b'
96
+ })).toBe('classic');
97
+ expect(resolveInboxLayout({
98
+ layoutType: 'video',
99
+ title: 't',
100
+ body: 'b'
101
+ })).toBe('classic');
102
+ });
103
+ });
104
+ describe('getValidSlides', () => {
105
+ it('drops slides without an image and keeps at most the SDK limit', () => {
106
+ const slides = [...Array.from({
107
+ length: MAX_CAROUSEL_SLIDES + 2
108
+ }, (_, index) => ({
109
+ imageUrl: `https://cdn.example.com/${index}.jpg`
110
+ })), {
111
+ caption: 'no image'
112
+ }];
113
+ const valid = getValidSlides(slides);
114
+ expect(valid).toHaveLength(MAX_CAROUSEL_SLIDES);
115
+ expect(valid.every(slide => !!slide.imageUrl)).toBe(true);
116
+ });
117
+ });
118
+ describe('getPreviewContent', () => {
119
+ const en = {
120
+ title: 'en'
121
+ };
122
+ const de = {
123
+ title: 'de'
124
+ };
125
+ it('prefers the explicit content over the localized map', () => {
126
+ expect(getPreviewContent(de, {
127
+ en
128
+ }, 'en')).toBe(de);
129
+ });
130
+ it('picks by language with default/en/first fallbacks', () => {
131
+ expect(getPreviewContent(null, {
132
+ en,
133
+ de
134
+ }, 'de')).toBe(de);
135
+ expect(getPreviewContent(null, {
136
+ en,
137
+ de
138
+ }, 'fr')).toBe(en);
139
+ expect(getPreviewContent(null, {
140
+ de
141
+ }, 'fr')).toBe(de);
142
+ expect(getPreviewContent(null, undefined, 'en')).toBeNull();
143
+ expect(getPreviewContent(null, {}, 'en')).toBeNull();
144
+ });
145
+ });
146
+ describe('empty state', () => {
147
+ it('detects content with nothing to render', () => {
148
+ expect(isEmptyContent({
149
+ layoutType: 'captioned'
150
+ })).toBe(true);
151
+ expect(isEmptyContent({
152
+ carousel: [{
153
+ caption: 'no image'
154
+ }]
155
+ })).toBe(true);
156
+ expect(isEmptyContent({
157
+ title: 't'
158
+ })).toBe(false);
159
+ expect(isEmptyContent({
160
+ bannerUrl: 'https://cdn.example.com/hero.jpg'
161
+ })).toBe(false);
162
+ });
163
+ it('shows the placeholder of the requested layout, classic for unknown', () => {
164
+ expect(getEmptyLayout({
165
+ layoutType: 'banner'
166
+ })).toBe('banner');
167
+ expect(getEmptyLayout({
168
+ layoutType: 'video'
169
+ })).toBe('classic');
170
+ expect(getEmptyLayout({})).toBe('classic');
171
+ });
172
+ });
173
+ describe('getPreviewTexts', () => {
174
+ it('moves the body into the title slot for classic without a title', () => {
175
+ expect(getPreviewTexts({
176
+ body: 'b'
177
+ }, 'classic')).toEqual({
178
+ title: 'b',
179
+ body: ''
180
+ });
181
+ expect(getPreviewTexts({
182
+ title: 't',
183
+ body: 'b'
184
+ }, 'classic')).toEqual({
185
+ title: 't',
186
+ body: 'b'
187
+ });
188
+ });
189
+ it('keeps the fields as-is for media layouts', () => {
190
+ expect(getPreviewTexts({
191
+ body: 'b'
192
+ }, 'captioned')).toEqual({
193
+ title: '',
194
+ body: 'b'
195
+ });
196
+ expect(getPreviewTexts({}, 'classic')).toEqual({
197
+ title: '',
198
+ body: ''
199
+ });
200
+ });
201
+ });
@@ -0,0 +1,4 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { InboxCellProps } from '../../types';
3
+ /** Android inbox cell: a flat list row with the time trailing the title. */
4
+ export declare function InboxAndroidCell(props: InboxCellProps): ReactElement;
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Color } from '@pushwoosh/kit-constants';
3
+ import { FlexContainer, Vertical } from '@pushwoosh/kit-helpers';
4
+ import { Text } from '@pushwoosh/kit-typography';
5
+ import { ClampText } from '../../styles';
6
+ import { InboxCellAvatar } from '../InboxCellAvatar';
7
+ import { InboxCellMedia } from '../InboxCellMedia';
8
+ /** Android inbox cell: a flat list row with the time trailing the title. */
9
+ export function InboxAndroidCell(props) {
10
+ const {
11
+ content,
12
+ layout,
13
+ isEmpty,
14
+ title,
15
+ body,
16
+ dateLabel
17
+ } = props;
18
+ const texts = _jsxs(Vertical, {
19
+ "$flex": 1,
20
+ "$gap": 8,
21
+ "$minWidth": 0,
22
+ children: [_jsxs(Text, {
23
+ "$color": Color.MAIN,
24
+ "$ellipsis": true,
25
+ "$variant": "footnote",
26
+ children: [title, ' ', _jsx(Text, {
27
+ "$color": Color.LOCKED,
28
+ "$variant": "footnote",
29
+ children: dateLabel
30
+ })]
31
+ }), !!body && _jsx(ClampText, {
32
+ "$clamp": 3,
33
+ "$color": Color.MAIN,
34
+ "$variant": "footnote",
35
+ children: body
36
+ })]
37
+ });
38
+ if (layout === 'classic') {
39
+ return _jsx(Vertical, {
40
+ "$bgColor": Color.CLEAR,
41
+ "$border": {
42
+ bottom: `1px solid ${Color.DIVIDER}`
43
+ },
44
+ "$gap": 16,
45
+ "$padding": 16,
46
+ children: _jsxs(FlexContainer, {
47
+ "$alignItems": "start",
48
+ "$gap": 12,
49
+ children: [_jsx(InboxCellAvatar, {
50
+ content: content,
51
+ isEmpty: isEmpty,
52
+ platform: "android"
53
+ }), texts]
54
+ })
55
+ });
56
+ }
57
+ return _jsxs(Vertical, {
58
+ "$bgColor": Color.CLEAR,
59
+ "$border": {
60
+ bottom: `1px solid ${Color.DIVIDER}`
61
+ },
62
+ "$gap": 16,
63
+ "$padding": 16,
64
+ children: [_jsx(InboxCellMedia, {
65
+ content: content,
66
+ isEmpty: isEmpty,
67
+ layout: layout
68
+ }), layout !== 'banner' && texts]
69
+ });
70
+ }
@@ -0,0 +1 @@
1
+ export { InboxAndroidCell } from './InboxAndroidCell';
@@ -0,0 +1 @@
1
+ export { InboxAndroidCell } from './InboxAndroidCell';
@@ -0,0 +1,4 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { InboxCellAvatarProps } from './types';
3
+ /** Inbox icon of a cell: the uploaded image, the colored initial the SDK falls back to, or the placeholder. */
4
+ export declare function InboxCellAvatar(props: InboxCellAvatarProps): ReactElement;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Color } from '@pushwoosh/kit-constants';
3
+ import { Horizontal } from '@pushwoosh/kit-helpers';
4
+ import { Text } from '@pushwoosh/kit-typography';
5
+ import { AvatarImage } from './styles';
6
+ import { INBOX_AVATAR } from '../../constants';
7
+ import { getIconInitial } from '../../helpers';
8
+ /** Inbox icon of a cell: the uploaded image, the colored initial the SDK falls back to, or the placeholder. */
9
+ export function InboxCellAvatar(props) {
10
+ const {
11
+ content,
12
+ isEmpty,
13
+ platform
14
+ } = props;
15
+ const {
16
+ size,
17
+ radius,
18
+ initialVariant
19
+ } = INBOX_AVATAR[platform];
20
+ if (isEmpty) {
21
+ return _jsx(Horizontal, {
22
+ "$bgColor": Color.BRIGHT_LIGHT_HOVER,
23
+ "$border": `1px dashed ${Color.BRIGHT}`,
24
+ "$borderRadius": radius,
25
+ "$height": size,
26
+ "$shrink": 0,
27
+ "$width": size
28
+ });
29
+ }
30
+ return _jsx(Horizontal, {
31
+ "$alignItems": "center",
32
+ "$bgColor": Color.PHANTOM,
33
+ "$borderRadius": radius,
34
+ "$height": size,
35
+ "$justifyContent": "center",
36
+ "$overflow": "hidden",
37
+ "$shrink": 0,
38
+ "$width": size,
39
+ children: content.iconUrl ? _jsx(AvatarImage, {
40
+ alt: content.title || '',
41
+ src: content.iconUrl
42
+ }) : _jsx(Text, {
43
+ "$color": Color.MAIN,
44
+ "$variant": initialVariant,
45
+ children: getIconInitial(content)
46
+ })
47
+ });
48
+ }
@@ -0,0 +1 @@
1
+ export { InboxCellAvatar } from './InboxCellAvatar';
@@ -0,0 +1 @@
1
+ export { InboxCellAvatar } from './InboxCellAvatar';
@@ -0,0 +1 @@
1
+ export declare const AvatarImage: import("styled-components").StyledComponent<"img", any, {}, never>;
@@ -0,0 +1,5 @@
1
+ import styled from 'styled-components';
2
+ export const AvatarImage = styled.img.withConfig({
3
+ displayName: "AvatarImage",
4
+ componentId: "sc-1mitg2x-0"
5
+ })(["width:100%;height:100%;object-fit:cover;"]);
@@ -0,0 +1,10 @@
1
+ import type { InboxPreviewContent, InboxPreviewPlatform } from '../../types';
2
+ /** Props of {@link InboxCellAvatar}. */
3
+ export interface InboxCellAvatarProps {
4
+ /** Content whose icon is drawn. */
5
+ content: InboxPreviewContent;
6
+ /** Draw the dashed placeholder instead of the icon. */
7
+ isEmpty: boolean;
8
+ /** Platform whose icon geometry is used. */
9
+ platform: InboxPreviewPlatform;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { InboxCellCarouselProps } from './types';
3
+ /** Carousel cell: the slides the SDK would keep, switched by a tap on the image or by a pager dot. */
4
+ export declare function InboxCellCarousel(props: InboxCellCarouselProps): ReactElement;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Horizontal } from '@pushwoosh/kit-helpers';
4
+ import { CarouselViewport, PagerDot, SlideCaption } from './styles';
5
+ import { getValidSlides } from '../../helpers';
6
+ import { HeroImage } from '../../styles';
7
+ /** Carousel cell: the slides the SDK would keep, switched by a tap on the image or by a pager dot. */
8
+ export function InboxCellCarousel(props) {
9
+ const {
10
+ content,
11
+ dotsOnImage = false
12
+ } = props;
13
+ const slides = getValidSlides(content.carousel);
14
+ const [index, setIndex] = useState(0);
15
+ const activeIndex = Math.min(index, slides.length - 1);
16
+ const current = slides[activeIndex];
17
+ const dots = slides.length > 1 && slides.map((_slide, slideIndex) => _jsx(PagerDot, {
18
+ "$active": slideIndex === activeIndex,
19
+ onClick: event => {
20
+ event.stopPropagation();
21
+ setIndex(slideIndex);
22
+ },
23
+ type: "button"
24
+ }, slideIndex));
25
+ return _jsxs(_Fragment, {
26
+ children: [_jsxs(CarouselViewport, {
27
+ onClick: () => setIndex((activeIndex + 1) % slides.length),
28
+ children: [_jsx(HeroImage, {
29
+ alt: current.caption || '',
30
+ src: current.imageUrl
31
+ }), !!current.caption && _jsx(SlideCaption, {
32
+ children: current.caption
33
+ }), dotsOnImage && dots && _jsx(Horizontal, {
34
+ "$alignItems": "center",
35
+ "$bottom": 8,
36
+ "$gap": 6,
37
+ "$justifyContent": "center",
38
+ "$left": 0,
39
+ "$position": "absolute",
40
+ "$right": 0,
41
+ children: dots
42
+ })]
43
+ }), !dotsOnImage && dots && _jsx(Horizontal, {
44
+ "$alignItems": "center",
45
+ "$gap": 6,
46
+ "$justifyContent": "center",
47
+ "$padding": {
48
+ top: 2
49
+ },
50
+ children: dots
51
+ })]
52
+ });
53
+ }
@@ -0,0 +1 @@
1
+ export { InboxCellCarousel } from './InboxCellCarousel';
@@ -0,0 +1 @@
1
+ export { InboxCellCarousel } from './InboxCellCarousel';