@pushwoosh/dumb-components 2.7.1 → 2.8.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 (76) hide show
  1. package/DropdownMenu/components/DropdownMenu/DropdownMenu.d.ts +5 -3
  2. package/DropdownMenu/components/DropdownMenu/DropdownMenu.js +5 -3
  3. package/DropdownMenu/components/DropdownMenuButton/DropdownMenuButton.d.ts +3 -1
  4. package/DropdownMenu/components/DropdownMenuButton/DropdownMenuButton.js +3 -1
  5. package/DropdownMenu/components/DropdownMenuItem/DropdownMenuItem.d.ts +1 -1
  6. package/DropdownMenu/components/DropdownMenuItem/DropdownMenuItem.js +1 -1
  7. package/DropdownMenu/components/DropdownMenuSection/DropdownMenuSection.d.ts +2 -1
  8. package/DropdownMenu/components/DropdownMenuSection/DropdownMenuSection.js +2 -1
  9. package/DropdownMenu/components/DropdownMenuSection/types.d.ts +1 -1
  10. package/DropdownMenu/components/DropdownMenuSubmenu/DropdownMenuSubmenu.d.ts +6 -4
  11. package/DropdownMenu/components/DropdownMenuSubmenu/DropdownMenuSubmenu.js +6 -4
  12. package/PushStoriesPreview/PushStoriesPreview.d.ts +5 -0
  13. package/PushStoriesPreview/PushStoriesPreview.js +71 -0
  14. package/PushStoriesPreview/__tests__/PushStoriesPreview.test.d.ts +1 -0
  15. package/PushStoriesPreview/__tests__/PushStoriesPreview.test.js +234 -0
  16. package/PushStoriesPreview/components/PushStoriesPage/PushStoriesPage.d.ts +4 -0
  17. package/PushStoriesPreview/components/PushStoriesPage/PushStoriesPage.js +38 -0
  18. package/PushStoriesPreview/components/PushStoriesPage/index.d.ts +1 -0
  19. package/PushStoriesPreview/components/PushStoriesPage/index.js +1 -0
  20. package/PushStoriesPreview/components/PushStoriesPage/styles.d.ts +7 -0
  21. package/PushStoriesPreview/components/PushStoriesPage/styles.js +31 -0
  22. package/PushStoriesPreview/components/PushStoriesPage/types.d.ts +6 -0
  23. package/PushStoriesPreview/components/PushStoriesPage/types.js +1 -0
  24. package/PushStoriesPreview/components/PushStoriesProgress/PushStoriesProgress.d.ts +4 -0
  25. package/PushStoriesPreview/components/PushStoriesProgress/PushStoriesProgress.js +24 -0
  26. package/PushStoriesPreview/components/PushStoriesProgress/index.d.ts +1 -0
  27. package/PushStoriesPreview/components/PushStoriesProgress/index.js +1 -0
  28. package/PushStoriesPreview/components/PushStoriesProgress/styles.d.ts +5 -0
  29. package/PushStoriesPreview/components/PushStoriesProgress/styles.js +17 -0
  30. package/PushStoriesPreview/components/PushStoriesProgress/types.d.ts +15 -0
  31. package/PushStoriesPreview/components/PushStoriesProgress/types.js +1 -0
  32. package/PushStoriesPreview/constants.d.ts +8 -0
  33. package/PushStoriesPreview/constants.js +8 -0
  34. package/PushStoriesPreview/helpers.d.ts +7 -0
  35. package/PushStoriesPreview/helpers.js +19 -0
  36. package/PushStoriesPreview/index.d.ts +4 -0
  37. package/PushStoriesPreview/index.js +3 -0
  38. package/PushStoriesPreview/polyglot/index.d.ts +2 -0
  39. package/PushStoriesPreview/polyglot/index.js +34 -0
  40. package/PushStoriesPreview/polyglot/messages_ar.d.ts +3 -0
  41. package/PushStoriesPreview/polyglot/messages_ar.js +4 -0
  42. package/PushStoriesPreview/polyglot/messages_bn.d.ts +3 -0
  43. package/PushStoriesPreview/polyglot/messages_bn.js +4 -0
  44. package/PushStoriesPreview/polyglot/messages_de.d.ts +3 -0
  45. package/PushStoriesPreview/polyglot/messages_de.js +4 -0
  46. package/PushStoriesPreview/polyglot/messages_en.d.ts +3 -0
  47. package/PushStoriesPreview/polyglot/messages_en.js +4 -0
  48. package/PushStoriesPreview/polyglot/messages_es.d.ts +3 -0
  49. package/PushStoriesPreview/polyglot/messages_es.js +4 -0
  50. package/PushStoriesPreview/polyglot/messages_fr.d.ts +3 -0
  51. package/PushStoriesPreview/polyglot/messages_fr.js +4 -0
  52. package/PushStoriesPreview/polyglot/messages_he.d.ts +3 -0
  53. package/PushStoriesPreview/polyglot/messages_he.js +4 -0
  54. package/PushStoriesPreview/polyglot/messages_hi.d.ts +3 -0
  55. package/PushStoriesPreview/polyglot/messages_hi.js +4 -0
  56. package/PushStoriesPreview/polyglot/messages_ja.d.ts +3 -0
  57. package/PushStoriesPreview/polyglot/messages_ja.js +4 -0
  58. package/PushStoriesPreview/polyglot/messages_ko.d.ts +3 -0
  59. package/PushStoriesPreview/polyglot/messages_ko.js +4 -0
  60. package/PushStoriesPreview/polyglot/messages_pt.d.ts +3 -0
  61. package/PushStoriesPreview/polyglot/messages_pt.js +4 -0
  62. package/PushStoriesPreview/polyglot/messages_ru.d.ts +3 -0
  63. package/PushStoriesPreview/polyglot/messages_ru.js +4 -0
  64. package/PushStoriesPreview/polyglot/messages_th.d.ts +3 -0
  65. package/PushStoriesPreview/polyglot/messages_th.js +4 -0
  66. package/PushStoriesPreview/polyglot/messages_zh.d.ts +3 -0
  67. package/PushStoriesPreview/polyglot/messages_zh.js +4 -0
  68. package/PushStoriesPreview/polyglot/types.d.ts +5 -0
  69. package/PushStoriesPreview/polyglot/types.js +1 -0
  70. package/PushStoriesPreview/styles.d.ts +6 -0
  71. package/PushStoriesPreview/styles.js +23 -0
  72. package/PushStoriesPreview/types.d.ts +32 -0
  73. package/PushStoriesPreview/types.js +1 -0
  74. package/index.d.ts +1 -0
  75. package/index.js +1 -0
  76. package/package.json +1 -1
@@ -2,14 +2,16 @@ import { type FC } from 'react';
2
2
  import { type DropdownMenuProps } from './types';
3
3
  /**
4
4
  * Floating dropdown menu anchored to a custom trigger (`renderHandler`), with
5
- * auto-placement, click-outside close and layered z-index. Fill it with
6
- * {@link DropdownMenuItem}, {@link DropdownMenuSection} and {@link DropdownMenuSubmenu}.
5
+ * auto-placement, click-outside close and layered z-index. Children are {@link DropdownMenuSection}s
6
+ * holding {@link DropdownMenuItem}s and {@link DropdownMenuSubmenu}s — the surface itself has no padding.
7
7
  *
8
8
  * @example
9
9
  * <DropdownMenu renderHandler={({ ref, onClick, isOpen }) => (
10
10
  * <Button boxRef={ref} onClick={onClick} isOpen={isOpen} color="secondary">Menu</Button>
11
11
  * )}>
12
- * <DropdownMenuItem onClick={edit}>Edit</DropdownMenuItem>
12
+ * <DropdownMenuSection>
13
+ * <DropdownMenuItem onClick={edit}>Edit</DropdownMenuItem>
14
+ * </DropdownMenuSection>
13
15
  * </DropdownMenu>
14
16
  */
15
17
  export declare const DropdownMenu: FC<DropdownMenuProps>;
@@ -10,14 +10,16 @@ import { useSubmenuRegistry } from '../../shared/useSubmenuRegistry';
10
10
  const defaultPlacements = ['top-start', 'top-end', 'bottom-start', 'bottom-end'];
11
11
  /**
12
12
  * Floating dropdown menu anchored to a custom trigger (`renderHandler`), with
13
- * auto-placement, click-outside close and layered z-index. Fill it with
14
- * {@link DropdownMenuItem}, {@link DropdownMenuSection} and {@link DropdownMenuSubmenu}.
13
+ * auto-placement, click-outside close and layered z-index. Children are {@link DropdownMenuSection}s
14
+ * holding {@link DropdownMenuItem}s and {@link DropdownMenuSubmenu}s — the surface itself has no padding.
15
15
  *
16
16
  * @example
17
17
  * <DropdownMenu renderHandler={({ ref, onClick, isOpen }) => (
18
18
  * <Button boxRef={ref} onClick={onClick} isOpen={isOpen} color="secondary">Menu</Button>
19
19
  * )}>
20
- * <DropdownMenuItem onClick={edit}>Edit</DropdownMenuItem>
20
+ * <DropdownMenuSection>
21
+ * <DropdownMenuItem onClick={edit}>Edit</DropdownMenuItem>
22
+ * </DropdownMenuSection>
21
23
  * </DropdownMenu>
22
24
  */
23
25
  export const DropdownMenu = ({
@@ -5,7 +5,9 @@ import { type DropdownMenuButtonProps } from './types';
5
5
  *
6
6
  * @example
7
7
  * <DropdownMenuButton label="Actions" color="secondary">
8
- * <DropdownMenuItem onClick={remove} isDanger>Delete</DropdownMenuItem>
8
+ * <DropdownMenuSection>
9
+ * <DropdownMenuItem onClick={remove} isDanger>Delete</DropdownMenuItem>
10
+ * </DropdownMenuSection>
9
11
  * </DropdownMenuButton>
10
12
  */
11
13
  export declare const DropdownMenuButton: FC<DropdownMenuButtonProps>;
@@ -6,7 +6,9 @@ import { DropdownMenu } from '../DropdownMenu';
6
6
  *
7
7
  * @example
8
8
  * <DropdownMenuButton label="Actions" color="secondary">
9
- * <DropdownMenuItem onClick={remove} isDanger>Delete</DropdownMenuItem>
9
+ * <DropdownMenuSection>
10
+ * <DropdownMenuItem onClick={remove} isDanger>Delete</DropdownMenuItem>
11
+ * </DropdownMenuSection>
10
12
  * </DropdownMenuButton>
11
13
  */
12
14
  export const DropdownMenuButton = ({
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Clickable row inside a {@link DropdownMenu}, with active (checked), danger and
3
3
  * disabled states. A disabled item with `hint` stays focusable and explains itself in a tooltip.
4
- * Must be rendered inside a {@link DropdownMenu}.
4
+ * Must be rendered inside a {@link DropdownMenuSection} of a {@link DropdownMenu}.
5
5
  *
6
6
  * @example
7
7
  * <DropdownMenuItem onClick={select} isActive={selected}>Option</DropdownMenuItem>
@@ -7,7 +7,7 @@ import { DropdownContext } from '../../context';
7
7
  /**
8
8
  * Clickable row inside a {@link DropdownMenu}, with active (checked), danger and
9
9
  * disabled states. A disabled item with `hint` stays focusable and explains itself in a tooltip.
10
- * Must be rendered inside a {@link DropdownMenu}.
10
+ * Must be rendered inside a {@link DropdownMenuSection} of a {@link DropdownMenu}.
11
11
  *
12
12
  * @example
13
13
  * <DropdownMenuItem onClick={select} isActive={selected}>Option</DropdownMenuItem>
@@ -1,7 +1,8 @@
1
1
  import { type FC } from 'react';
2
2
  import { type DropdownMenuSectionProps } from './types';
3
3
  /**
4
- * Groups {@link DropdownMenuItem}s under an optional heading inside a {@link DropdownMenu}.
4
+ * Required wrapper for {@link DropdownMenuItem}s inside a {@link DropdownMenu}: gives the rows their
5
+ * 8px vertical padding and a divider between sections. `title` is optional, the section is not.
5
6
  *
6
7
  * @example
7
8
  * <DropdownMenuSection title="Account">
@@ -4,7 +4,8 @@ import { Horizontal } from '@pushwoosh/kit-helpers';
4
4
  import { Text } from '@pushwoosh/kit-typography';
5
5
  import { DropdownMenuSectionRoot } from './styles';
6
6
  /**
7
- * Groups {@link DropdownMenuItem}s under an optional heading inside a {@link DropdownMenu}.
7
+ * Required wrapper for {@link DropdownMenuItem}s inside a {@link DropdownMenu}: gives the rows their
8
+ * 8px vertical padding and a divider between sections. `title` is optional, the section is not.
8
9
  *
9
10
  * @example
10
11
  * <DropdownMenuSection title="Account">
@@ -1,7 +1,7 @@
1
1
  import { type PropsWithChildren } from 'react';
2
2
  /** Props of {@link DropdownMenuSection}. */
3
3
  export type DropdownMenuSectionProps = PropsWithChildren<{
4
- /** Optional uppercase section heading. */
4
+ /** Optional uppercase section heading; the section itself is required around menu rows. */
5
5
  title?: string;
6
6
  /** Background color of the section. */
7
7
  bgColor?: string;
@@ -2,11 +2,13 @@ import { type FC } from 'react';
2
2
  import { type DropdownMenuSubmenuProps } from './types';
3
3
  /**
4
4
  * Nested submenu inside a {@link DropdownMenu}: a trigger row that opens a flyout
5
- * of further items on hover or click. Must be rendered inside a {@link DropdownMenu}.
5
+ * of further items on hover or click. Must be rendered inside a {@link DropdownMenuSection}.
6
6
  *
7
7
  * @example
8
- * <DropdownMenuSubmenu label="More">
9
- * <DropdownMenuItem onClick={archive}>Archive</DropdownMenuItem>
10
- * </DropdownMenuSubmenu>
8
+ * <DropdownMenuSection>
9
+ * <DropdownMenuSubmenu label="More">
10
+ * <DropdownMenuItem onClick={archive}>Archive</DropdownMenuItem>
11
+ * </DropdownMenuSubmenu>
12
+ * </DropdownMenuSection>
11
13
  */
12
14
  export declare const DropdownMenuSubmenu: FC<DropdownMenuSubmenuProps>;
@@ -12,12 +12,14 @@ const OPEN_DELAY = 100;
12
12
  const CLOSE_DELAY = 200;
13
13
  /**
14
14
  * Nested submenu inside a {@link DropdownMenu}: a trigger row that opens a flyout
15
- * of further items on hover or click. Must be rendered inside a {@link DropdownMenu}.
15
+ * of further items on hover or click. Must be rendered inside a {@link DropdownMenuSection}.
16
16
  *
17
17
  * @example
18
- * <DropdownMenuSubmenu label="More">
19
- * <DropdownMenuItem onClick={archive}>Archive</DropdownMenuItem>
20
- * </DropdownMenuSubmenu>
18
+ * <DropdownMenuSection>
19
+ * <DropdownMenuSubmenu label="More">
20
+ * <DropdownMenuItem onClick={archive}>Archive</DropdownMenuItem>
21
+ * </DropdownMenuSubmenu>
22
+ * </DropdownMenuSection>
21
23
  */
22
24
  export const DropdownMenuSubmenu = ({
23
25
  label,
@@ -0,0 +1,5 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { PushStoriesPreviewProps } from './types';
3
+ /** Expanded iOS push story as `PushwooshStoriesViewController` draws it: 2:3 page, progress, scrim, button.
4
+ * Fills its container's width, so it fits a form column as well as the phone of a push preview. */
5
+ export declare function PushStoriesPreview(props: PushStoriesPreviewProps): ReactElement;
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { ImageIcon } from '@pushwoosh/kit-icons';
4
+ import { PushStoriesPage } from './components/PushStoriesPage';
5
+ import { PushStoriesProgress } from './components/PushStoriesProgress';
6
+ import { clampStoryIndex, normalizeStoryDuration } from './helpers';
7
+ import { Lang, useLang } from './polyglot';
8
+ import { EmptyState, StoriesCanvas, StoriesFrame, TapZone } from './styles';
9
+ /** Expanded iOS push story as `PushwooshStoriesViewController` draws it: 2:3 page, progress, scrim, button.
10
+ * Fills its container's width, so it fits a form column as well as the phone of a push preview. */
11
+ export function PushStoriesPreview(props) {
12
+ const {
13
+ pages,
14
+ activeIndex,
15
+ defaultActiveIndex = 0,
16
+ onActiveIndexChange,
17
+ autoplay = false,
18
+ interactive = true,
19
+ width
20
+ } = props;
21
+ const lang = useLang();
22
+ const [ownIndex, setOwnIndex] = useState(defaultActiveIndex);
23
+ const [cycle, setCycle] = useState(0);
24
+ if (pages.length === 0) {
25
+ return _jsx(StoriesFrame, {
26
+ "$width": width,
27
+ children: _jsx(StoriesCanvas, {
28
+ children: _jsxs(EmptyState, {
29
+ children: [_jsx(ImageIcon, {
30
+ size: "medium",
31
+ view: "lined"
32
+ }), _jsx(Lang, {
33
+ message: "DUMB.PUSH_STORIES_PREVIEW.EMPTY"
34
+ })]
35
+ })
36
+ })
37
+ });
38
+ }
39
+ const index = clampStoryIndex(activeIndex ?? ownIndex, pages.length);
40
+ const goNext = () => {
41
+ const next = (index + 1) % pages.length;
42
+ setCycle(value => value + 1);
43
+ if (activeIndex === undefined) {
44
+ setOwnIndex(next);
45
+ }
46
+ if (next !== index) {
47
+ onActiveIndexChange?.(next);
48
+ }
49
+ };
50
+ return _jsx(StoriesFrame, {
51
+ "$width": width,
52
+ children: _jsxs(StoriesCanvas, {
53
+ children: [_jsx(PushStoriesPage, {
54
+ page: pages[index]
55
+ }), _jsx(PushStoriesProgress, {
56
+ count: pages.length,
57
+ activeIndex: index,
58
+ playing: autoplay,
59
+ duration: normalizeStoryDuration(pages[index].duration),
60
+ cycle: cycle,
61
+ onComplete: goNext
62
+ }), interactive && _jsx(TapZone, {
63
+ "aria-label": lang({
64
+ message: 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE'
65
+ }),
66
+ type: "button",
67
+ onClick: goNext
68
+ })]
69
+ })
70
+ });
71
+ }
@@ -0,0 +1,234 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { hasStoryButton, normalizeStoryDuration } from '../helpers';
4
+ import { PushStoriesPreview } from '../PushStoriesPreview';
5
+ const pages = [{
6
+ image: 'https://example.com/1.jpg',
7
+ title: 'Welcome',
8
+ subtitle: 'Swipe to explore',
9
+ buttonTitle: 'Get started',
10
+ link: 'app://one'
11
+ }, {
12
+ image: 'https://example.com/2.jpg',
13
+ title: 'Stay in the loop',
14
+ duration: 4
15
+ }, {
16
+ image: 'https://example.com/3.jpg',
17
+ title: 'You are all set',
18
+ buttonTitle: 'Open app',
19
+ link: 'app://three'
20
+ }];
21
+ const next = () => {
22
+ fireEvent.click(screen.getByLabelText('Next page'));
23
+ };
24
+ const progressFill = index => screen.getByTestId('push-stories-progress').children[index].firstElementChild;
25
+ describe('normalizeStoryDuration', () => {
26
+ it('plays a missing, zero, negative or non-finite duration for 5 seconds', () => {
27
+ expect(normalizeStoryDuration(undefined)).toBe(5);
28
+ expect(normalizeStoryDuration(0)).toBe(5);
29
+ expect(normalizeStoryDuration(-3)).toBe(5);
30
+ expect(normalizeStoryDuration(Number.NaN)).toBe(5);
31
+ expect(normalizeStoryDuration(Number.POSITIVE_INFINITY)).toBe(5);
32
+ });
33
+ it('keeps a valid duration and caps it at 30 seconds', () => {
34
+ expect(normalizeStoryDuration(1.5)).toBe(1.5);
35
+ expect(normalizeStoryDuration(45)).toBe(30);
36
+ });
37
+ });
38
+ describe('hasStoryButton', () => {
39
+ it('needs both a title and a link with a scheme, as the SDK does', () => {
40
+ expect(hasStoryButton({
41
+ buttonTitle: 'Go',
42
+ link: 'app://x'
43
+ })).toBe(true);
44
+ expect(hasStoryButton({
45
+ buttonTitle: 'Go',
46
+ link: 'https://example.com'
47
+ })).toBe(true);
48
+ expect(hasStoryButton({
49
+ buttonTitle: 'Go',
50
+ link: 'example.com/promo'
51
+ })).toBe(false);
52
+ expect(hasStoryButton({
53
+ buttonTitle: 'Go'
54
+ })).toBe(false);
55
+ expect(hasStoryButton({
56
+ buttonTitle: ' ',
57
+ link: 'app://x'
58
+ })).toBe(false);
59
+ expect(hasStoryButton({
60
+ link: 'app://x'
61
+ })).toBe(false);
62
+ });
63
+ });
64
+ describe('PushStoriesPreview', () => {
65
+ it('draws the empty state without pages', () => {
66
+ render(_jsx(PushStoriesPreview, {
67
+ pages: []
68
+ }));
69
+ expect(screen.getByText('Add a page to see the story preview')).toBeInTheDocument();
70
+ expect(screen.queryByTestId('push-stories-progress')).not.toBeInTheDocument();
71
+ });
72
+ it('draws one progress segment per page and the first page text', () => {
73
+ render(_jsx(PushStoriesPreview, {
74
+ pages: pages
75
+ }));
76
+ expect(screen.getByTestId('push-stories-progress').children).toHaveLength(3);
77
+ expect(screen.getByText('Welcome')).toBeInTheDocument();
78
+ expect(screen.getByText('Swipe to explore')).toBeInTheDocument();
79
+ expect(screen.getByText('Get started')).toBeInTheDocument();
80
+ });
81
+ it('keeps one segment per page and one image on screen through clicks and page list changes', () => {
82
+ const {
83
+ container,
84
+ rerender
85
+ } = render(_jsx(PushStoriesPreview, {
86
+ pages: pages
87
+ }));
88
+ const expectShape = count => {
89
+ expect(screen.getByTestId('push-stories-progress').children).toHaveLength(count);
90
+ expect(container.querySelectorAll('img')).toHaveLength(1);
91
+ };
92
+ for (let click = 0; click < 7; click += 1) {
93
+ expectShape(3);
94
+ next();
95
+ }
96
+ rerender(_jsx(PushStoriesPreview, {
97
+ pages: [...pages, {
98
+ image: 'https://example.com/4.jpg',
99
+ title: 'Fourth'
100
+ }]
101
+ }));
102
+ expectShape(4);
103
+ rerender(_jsx(PushStoriesPreview, {
104
+ pages: pages.slice(0, 2)
105
+ }));
106
+ expectShape(2);
107
+ });
108
+ it('hides the button of a page without a link', () => {
109
+ render(_jsx(PushStoriesPreview, {
110
+ pages: [{
111
+ ...pages[0],
112
+ link: ''
113
+ }]
114
+ }));
115
+ expect(screen.queryByText('Get started')).not.toBeInTheDocument();
116
+ });
117
+ it('shows the next page on every click and goes round after the last one', () => {
118
+ render(_jsx(PushStoriesPreview, {
119
+ pages: pages
120
+ }));
121
+ next();
122
+ expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
123
+ expect(progressFill(0)).not.toBeNull();
124
+ next();
125
+ expect(screen.getByText('You are all set')).toBeInTheDocument();
126
+ next();
127
+ expect(screen.getByText('Welcome')).toBeInTheDocument();
128
+ expect(progressFill(1)).toBeNull();
129
+ next();
130
+ expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
131
+ });
132
+ it('has a single click target over the whole story', () => {
133
+ render(_jsx(PushStoriesPreview, {
134
+ pages: pages
135
+ }));
136
+ expect(screen.getAllByRole('button')).toHaveLength(1);
137
+ });
138
+ it('follows the activeIndex prop and reports clicks instead of moving itself', () => {
139
+ const onActiveIndexChange = jest.fn();
140
+ const {
141
+ rerender
142
+ } = render(_jsx(PushStoriesPreview, {
143
+ pages: pages,
144
+ activeIndex: 1,
145
+ onActiveIndexChange: onActiveIndexChange
146
+ }));
147
+ expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
148
+ next();
149
+ expect(onActiveIndexChange).toHaveBeenCalledWith(2);
150
+ expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
151
+ rerender(_jsx(PushStoriesPreview, {
152
+ pages: pages,
153
+ activeIndex: 2,
154
+ onActiveIndexChange: onActiveIndexChange
155
+ }));
156
+ expect(screen.getByText('You are all set')).toBeInTheDocument();
157
+ });
158
+ it('clamps an activeIndex past the last page', () => {
159
+ render(_jsx(PushStoriesPreview, {
160
+ pages: pages,
161
+ activeIndex: 9
162
+ }));
163
+ expect(screen.getByText('You are all set')).toBeInTheDocument();
164
+ });
165
+ it('advances when the autoplay segment finishes and goes round after the last page', () => {
166
+ render(_jsx(PushStoriesPreview, {
167
+ pages: pages.slice(0, 2),
168
+ autoplay: true
169
+ }));
170
+ fireEvent.animationEnd(progressFill(0));
171
+ expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
172
+ fireEvent.animationEnd(progressFill(1));
173
+ expect(screen.getByText('Welcome')).toBeInTheDocument();
174
+ expect(progressFill(1)).toBeNull();
175
+ });
176
+ it('fills segments up to and including the current page without autoplay', () => {
177
+ const {
178
+ container
179
+ } = render(_jsx(PushStoriesPreview, {
180
+ pages: pages
181
+ }));
182
+ const filled = () => [...screen.getByTestId('push-stories-progress').children].filter(track => track.firstElementChild).length;
183
+ expect(filled()).toBe(1);
184
+ next();
185
+ expect(filled()).toBe(2);
186
+ next();
187
+ expect(filled()).toBe(3);
188
+ next();
189
+ expect(filled()).toBe(1);
190
+ expect(container.querySelectorAll('img')).toHaveLength(1);
191
+ });
192
+ it('starts the next segment from zero when a click interrupts autoplay', () => {
193
+ render(_jsx(PushStoriesPreview, {
194
+ pages: pages,
195
+ autoplay: true
196
+ }));
197
+ const interrupted = progressFill(0);
198
+ next();
199
+ expect(screen.getByText('Stay in the loop')).toBeInTheDocument();
200
+ expect(progressFill(0)).not.toBe(interrupted);
201
+ expect(progressFill(1)).not.toBeNull();
202
+ });
203
+ it('replays a single page instead of moving anywhere', () => {
204
+ render(_jsx(PushStoriesPreview, {
205
+ pages: pages.slice(0, 1),
206
+ autoplay: true
207
+ }));
208
+ const run = progressFill(0);
209
+ next();
210
+ expect(screen.getByTestId('push-stories-progress').children).toHaveLength(1);
211
+ expect(screen.getByText('Welcome')).toBeInTheDocument();
212
+ expect(progressFill(0)).not.toBe(run);
213
+ });
214
+ it('draws a placeholder for a missing or broken image', () => {
215
+ const {
216
+ container
217
+ } = render(_jsx(PushStoriesPreview, {
218
+ pages: [{
219
+ title: 'No image'
220
+ }, pages[1]]
221
+ }));
222
+ expect(screen.getByTestId('push-stories-image-placeholder')).toBeInTheDocument();
223
+ next();
224
+ fireEvent.error(container.querySelector('img'));
225
+ expect(screen.getByTestId('push-stories-image-placeholder')).toBeInTheDocument();
226
+ });
227
+ it('drops the click target when not interactive', () => {
228
+ render(_jsx(PushStoriesPreview, {
229
+ pages: pages,
230
+ interactive: false
231
+ }));
232
+ expect(screen.queryByLabelText('Next page')).not.toBeInTheDocument();
233
+ });
234
+ });
@@ -0,0 +1,4 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { PushStoriesPageProps } from './types';
3
+ /** One story page: the image, the scrim and the title, subtitle and button laid over it. */
4
+ export declare function PushStoriesPage(props: PushStoriesPageProps): ReactElement;
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { ImageIcon } from '@pushwoosh/kit-icons';
4
+ import { BottomArea, ImagePlaceholder, PageButton, PageButtonLabel, PageImage, PageSubtitle, PageTitle } from './styles';
5
+ import { hasStoryButton } from '../../helpers';
6
+ /** One story page: the image, the scrim and the title, subtitle and button laid over it. */
7
+ export function PushStoriesPage(props) {
8
+ const {
9
+ page
10
+ } = props;
11
+ const [failedImage, setFailedImage] = useState();
12
+ const image = page.image?.trim();
13
+ const title = page.title?.trim();
14
+ const subtitle = page.subtitle?.trim();
15
+ return _jsxs(_Fragment, {
16
+ children: [image && failedImage !== image ? _jsx(PageImage, {
17
+ alt: "",
18
+ src: image,
19
+ onError: () => setFailedImage(image)
20
+ }) : _jsx(ImagePlaceholder, {
21
+ "data-testid": "push-stories-image-placeholder",
22
+ children: _jsx(ImageIcon, {
23
+ size: "medium",
24
+ view: "lined"
25
+ })
26
+ }), _jsxs(BottomArea, {
27
+ children: [title && _jsx(PageTitle, {
28
+ children: title
29
+ }), subtitle && _jsx(PageSubtitle, {
30
+ children: subtitle
31
+ }), hasStoryButton(page) && _jsx(PageButton, {
32
+ children: _jsx(PageButtonLabel, {
33
+ children: page.buttonTitle?.trim()
34
+ })
35
+ })]
36
+ })]
37
+ });
38
+ }
@@ -0,0 +1 @@
1
+ export { PushStoriesPage } from './PushStoriesPage';
@@ -0,0 +1 @@
1
+ export { PushStoriesPage } from './PushStoriesPage';
@@ -0,0 +1,7 @@
1
+ export declare const PageImage: import("styled-components").StyledComponent<"img", any, {}, never>;
2
+ export declare const ImagePlaceholder: import("styled-components").StyledComponent<"div", any, {}, never>;
3
+ export declare const BottomArea: import("styled-components").StyledComponent<"div", any, {}, never>;
4
+ export declare const PageTitle: import("styled-components").StyledComponent<"div", any, {}, never>;
5
+ export declare const PageSubtitle: import("styled-components").StyledComponent<"div", any, {}, never>;
6
+ export declare const PageButton: import("styled-components").StyledComponent<"div", any, {}, never>;
7
+ export declare const PageButtonLabel: import("styled-components").StyledComponent<"span", any, {}, never>;
@@ -0,0 +1,31 @@
1
+ import styled from 'styled-components';
2
+ import { PLACEHOLDER_BACKGROUND } from '../../constants';
3
+ import { scaled } from '../../helpers';
4
+ export const PageImage = styled.img.withConfig({
5
+ displayName: "PageImage",
6
+ componentId: "sc-1q2ujdq-0"
7
+ })(["position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"]);
8
+ export const ImagePlaceholder = styled.div.withConfig({
9
+ displayName: "ImagePlaceholder",
10
+ componentId: "sc-1q2ujdq-1"
11
+ })(["position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:", ";color:rgba(255,255,255,0.45);"], PLACEHOLDER_BACKGROUND);
12
+ export const BottomArea = styled.div.withConfig({
13
+ displayName: "BottomArea",
14
+ componentId: "sc-1q2ujdq-2"
15
+ })(["position:absolute;left:0;right:0;bottom:0;z-index:2;display:flex;flex-direction:column;padding:", " ", " ", ";background:linear-gradient(to bottom,rgba(0,0,0,0) 0%,rgba(0,0,0,0.75) 100%);pointer-events:none;"], scaled(44), scaled(24), scaled(24));
16
+ export const PageTitle = styled.div.withConfig({
17
+ displayName: "PageTitle",
18
+ componentId: "sc-1q2ujdq-3"
19
+ })(["display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;color:#FFF;font-size:", ";font-weight:700;line-height:1.2;overflow-wrap:anywhere;"], scaled(21));
20
+ export const PageSubtitle = styled.div.withConfig({
21
+ displayName: "PageSubtitle",
22
+ componentId: "sc-1q2ujdq-4"
23
+ })(["display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;color:rgba(255,255,255,0.85);font-size:", ";font-weight:400;line-height:1.3;overflow-wrap:anywhere;* + &{margin-top:", ";}"], scaled(14), scaled(6));
24
+ export const PageButton = styled.div.withConfig({
25
+ displayName: "PageButton",
26
+ componentId: "sc-1q2ujdq-5"
27
+ })(["display:flex;align-items:center;justify-content:center;height:", ";padding:0 ", ";border-radius:", ";background:#FFF;box-shadow:0 ", " ", " rgba(0,0,0,0.28);color:#0F0F0F;font-size:", ";font-weight:600;* + &{margin-top:", ";}"], scaled(52), scaled(28), scaled(26), scaled(6), scaled(28), scaled(17), scaled(16));
28
+ export const PageButtonLabel = styled.span.withConfig({
29
+ displayName: "PageButtonLabel",
30
+ componentId: "sc-1q2ujdq-6"
31
+ })(["overflow:hidden;white-space:nowrap;text-overflow:ellipsis;"]);
@@ -0,0 +1,6 @@
1
+ import type { PushStoriesPreviewPage } from '../../types';
2
+ /** Props of {@link PushStoriesPage}. */
3
+ export interface PushStoriesPageProps {
4
+ /** Page to draw. */
5
+ page: PushStoriesPreviewPage;
6
+ }
@@ -0,0 +1,4 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { PushStoriesProgressProps } from './types';
3
+ /** Segmented bar over the top of the story, one segment per page. */
4
+ export declare function PushStoriesProgress(props: PushStoriesProgressProps): ReactElement;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ProgressFill, ProgressRow, ProgressTrack } from './styles';
3
+ /** Segmented bar over the top of the story, one segment per page. */
4
+ export function PushStoriesProgress(props) {
5
+ const {
6
+ count,
7
+ activeIndex,
8
+ playing,
9
+ duration,
10
+ cycle,
11
+ onComplete
12
+ } = props;
13
+ return _jsx(ProgressRow, {
14
+ "data-testid": "push-stories-progress",
15
+ children: Array.from({
16
+ length: count
17
+ }, (_, index) => _jsxs(ProgressTrack, {
18
+ children: [(index < activeIndex || index === activeIndex && !playing) && _jsx(ProgressFill, {}), index === activeIndex && playing && _jsx(ProgressFill, {
19
+ "$duration": duration,
20
+ onAnimationEnd: onComplete
21
+ }, cycle)]
22
+ }, index))
23
+ });
24
+ }
@@ -0,0 +1 @@
1
+ export { PushStoriesProgress } from './PushStoriesProgress';
@@ -0,0 +1 @@
1
+ export { PushStoriesProgress } from './PushStoriesProgress';
@@ -0,0 +1,5 @@
1
+ export declare const ProgressRow: import("styled-components").StyledComponent<"div", any, {}, never>;
2
+ export declare const ProgressTrack: import("styled-components").StyledComponent<"div", any, {}, never>;
3
+ export declare const ProgressFill: import("styled-components").StyledComponent<"div", any, {
4
+ $duration?: number;
5
+ }, never>;
@@ -0,0 +1,17 @@
1
+ import styled, { css, keyframes } from 'styled-components';
2
+ import { scaled } from '../../helpers';
3
+ const grow = keyframes(["from{width:0;}to{width:100%;}"]);
4
+ export const ProgressRow = styled.div.withConfig({
5
+ displayName: "ProgressRow",
6
+ componentId: "sc-1p5nb36-0"
7
+ })(["position:absolute;top:", ";left:", ";right:", ";z-index:2;display:flex;gap:", ";pointer-events:none;"], scaled(23), scaled(12), scaled(12), scaled(4));
8
+ export const ProgressTrack = styled.div.withConfig({
9
+ displayName: "ProgressTrack",
10
+ componentId: "sc-1p5nb36-1"
11
+ })(["flex:1;height:max(2px,", ");border-radius:", ";background:rgba(255,255,255,0.3);overflow:hidden;"], scaled(3), scaled(1.5));
12
+ export const ProgressFill = styled.div.withConfig({
13
+ displayName: "ProgressFill",
14
+ componentId: "sc-1p5nb36-2"
15
+ })(["width:100%;height:100%;background:#FFF;", ""], ({
16
+ $duration
17
+ }) => $duration !== undefined && css(["animation:", " ", "s linear both;"], grow, $duration));
@@ -0,0 +1,15 @@
1
+ /** Props of {@link PushStoriesProgress}. */
2
+ export interface PushStoriesProgressProps {
3
+ /** Number of pages, one segment each. */
4
+ count: number;
5
+ /** Segment of the page on screen. */
6
+ activeIndex: number;
7
+ /** Fill the active segment over `duration`; otherwise it is drawn full, so page N shows N full segments. */
8
+ playing: boolean;
9
+ /** Seconds the active segment takes to fill. */
10
+ duration: number;
11
+ /** Changes whenever playback of the active page restarts, even on the same index. */
12
+ cycle: number;
13
+ /** Called when the active segment finishes filling. */
14
+ onComplete: () => void;
15
+ }
@@ -0,0 +1,8 @@
1
+ /** Height ÷ width of the stories area, the iOS SDK's default `storyAspectRatio`. */
2
+ export declare const PUSH_STORIES_ASPECT_RATIO = 1.5;
3
+ /** Seconds a page stays on screen when its duration is missing or invalid. */
4
+ export declare const PUSH_STORIES_DEFAULT_DURATION = 5;
5
+ /** Longest page duration in seconds the iOS SDK honours. */
6
+ export declare const PUSH_STORIES_MAX_DURATION = 30;
7
+ export declare const REFERENCE_WIDTH = 375;
8
+ export declare const PLACEHOLDER_BACKGROUND = "linear-gradient(160deg, #3A3A3C 0%, #1C1C1E 100%)";
@@ -0,0 +1,8 @@
1
+ /** Height ÷ width of the stories area, the iOS SDK's default `storyAspectRatio`. */
2
+ export const PUSH_STORIES_ASPECT_RATIO = 1.5;
3
+ /** Seconds a page stays on screen when its duration is missing or invalid. */
4
+ export const PUSH_STORIES_DEFAULT_DURATION = 5;
5
+ /** Longest page duration in seconds the iOS SDK honours. */
6
+ export const PUSH_STORIES_MAX_DURATION = 30;
7
+ export const REFERENCE_WIDTH = 375;
8
+ export const PLACEHOLDER_BACKGROUND = 'linear-gradient(160deg, #3A3A3C 0%, #1C1C1E 100%)';
@@ -0,0 +1,7 @@
1
+ import type { PushStoriesPreviewPage } from './types';
2
+ /** Page duration in seconds as the iOS SDK plays it. */
3
+ export declare function normalizeStoryDuration(duration?: number): number;
4
+ /** Whether the iOS SDK draws the page's button: it needs a title and a link with a scheme. */
5
+ export declare function hasStoryButton(page: PushStoriesPreviewPage): boolean;
6
+ export declare function clampStoryIndex(index: number, length: number): number;
7
+ export declare function scaled(points: number): string;
@@ -0,0 +1,19 @@
1
+ import { PUSH_STORIES_DEFAULT_DURATION, PUSH_STORIES_MAX_DURATION, REFERENCE_WIDTH } from './constants';
2
+ const URL_SCHEME = /^[a-z][a-z\d+.-]*:/i;
3
+ /** Page duration in seconds as the iOS SDK plays it. */
4
+ export function normalizeStoryDuration(duration) {
5
+ if (typeof duration !== 'number' || !Number.isFinite(duration) || duration <= 0) {
6
+ return PUSH_STORIES_DEFAULT_DURATION;
7
+ }
8
+ return Math.min(duration, PUSH_STORIES_MAX_DURATION);
9
+ }
10
+ /** Whether the iOS SDK draws the page's button: it needs a title and a link with a scheme. */
11
+ export function hasStoryButton(page) {
12
+ return !!page.buttonTitle?.trim() && URL_SCHEME.test(page.link?.trim() || '');
13
+ }
14
+ export function clampStoryIndex(index, length) {
15
+ return Math.max(0, Math.min(index, length - 1));
16
+ }
17
+ export function scaled(points) {
18
+ return `calc(${points} * 100cqw / ${REFERENCE_WIDTH})`;
19
+ }
@@ -0,0 +1,4 @@
1
+ export { PushStoriesPreview } from './PushStoriesPreview';
2
+ export { hasStoryButton, normalizeStoryDuration } from './helpers';
3
+ export { PUSH_STORIES_ASPECT_RATIO, PUSH_STORIES_DEFAULT_DURATION, PUSH_STORIES_MAX_DURATION, } from './constants';
4
+ export type { PushStoriesPreviewPage, PushStoriesPreviewProps } from './types';
@@ -0,0 +1,3 @@
1
+ export { PushStoriesPreview } from './PushStoriesPreview';
2
+ export { hasStoryButton, normalizeStoryDuration } from './helpers';
3
+ export { PUSH_STORIES_ASPECT_RATIO, PUSH_STORIES_DEFAULT_DURATION, PUSH_STORIES_MAX_DURATION } from './constants';
@@ -0,0 +1,2 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ export declare const Lang: import("react").FC<import("../../polyglot").LangProps<PushStoriesPreviewMessages>>, useLang: () => (props: import("../../polyglot").LangProps<PushStoriesPreviewMessages>) => string;
@@ -0,0 +1,34 @@
1
+ import { messages as ar } from './messages_ar';
2
+ import { messages as bn } from './messages_bn';
3
+ import { messages as de } from './messages_de';
4
+ import { messages as en } from './messages_en';
5
+ import { messages as es } from './messages_es';
6
+ import { messages as fr } from './messages_fr';
7
+ import { messages as he } from './messages_he';
8
+ import { messages as hi } from './messages_hi';
9
+ import { messages as ja } from './messages_ja';
10
+ import { messages as ko } from './messages_ko';
11
+ import { messages as pt } from './messages_pt';
12
+ import { messages as ru } from './messages_ru';
13
+ import { messages as th } from './messages_th';
14
+ import { messages as zh } from './messages_zh';
15
+ import { createLang } from '../../polyglot';
16
+ export const {
17
+ Lang,
18
+ useLang
19
+ } = createLang({
20
+ ar,
21
+ bn,
22
+ de,
23
+ en,
24
+ es,
25
+ fr,
26
+ he,
27
+ hi,
28
+ ja,
29
+ ko,
30
+ pt,
31
+ ru,
32
+ th,
33
+ zh
34
+ });
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'أضف صفحة لعرض معاينة القصة',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'الصفحة التالية'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'স্টোরির প্রিভিউ দেখতে একটি পেজ যোগ করুন',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'পরের পেজ'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'Fügen Sie eine Seite hinzu, um die Story-Vorschau zu sehen',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'Nächste Seite'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'Add a page to see the story preview',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'Next page'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'Añade una página para ver la vista previa de la historia',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'Página siguiente'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'Ajoutez une page pour voir l’aperçu de la story',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'Page suivante'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'הוסיפו עמוד כדי לראות תצוגה מקדימה של הסטורי',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'העמוד הבא'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'स्टोरी का प्रीव्यू देखने के लिए एक पेज जोड़ें',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'अगला पेज'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'ページを追加するとストーリーのプレビューが表示されます',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': '次のページ'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': '페이지를 추가하면 스토리 미리보기가 표시됩니다',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': '다음 페이지'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'Adicione uma página para ver a pré-visualização da história',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'Próxima página'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'Добавьте страницу, чтобы увидеть превью сториз',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'Следующая страница'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': 'เพิ่มหน้าเพื่อดูตัวอย่างสตอรี่',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': 'หน้าถัดไป'
4
+ };
@@ -0,0 +1,3 @@
1
+ import { type PushStoriesPreviewMessages } from './types';
2
+ import type { LangMessages } from '../../polyglot';
3
+ export declare const messages: LangMessages<PushStoriesPreviewMessages>;
@@ -0,0 +1,4 @@
1
+ export const messages = {
2
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': '添加页面以查看快拍预览',
3
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': '下一页'
4
+ };
@@ -0,0 +1,5 @@
1
+ import type { NoParams } from '../../polyglot';
2
+ export type PushStoriesPreviewMessages = {
3
+ 'DUMB.PUSH_STORIES_PREVIEW.EMPTY': NoParams;
4
+ 'DUMB.PUSH_STORIES_PREVIEW.NEXT_PAGE': NoParams;
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export declare const StoriesFrame: import("styled-components").StyledComponent<"div", any, {
2
+ $width?: string | number;
3
+ }, never>;
4
+ export declare const StoriesCanvas: import("styled-components").StyledComponent<"div", any, {}, never>;
5
+ export declare const TapZone: import("styled-components").StyledComponent<"button", any, {}, never>;
6
+ export declare const EmptyState: import("styled-components").StyledComponent<"div", any, {}, never>;
@@ -0,0 +1,23 @@
1
+ import styled from 'styled-components';
2
+ import { FontFamily } from '@pushwoosh/kit-constants';
3
+ import { toCssValue } from '@pushwoosh/kit-helpers';
4
+ import { PLACEHOLDER_BACKGROUND, PUSH_STORIES_ASPECT_RATIO } from './constants';
5
+ import { scaled } from './helpers';
6
+ export const StoriesFrame = styled.div.withConfig({
7
+ displayName: "StoriesFrame",
8
+ componentId: "sc-udrlhm-0"
9
+ })(["position:relative;container-type:inline-size;width:", ";aspect-ratio:1 / ", ";font-family:", ";user-select:none;"], ({
10
+ $width
11
+ }) => $width ? toCssValue($width) : '100%', PUSH_STORIES_ASPECT_RATIO, FontFamily.MAIN);
12
+ export const StoriesCanvas = styled.div.withConfig({
13
+ displayName: "StoriesCanvas",
14
+ componentId: "sc-udrlhm-1"
15
+ })(["position:absolute;inset:0;overflow:hidden;border-radius:", ";background:#000;isolation:isolate;"], scaled(24));
16
+ export const TapZone = styled.button.withConfig({
17
+ displayName: "TapZone",
18
+ componentId: "sc-udrlhm-2"
19
+ })(["position:absolute;inset:0;z-index:1;padding:0;border:none;background:transparent;cursor:pointer;&:focus-visible{outline:none;box-shadow:inset 0 0 0 2px rgba(255,255,255,0.6);}"]);
20
+ export const EmptyState = styled.div.withConfig({
21
+ displayName: "EmptyState",
22
+ componentId: "sc-udrlhm-3"
23
+ })(["position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:", ";padding:0 ", ";background:", ";color:rgba(255,255,255,0.7);font-size:", ";line-height:1.35;text-align:center;"], scaled(12), scaled(32), PLACEHOLDER_BACKGROUND, scaled(15));
@@ -0,0 +1,32 @@
1
+ /** One full-screen page of an iOS push story, in the shape the push preset form edits. */
2
+ export interface PushStoriesPreviewPage {
3
+ /** Full-bleed image URL; an empty or broken one is drawn as a placeholder. */
4
+ image?: string;
5
+ /** Seconds before the page auto-advances; clamped to 30, non-positive falls back to 5. */
6
+ duration?: number;
7
+ /** Title over the bottom of the page, two lines at most. */
8
+ title?: string;
9
+ /** Subtitle under the title, two lines at most. */
10
+ subtitle?: string;
11
+ /** Call-to-action text; the button shows only together with a `link` that has a scheme. */
12
+ buttonTitle?: string;
13
+ /** Deep link the button opens on the device. */
14
+ link?: string;
15
+ }
16
+ /** Props of {@link PushStoriesPreview}. */
17
+ export interface PushStoriesPreviewProps {
18
+ /** Pages in playback order; an empty list draws the empty state. */
19
+ pages: PushStoriesPreviewPage[];
20
+ /** Page to show; pass it to control the preview, e.g. to follow the page being edited. */
21
+ activeIndex?: number;
22
+ /** Page shown first when `activeIndex` is not controlled (default 0). */
23
+ defaultActiveIndex?: number;
24
+ /** Called when a click or autoplay moves to another page. */
25
+ onActiveIndexChange?: (index: number) => void;
26
+ /** Advance pages by their `duration`, animating the progress bar, round and round (default false). */
27
+ autoplay?: boolean;
28
+ /** A click anywhere on the story shows the next page, after the last one the first (default true). */
29
+ interactive?: boolean;
30
+ /** CSS width; the height follows the device's 2:3 stories ratio (default 100%). */
31
+ width?: string | number;
32
+ }
@@ -0,0 +1 @@
1
+ export {};
package/index.d.ts CHANGED
@@ -34,6 +34,7 @@ export { Link, type LinkPropsWithoutAs, type LinkPropsWithAsTag, type LinkPropsW
34
34
  export { StatisticCard } from './StatisticCard';
35
35
  export { EmailTemplatePreview, NoEmailTemplatePreview, replaceLocalization, replacePlaceholders, matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE, type EmailClickMapItem, type EmailClickMapPlacement, type EmailClickMapProps, type PreviewIframeProps, } from './EmailTemplatePreview';
36
36
  export { PushPreview } from './PushPreview';
37
+ export { PushStoriesPreview, hasStoryButton, normalizeStoryDuration, PUSH_STORIES_ASPECT_RATIO, PUSH_STORIES_DEFAULT_DURATION, PUSH_STORIES_MAX_DURATION, type PushStoriesPreviewPage, type PushStoriesPreviewProps, } from './PushStoriesPreview';
37
38
  export { InboxPreview, resolveInboxLayout, MAX_CAROUSEL_SLIDES, type InboxLayoutType, type InboxPreviewPlatform, type InboxPreviewContent, type InboxPreviewProps, type InboxPreviewSlide, } from './InboxPreview';
38
39
  export { WhatsAppPreview } from './WhatsAppPreview';
39
40
  export type { WhatsAppAttachment, WhatsAppAttachmentKind, WhatsAppMessage, WhatsAppPreviewProps, } from './WhatsAppPreview';
package/index.js CHANGED
@@ -34,6 +34,7 @@ export { Link } from './Link';
34
34
  export { StatisticCard } from './StatisticCard';
35
35
  export { EmailTemplatePreview, NoEmailTemplatePreview, replaceLocalization, replacePlaceholders, matchClickMapItems, decorateClickMap, CLICK_MAP_BADGE_ATTRIBUTE } from './EmailTemplatePreview';
36
36
  export { PushPreview } from './PushPreview';
37
+ export { PushStoriesPreview, hasStoryButton, normalizeStoryDuration, PUSH_STORIES_ASPECT_RATIO, PUSH_STORIES_DEFAULT_DURATION, PUSH_STORIES_MAX_DURATION } from './PushStoriesPreview';
37
38
  export { InboxPreview, resolveInboxLayout, MAX_CAROUSEL_SLIDES } from './InboxPreview';
38
39
  export { WhatsAppPreview } from './WhatsAppPreview';
39
40
  export { Toast, ToastProvider, useToast } from './Toast';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",