@streamscloud/embeddable 16.3.1 → 17.0.0-1775740482426
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/dist/ads/ad-card/cmp.ad-card.svelte +11 -9
- package/dist/feed-player/cmp.close-button.svelte +40 -0
- package/dist/feed-player/cmp.close-button.svelte.d.ts +19 -0
- package/dist/feed-player/cmp.feed-player.svelte +516 -0
- package/dist/feed-player/cmp.feed-player.svelte.d.ts +14 -0
- package/dist/feed-player/feed-player-localization.d.ts +16 -0
- package/dist/feed-player/feed-player-localization.js +70 -0
- package/dist/feed-player/index.d.ts +3 -0
- package/dist/feed-player/index.js +2 -0
- package/dist/feed-player/sidebar/article-tab.svelte +98 -0
- package/dist/feed-player/sidebar/article-tab.svelte.d.ts +19 -0
- package/dist/feed-player/sidebar/information-tab.svelte +102 -0
- package/dist/feed-player/sidebar/information-tab.svelte.d.ts +26 -0
- package/dist/feed-player/sidebar/playlist-tab.svelte +110 -0
- package/dist/feed-player/sidebar/playlist-tab.svelte.d.ts +19 -0
- package/dist/feed-player/sidebar/post-card.svelte +128 -0
- package/dist/feed-player/sidebar/post-card.svelte.d.ts +24 -0
- package/dist/feed-player/sidebar/recommended-tab.svelte +165 -0
- package/dist/feed-player/sidebar/recommended-tab.svelte.d.ts +24 -0
- package/dist/feed-player/sidebar/sidebar-panel.svelte +84 -0
- package/dist/feed-player/sidebar/sidebar-panel.svelte.d.ts +28 -0
- package/dist/feed-player/sidebar/sidebar-tab-bar.svelte +45 -0
- package/dist/feed-player/sidebar/sidebar-tab-bar.svelte.d.ts +20 -0
- package/dist/feed-player/sidebar/types.d.ts +4 -0
- package/dist/feed-player/sidebar/types.js +1 -0
- package/dist/feed-player/types.d.ts +62 -0
- package/dist/feed-player/types.js +1 -0
- package/dist/posts/attachments/cmp.attachments.svelte +7 -2
- package/dist/posts/post-viewer/cmp.post-viewer.svelte +10 -7
- package/dist/posts/post-viewer/media/post-media.svelte +1 -2
- package/dist/posts/posts-player/posts-player-view.svelte +2 -0
- package/dist/products/product-card/cmp.product-card.svelte +16 -12
- package/dist/streams/streams-player/streams-player-view.svelte +2 -0
- package/dist/ui/media-items/media-item-view/cmp.media-item-view.svelte +1 -1
- package/package.json +7 -3
|
@@ -35,8 +35,8 @@ const handleAdClick = () => {
|
|
|
35
35
|
|
|
36
36
|
<div
|
|
37
37
|
class="ad-card"
|
|
38
|
-
style:--ad-card--cta
|
|
39
|
-
style:--ad-card--cta
|
|
38
|
+
style:--_--ad-card--cta--background={ad.ctaButton?.background}
|
|
39
|
+
style:--_--ad-card--cta--text-color={ad.ctaButton?.textColor}
|
|
40
40
|
inert={inert}
|
|
41
41
|
use:trackImpression>
|
|
42
42
|
<div class="ad-card__image">
|
|
@@ -81,6 +81,10 @@ const handleAdClick = () => {
|
|
|
81
81
|
--_ad-card--background-color: var(--ad-card--background-color, rgb(from light-dark(#ffffff, #000000) r g b / 90%));
|
|
82
82
|
--_ad-card--border-color: var(--ad-card--border-color, light-dark(#f2f2f2, #000000));
|
|
83
83
|
--_ad-card--price-color: var(--ad-card--price-color, inherit);
|
|
84
|
+
--_ad-card--text--primary: var(--ad-card--text--primary, light-dark(#000000, #ffffff));
|
|
85
|
+
--_ad-card--text-secondary: var(--ad-card--text-secondary, light-dark(#6b7280, #d1d5db));
|
|
86
|
+
--_ad-card--cta--background: var(--_--ad-card--cta--background, light-dark(#ffffff, #111827));
|
|
87
|
+
--_ad-card--cta--text-color: var(--_--ad-card--cta--text-color, light-dark(#000000, #ffffff));
|
|
84
88
|
width: 100%;
|
|
85
89
|
height: max-content;
|
|
86
90
|
display: flex;
|
|
@@ -174,6 +178,7 @@ const handleAdClick = () => {
|
|
|
174
178
|
overflow: hidden;
|
|
175
179
|
}
|
|
176
180
|
.ad-card__title {
|
|
181
|
+
color: var(--_ad-card--text--primary);
|
|
177
182
|
font-weight: 700;
|
|
178
183
|
font-size: 1.125rem;
|
|
179
184
|
line-height: 1.375rem;
|
|
@@ -189,7 +194,7 @@ const handleAdClick = () => {
|
|
|
189
194
|
}
|
|
190
195
|
.ad-card__description {
|
|
191
196
|
font-weight: 400;
|
|
192
|
-
color: var(--
|
|
197
|
+
color: var(--_ad-card--text-secondary);
|
|
193
198
|
font-size: 0.9375rem;
|
|
194
199
|
line-height: 1.375rem;
|
|
195
200
|
min-height: 1.375rem;
|
|
@@ -227,7 +232,7 @@ const handleAdClick = () => {
|
|
|
227
232
|
line-height: 1.0913rem;
|
|
228
233
|
letter-spacing: 0;
|
|
229
234
|
text-align: right;
|
|
230
|
-
color: var(--
|
|
235
|
+
color: var(--_ad-card--text-secondary);
|
|
231
236
|
white-space: nowrap;
|
|
232
237
|
overflow: hidden;
|
|
233
238
|
text-overflow: ellipsis;
|
|
@@ -243,11 +248,8 @@ const handleAdClick = () => {
|
|
|
243
248
|
}
|
|
244
249
|
.ad-card__button {
|
|
245
250
|
width: 100%;
|
|
246
|
-
--sc-kit--button--background:
|
|
247
|
-
|
|
248
|
-
var(--sc-player--dark--card-button, var(--ad-card--cta-background))
|
|
249
|
-
);
|
|
250
|
-
--sc-kit--button--font--color: var(--ad-card--cta-text-color);
|
|
251
|
+
--sc-kit--button--background: var(--_ad-card--cta--background);
|
|
252
|
+
--sc-kit--button--font--color: var(--_ad-card--cta--text-color);
|
|
251
253
|
}
|
|
252
254
|
.ad-card__button :global(*) {
|
|
253
255
|
width: 100%;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">import IconDismiss from '@fluentui/svg-icons/icons/dismiss_20_regular.svg?raw';
|
|
2
|
+
import { Icon } from '@streamscloud/kit/ui/icon';
|
|
3
|
+
const { on } = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<button type="button" class="close-button" onclick={() => on?.click?.()}>
|
|
7
|
+
<Icon src={IconDismiss} />
|
|
8
|
+
</button>
|
|
9
|
+
|
|
10
|
+
<!--
|
|
11
|
+
@component
|
|
12
|
+
Round close button — 32px circle with dismiss icon.
|
|
13
|
+
|
|
14
|
+
### CSS Custom Properties
|
|
15
|
+
| Property | Description | Default |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `--sc-fp--close-button--size` | Button size | `32px` |
|
|
18
|
+
| `--sc-fp--close-button--background` | Button background | `light-dark(#f1f6fd, #2a2a2a)` |
|
|
19
|
+
| `--sc-fp--close-button--color` | Icon color | `currentColor` |
|
|
20
|
+
| `--sc-fp--close-button--icon-size` | Icon size | `16px` |
|
|
21
|
+
-->
|
|
22
|
+
|
|
23
|
+
<style>.close-button {
|
|
24
|
+
--_close-button--size: var(--sc-fp--close-button--size, 2rem);
|
|
25
|
+
--_close-button--background: var(--sc-fp--close-button--background, light-dark(#f1f6fd, #2a2a2a));
|
|
26
|
+
--_close-button--color: var(--sc-fp--close-button--color, light-dark(#000000, #ffffff));
|
|
27
|
+
--_close-button--icon-size: var(--sc-fp--close-button--icon-size, 1rem);
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
width: var(--_close-button--size);
|
|
32
|
+
height: var(--_close-button--size);
|
|
33
|
+
padding: 0;
|
|
34
|
+
background: var(--_close-button--background);
|
|
35
|
+
border: none;
|
|
36
|
+
border-radius: 50%;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
--sc-kit--icon--size: var(--_close-button--icon-size);
|
|
39
|
+
--sc-kit--icon--color: var(--_close-button--color);
|
|
40
|
+
}</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
on?: {
|
|
3
|
+
click?: () => void;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Round close button — 32px circle with dismiss icon.
|
|
8
|
+
*
|
|
9
|
+
* ### CSS Custom Properties
|
|
10
|
+
* | Property | Description | Default |
|
|
11
|
+
* |---|---|---|
|
|
12
|
+
* | `--sc-fp--close-button--size` | Button size | `32px` |
|
|
13
|
+
* | `--sc-fp--close-button--background` | Button background | `light-dark(#f1f6fd, #2a2a2a)` |
|
|
14
|
+
* | `--sc-fp--close-button--color` | Icon color | `currentColor` |
|
|
15
|
+
* | `--sc-fp--close-button--icon-size` | Icon size | `16px` |
|
|
16
|
+
*/
|
|
17
|
+
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type Cmp = ReturnType<typeof Cmp>;
|
|
19
|
+
export default Cmp;
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
<script lang="ts">import { default as CloseButton } from './cmp.close-button.svelte';
|
|
2
|
+
import { PostActionsGenerator } from '../posts/controls';
|
|
3
|
+
import { getPostCoverImage, PostModel } from '../posts/model';
|
|
4
|
+
import { PostViewer } from '../posts/post-viewer';
|
|
5
|
+
import { FeedPlayerLocalization } from './feed-player-localization';
|
|
6
|
+
import { default as SidebarPanel } from './sidebar/sidebar-panel.svelte';
|
|
7
|
+
import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_28_regular.svg?raw';
|
|
8
|
+
import IconChevronUp from '@fluentui/svg-icons/icons/chevron_up_28_regular.svg?raw';
|
|
9
|
+
import IconInfo from '@fluentui/svg-icons/icons/info_24_regular.svg?raw';
|
|
10
|
+
import { AppLocale } from '@streamscloud/kit/core/locale';
|
|
11
|
+
import { slideHorizontally } from '@streamscloud/kit/core/transitions';
|
|
12
|
+
import { preloadImage } from '@streamscloud/kit/core/utils';
|
|
13
|
+
import { Loading } from '@streamscloud/kit/ui/loading';
|
|
14
|
+
import { PlayerButtons } from '@streamscloud/kit/ui/player/buttons';
|
|
15
|
+
import { FeedSlider } from '@streamscloud/kit/ui/player/feed-slider';
|
|
16
|
+
import { initBufferFromProvider } from '@streamscloud/kit/ui/player/providers';
|
|
17
|
+
import { untrack } from 'svelte';
|
|
18
|
+
let { dataProvider, socialInteractionsHandler, sharingHandler, analyticsHandler, recommendedHandler, playlistHandler, trackingParams: externalTrackingParams, settings, header, on } = $props();
|
|
19
|
+
$effect(() => {
|
|
20
|
+
if (settings?.locale) {
|
|
21
|
+
AppLocale.change(settings.locale);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const localization = new FeedPlayerLocalization();
|
|
25
|
+
// ── Background mode ──
|
|
26
|
+
const isGlassBackground = $derived(settings?.background === 'glass');
|
|
27
|
+
const isTransparentBackground = $derived(settings?.background === 'transparent');
|
|
28
|
+
let backgroundImageUrl = $state(null);
|
|
29
|
+
// ── Buffer ──
|
|
30
|
+
let buffer = $state.raw(null);
|
|
31
|
+
$effect(() => {
|
|
32
|
+
void dataProvider;
|
|
33
|
+
untrack(() => {
|
|
34
|
+
buffer = null;
|
|
35
|
+
backgroundImageUrl = null;
|
|
36
|
+
mappedPostsCache = {};
|
|
37
|
+
initBuffer();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const initBuffer = async () => {
|
|
41
|
+
const newBuffer = initBufferFromProvider(dataProvider);
|
|
42
|
+
await newBuffer.ensureWarmedUp();
|
|
43
|
+
if (newBuffer.loaded.length) {
|
|
44
|
+
const coverUrl = getPostCoverImage(newBuffer.loaded[0]);
|
|
45
|
+
await preloadImage(coverUrl);
|
|
46
|
+
backgroundImageUrl = coverUrl;
|
|
47
|
+
}
|
|
48
|
+
buffer = newBuffer;
|
|
49
|
+
};
|
|
50
|
+
// ── PostModel cache ──
|
|
51
|
+
let mappedPostsCache = {};
|
|
52
|
+
const itemAsPostModel = (item) => {
|
|
53
|
+
if (mappedPostsCache[item.id]) {
|
|
54
|
+
return mappedPostsCache[item.id];
|
|
55
|
+
}
|
|
56
|
+
const postModel = new PostModel(item);
|
|
57
|
+
mappedPostsCache[item.id] = postModel;
|
|
58
|
+
return postModel;
|
|
59
|
+
};
|
|
60
|
+
// ── Current post ──
|
|
61
|
+
const currentPostModel = $derived.by(() => {
|
|
62
|
+
if (!buffer?.current) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return itemAsPostModel(buffer.current);
|
|
66
|
+
});
|
|
67
|
+
const currentTrackingParams = $derived.by(() => {
|
|
68
|
+
if (externalTrackingParams === false) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const shortVideoId = currentPostModel?.postType === 'SHORT_VIDEO' ? currentPostModel.id : undefined;
|
|
72
|
+
if (!externalTrackingParams && !shortVideoId) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return { ...externalTrackingParams, shortVideoId };
|
|
76
|
+
});
|
|
77
|
+
const hasProducts = $derived(!!currentPostModel?.attachments && currentPostModel.attachments.products.length > 0);
|
|
78
|
+
const isArticle = $derived(currentPostModel?.postType === 'ARTICLE' && !!currentPostModel.articleId);
|
|
79
|
+
const hasAttachments = $derived(!!currentPostModel?.attachments);
|
|
80
|
+
// ── Sidebar tabs ──
|
|
81
|
+
let preferredTab = $state('information');
|
|
82
|
+
let activeSidebarTab = $state('information');
|
|
83
|
+
let selectedProductId = $state(null);
|
|
84
|
+
const visibleTabs = $derived.by(() => {
|
|
85
|
+
const tabs = [];
|
|
86
|
+
if (hasAttachments) {
|
|
87
|
+
tabs.push({ id: 'information', label: localization.tabInformation });
|
|
88
|
+
}
|
|
89
|
+
if (hasProducts) {
|
|
90
|
+
tabs.push({ id: 'product', label: localization.tabProduct });
|
|
91
|
+
}
|
|
92
|
+
if (isArticle) {
|
|
93
|
+
tabs.push({ id: 'article', label: localization.tabArticle });
|
|
94
|
+
}
|
|
95
|
+
if (recommendedHandler) {
|
|
96
|
+
tabs.push({ id: 'recommended', label: localization.tabRecommended });
|
|
97
|
+
}
|
|
98
|
+
if (playlistHandler) {
|
|
99
|
+
tabs.push({ id: 'playlist', label: localization.tabPlaylist });
|
|
100
|
+
}
|
|
101
|
+
return tabs;
|
|
102
|
+
});
|
|
103
|
+
const sidebarVisible = $derived(visibleTabs.length > 0);
|
|
104
|
+
const handleTabChange = (id) => {
|
|
105
|
+
preferredTab = id;
|
|
106
|
+
activeSidebarTab = id;
|
|
107
|
+
};
|
|
108
|
+
// Tab resolution: preferred tab if available, otherwise first visible
|
|
109
|
+
$effect(() => {
|
|
110
|
+
const tabs = visibleTabs;
|
|
111
|
+
if (tabs.length === 0) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (tabs.some((t) => t.id === preferredTab)) {
|
|
115
|
+
activeSidebarTab = preferredTab;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
activeSidebarTab = tabs[0].id;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Reset selectedProductId on post change
|
|
122
|
+
$effect(() => {
|
|
123
|
+
const post = currentPostModel;
|
|
124
|
+
untrack(() => {
|
|
125
|
+
if (post?.attachments && post.attachments.products.length > 0) {
|
|
126
|
+
selectedProductId = post.attachments.products[0].id;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
selectedProductId = null;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// ── Sidebar collapsed (user toggle) ──
|
|
134
|
+
let sidebarCollapsed = $state(false);
|
|
135
|
+
// ── Layout ──
|
|
136
|
+
const SAFE_AREA_SIZE = 80;
|
|
137
|
+
const SIDEBAR_WIDTH = 378;
|
|
138
|
+
const MOBILE_BREAKPOINT = 576;
|
|
139
|
+
// Measured from the root element
|
|
140
|
+
let totalWidth = $state(0);
|
|
141
|
+
let isMobileView = $derived(totalWidth <= MOBILE_BREAKPOINT);
|
|
142
|
+
const handleRootResize = (node) => {
|
|
143
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
144
|
+
totalWidth = entry.contentRect.width;
|
|
145
|
+
});
|
|
146
|
+
observer.observe(node);
|
|
147
|
+
return { destroy: () => observer.disconnect() };
|
|
148
|
+
};
|
|
149
|
+
// Measured from video-area
|
|
150
|
+
let contentViewWidth = $state(0);
|
|
151
|
+
let videoAreaWidth = $state(0);
|
|
152
|
+
let videoAreaHeight = $state(0);
|
|
153
|
+
// Priority: sidebar hides first, then video shrinks, then controls go overlay
|
|
154
|
+
// Step 1: Would sidebar + video + safe areas fit?
|
|
155
|
+
const sidebarCanBeShown = $derived.by(() => {
|
|
156
|
+
if (!sidebarVisible || isMobileView || videoAreaHeight === 0) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
// Estimate video width from height at 9:16
|
|
160
|
+
const videoW = videoAreaHeight * (9 / 16);
|
|
161
|
+
const areaWithSidebar = totalWidth - SIDEBAR_WIDTH;
|
|
162
|
+
const sideSpace = (areaWithSidebar - videoW) / 2;
|
|
163
|
+
return sideSpace >= SAFE_AREA_SIZE;
|
|
164
|
+
});
|
|
165
|
+
const sidebarWidth = $derived(sidebarCanBeShown && !sidebarCollapsed ? SIDEBAR_WIDTH : 0);
|
|
166
|
+
// Step 2: Controls overlay — only when side space without sidebar is still too small
|
|
167
|
+
const sidePanelsMaxWidth = $derived((videoAreaWidth - contentViewWidth) / 2);
|
|
168
|
+
const showControlsOverlay = $derived(isMobileView);
|
|
169
|
+
// ResizeObserver on video-area — same logic as generic player's handleSliderMounted
|
|
170
|
+
const handleVideoAreaResize = (node) => {
|
|
171
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
172
|
+
const { width: areaW, height: areaH } = entry.contentRect;
|
|
173
|
+
videoAreaWidth = areaW;
|
|
174
|
+
videoAreaHeight = areaH;
|
|
175
|
+
const ratio = 9 / 16;
|
|
176
|
+
let width;
|
|
177
|
+
let height;
|
|
178
|
+
let margin;
|
|
179
|
+
let contentWidthNumber = areaW;
|
|
180
|
+
if (isMobileView) {
|
|
181
|
+
width = '100%';
|
|
182
|
+
height = '100%';
|
|
183
|
+
margin = '0';
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// On desktop, reserve safe area on each side for controls
|
|
187
|
+
const maxContentWidth = areaW - 2 * SAFE_AREA_SIZE;
|
|
188
|
+
const heightBasedWidth = areaH * ratio;
|
|
189
|
+
contentWidthNumber = Math.min(maxContentWidth, heightBasedWidth);
|
|
190
|
+
width = `${contentWidthNumber}px`;
|
|
191
|
+
height = `${contentWidthNumber / ratio}px`;
|
|
192
|
+
margin = 'auto';
|
|
193
|
+
}
|
|
194
|
+
node.style.setProperty('--_feed-player--content--width', width);
|
|
195
|
+
node.style.setProperty('--_feed-player--content--height', height);
|
|
196
|
+
node.style.setProperty('--_feed-player--content--margin', margin);
|
|
197
|
+
contentViewWidth = contentWidthNumber;
|
|
198
|
+
});
|
|
199
|
+
observer.observe(node);
|
|
200
|
+
return { destroy: () => observer.disconnect() };
|
|
201
|
+
};
|
|
202
|
+
// ── Callbacks ──
|
|
203
|
+
const handleItemActivated = (id) => {
|
|
204
|
+
if (buffer) {
|
|
205
|
+
const item = buffer.loaded.find((i) => i.id === id);
|
|
206
|
+
if (item) {
|
|
207
|
+
backgroundImageUrl = getPostCoverImage(item);
|
|
208
|
+
if (item.postType === 'SHORT_VIDEO') {
|
|
209
|
+
analyticsHandler?.trackShortVideoView(id);
|
|
210
|
+
}
|
|
211
|
+
else if (item.postType) {
|
|
212
|
+
analyticsHandler?.trackPostOpened(item.postType, id);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
on?.postActivated?.(id);
|
|
217
|
+
};
|
|
218
|
+
// ── Analytics callbacks ──
|
|
219
|
+
const onProductClick = (productId) => {
|
|
220
|
+
if (!currentPostModel) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (currentPostModel.postType === 'SHORT_VIDEO') {
|
|
224
|
+
analyticsHandler?.trackShortVideoProductClick(productId, currentPostModel.id);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const onProductImpression = (productId) => {
|
|
228
|
+
if (!currentPostModel) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (currentPostModel.postType === 'SHORT_VIDEO') {
|
|
232
|
+
analyticsHandler?.trackShortVideoProductImpression(productId, currentPostModel.id);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const onAdClick = (adId) => {
|
|
236
|
+
analyticsHandler?.trackAdClick(adId);
|
|
237
|
+
};
|
|
238
|
+
const onAdImpression = (adId) => {
|
|
239
|
+
analyticsHandler?.trackAdImpression(adId);
|
|
240
|
+
};
|
|
241
|
+
// ── Action buttons (like, share, attachments toggle) ──
|
|
242
|
+
const postActionsGenerator = untrack(() => new PostActionsGenerator({
|
|
243
|
+
socialInteractionsHandler,
|
|
244
|
+
sharingHandler,
|
|
245
|
+
on: { attachmentClicked: () => (sidebarCollapsed = !sidebarCollapsed) }
|
|
246
|
+
}));
|
|
247
|
+
const itemActions = $derived.by(() => {
|
|
248
|
+
if (!currentPostModel) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
const handler = postActionsGenerator.getPostActionsHandler(currentPostModel);
|
|
252
|
+
const actions = [...handler.actions];
|
|
253
|
+
if (sidebarCanBeShown) {
|
|
254
|
+
actions.push({ icon: IconInfo, callback: () => (sidebarCollapsed = !sidebarCollapsed) });
|
|
255
|
+
}
|
|
256
|
+
return actions;
|
|
257
|
+
});
|
|
258
|
+
</script>
|
|
259
|
+
|
|
260
|
+
<div
|
|
261
|
+
class="feed-player"
|
|
262
|
+
class:feed-player--glass={isGlassBackground}
|
|
263
|
+
class:feed-player--glass-active={isGlassBackground && backgroundImageUrl}
|
|
264
|
+
class:feed-player--transparent={isTransparentBackground}
|
|
265
|
+
style:--_feed-player--background-image-url={backgroundImageUrl ? `url(${backgroundImageUrl})` : undefined}
|
|
266
|
+
use:handleRootResize>
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
<div class="feed-player__main">
|
|
270
|
+
{#if header}
|
|
271
|
+
<div class="feed-player__header">
|
|
272
|
+
{@render header()}
|
|
273
|
+
</div>
|
|
274
|
+
{/if}
|
|
275
|
+
{#if buffer}
|
|
276
|
+
<div class="feed-player__video-area" class:feed-player__video-area--with-header={!!header} use:handleVideoAreaResize>
|
|
277
|
+
<FeedSlider buffer={buffer} on={{ itemActivated: handleItemActivated }}>
|
|
278
|
+
{#snippet children({ item })}
|
|
279
|
+
{@const postModel = itemAsPostModel(item)}
|
|
280
|
+
<div class="feed-player__content">
|
|
281
|
+
<PostViewer
|
|
282
|
+
model={postModel}
|
|
283
|
+
controlActions={itemActions}
|
|
284
|
+
trackingParams={externalTrackingParams ?? null}
|
|
285
|
+
enableAttachments={false}
|
|
286
|
+
enableControls={showControlsOverlay}
|
|
287
|
+
autoplay="on-appearance"
|
|
288
|
+
on={{
|
|
289
|
+
productClick: onProductClick,
|
|
290
|
+
productImpression: onProductImpression,
|
|
291
|
+
adClick: onAdClick,
|
|
292
|
+
adImpression: onAdImpression,
|
|
293
|
+
articleReadMore: (id) => on?.articleReadMore?.(id)
|
|
294
|
+
}} />
|
|
295
|
+
</div>
|
|
296
|
+
{/snippet}
|
|
297
|
+
</FeedSlider>
|
|
298
|
+
|
|
299
|
+
{#if !showControlsOverlay}
|
|
300
|
+
<div class="feed-player__controls" style:--_feed-player--controls-width="{sidePanelsMaxWidth}px">
|
|
301
|
+
<div class="feed-player__controls-spacer"> </div>
|
|
302
|
+
<div class="feed-player__controls-left">
|
|
303
|
+
<PlayerButtons actions={itemActions} scaleEffect={true} />
|
|
304
|
+
|
|
305
|
+
{#if buffer && buffer.loaded.length > 1}
|
|
306
|
+
<div class="feed-player__navigation">
|
|
307
|
+
<PlayerButtons actions={[{ icon: IconChevronUp, callback: buffer.loadPrevious, disabled: !buffer.canLoadPrevious }]} scaleEffect={true} />
|
|
308
|
+
<PlayerButtons actions={[{ icon: IconChevronDown, callback: buffer.loadNext, disabled: !buffer.canLoadNext }]} scaleEffect={true} />
|
|
309
|
+
</div>
|
|
310
|
+
{/if}
|
|
311
|
+
</div>
|
|
312
|
+
<div class="feed-player__controls-spacer feed-player__controls-spacer--right"> </div>
|
|
313
|
+
</div>
|
|
314
|
+
{/if}
|
|
315
|
+
</div>
|
|
316
|
+
{/if}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
</div>
|
|
320
|
+
{#if buffer}
|
|
321
|
+
<div class="feed-player__sidebar-container">
|
|
322
|
+
{#if on?.closed}
|
|
323
|
+
<div class="feed-player__close">
|
|
324
|
+
<CloseButton on={{ click: on.closed }} />
|
|
325
|
+
</div>
|
|
326
|
+
{/if}
|
|
327
|
+
|
|
328
|
+
{#if sidebarWidth > 0 && currentPostModel}
|
|
329
|
+
<div class="feed-player__sidebar" class:feed-player__sidebar--with-close={on?.closed} style:--_--feed-player--sidebar--width={SIDEBAR_WIDTH+'px'}
|
|
330
|
+
transition:slideHorizontally|local>
|
|
331
|
+
<SidebarPanel
|
|
332
|
+
tabs={visibleTabs}
|
|
333
|
+
activeTabId={activeSidebarTab}
|
|
334
|
+
model={currentPostModel}
|
|
335
|
+
trackingParams={currentTrackingParams}
|
|
336
|
+
selectedProductId={selectedProductId}
|
|
337
|
+
{recommendedHandler}
|
|
338
|
+
{playlistHandler}
|
|
339
|
+
on={{
|
|
340
|
+
tabChange: handleTabChange,
|
|
341
|
+
productClick: onProductClick,
|
|
342
|
+
productImpression: onProductImpression,
|
|
343
|
+
productBuy: on?.productBuy,
|
|
344
|
+
productSelect: (id) => {
|
|
345
|
+
selectedProductId = id;
|
|
346
|
+
handleTabChange('product');
|
|
347
|
+
},
|
|
348
|
+
adClick: onAdClick,
|
|
349
|
+
adImpression: onAdImpression,
|
|
350
|
+
articleReadMore: on?.articleReadMore,
|
|
351
|
+
postActivate: (id) => buffer?.tryActivateItemById(id),
|
|
352
|
+
playlistSelect: (id) => on?.playlistSelect?.(id)
|
|
353
|
+
}} />
|
|
354
|
+
</div>
|
|
355
|
+
{/if}
|
|
356
|
+
</div>
|
|
357
|
+
{/if}
|
|
358
|
+
{#if !buffer}
|
|
359
|
+
<Loading positionAbsoluteCenter={true} timeout={500} />
|
|
360
|
+
{/if}
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<!--
|
|
364
|
+
@component
|
|
365
|
+
Vertical post feed player with a tabbed sidebar for products, articles, and recommendations.
|
|
366
|
+
|
|
367
|
+
### CSS Custom Properties
|
|
368
|
+
| Property | Description | Default |
|
|
369
|
+
|---|---|---|
|
|
370
|
+
| `--sc-fp--background` | Player background | `light-dark(white, black)` |
|
|
371
|
+
| `--sc-fp--button--color` | Control button color | inherited |
|
|
372
|
+
| `--sc-fp--button--color--inactive` | Inactive control button color | inherited |
|
|
373
|
+
-->
|
|
374
|
+
|
|
375
|
+
<style>.feed-player {
|
|
376
|
+
--_feed-player--background: var(--sc-fp--background, light-dark(#ffffff, #000000));
|
|
377
|
+
--_feed-player--button--color: var(--sc-fp--button--color);
|
|
378
|
+
--_feed-player--button--color--inactive: var(--sc-fp--button--color--inactive);
|
|
379
|
+
--post-viewer--button--color: var(--_feed-player--button--color);
|
|
380
|
+
--post-viewer--button--color--inactive: var(--_feed-player--button--color--inactive);
|
|
381
|
+
position: relative;
|
|
382
|
+
width: 100%;
|
|
383
|
+
height: 100%;
|
|
384
|
+
display: flex;
|
|
385
|
+
background: var(--_feed-player--background);
|
|
386
|
+
overflow: hidden;
|
|
387
|
+
container-type: inline-size;
|
|
388
|
+
}
|
|
389
|
+
.feed-player::before {
|
|
390
|
+
content: "";
|
|
391
|
+
position: absolute;
|
|
392
|
+
inset: 0;
|
|
393
|
+
backdrop-filter: blur(1.875rem);
|
|
394
|
+
background-color: rgb(from var(--_feed-player--background) r g b/50%);
|
|
395
|
+
display: none;
|
|
396
|
+
z-index: 0;
|
|
397
|
+
}
|
|
398
|
+
.feed-player--glass::before {
|
|
399
|
+
display: block;
|
|
400
|
+
}
|
|
401
|
+
.feed-player--glass-active {
|
|
402
|
+
background-image: var(--_feed-player--background-image-url);
|
|
403
|
+
background-size: cover;
|
|
404
|
+
background-position: center;
|
|
405
|
+
}
|
|
406
|
+
.feed-player--transparent {
|
|
407
|
+
background: transparent;
|
|
408
|
+
}
|
|
409
|
+
.feed-player__main {
|
|
410
|
+
display: flex;
|
|
411
|
+
flex-direction: column;
|
|
412
|
+
width: 100%;
|
|
413
|
+
flex: 1;
|
|
414
|
+
min-height: 0;
|
|
415
|
+
position: relative;
|
|
416
|
+
z-index: 1;
|
|
417
|
+
}
|
|
418
|
+
.feed-player__header {
|
|
419
|
+
position: relative;
|
|
420
|
+
flex-shrink: 0;
|
|
421
|
+
display: flex;
|
|
422
|
+
justify-content: center;
|
|
423
|
+
align-items: center;
|
|
424
|
+
z-index: 1;
|
|
425
|
+
pointer-events: none;
|
|
426
|
+
/* Set 'container-type: inline-size;' to reference container*/
|
|
427
|
+
}
|
|
428
|
+
@container (width < 576px) {
|
|
429
|
+
.feed-player__header {
|
|
430
|
+
position: absolute;
|
|
431
|
+
top: 0;
|
|
432
|
+
left: 0;
|
|
433
|
+
right: 0;
|
|
434
|
+
z-index: 2;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
.feed-player__video-area {
|
|
438
|
+
flex: 1;
|
|
439
|
+
min-width: 0;
|
|
440
|
+
min-height: 0;
|
|
441
|
+
position: relative;
|
|
442
|
+
padding-bottom: 0.625rem;
|
|
443
|
+
overflow: hidden;
|
|
444
|
+
}
|
|
445
|
+
.feed-player__video-area--with-header {
|
|
446
|
+
padding-top: 0;
|
|
447
|
+
}
|
|
448
|
+
.feed-player__video-area {
|
|
449
|
+
/* Set 'container-type: inline-size;' to reference container*/
|
|
450
|
+
}
|
|
451
|
+
@container (width < 576px) {
|
|
452
|
+
.feed-player__video-area {
|
|
453
|
+
padding: 0;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
.feed-player__content {
|
|
457
|
+
width: var(--_feed-player--content--width, 100%);
|
|
458
|
+
height: var(--_feed-player--content--height, 100%);
|
|
459
|
+
margin: var(--_feed-player--content--margin, 0);
|
|
460
|
+
position: relative;
|
|
461
|
+
display: flex;
|
|
462
|
+
justify-content: center;
|
|
463
|
+
align-items: center;
|
|
464
|
+
}
|
|
465
|
+
.feed-player__controls {
|
|
466
|
+
position: absolute;
|
|
467
|
+
top: 0;
|
|
468
|
+
right: 0;
|
|
469
|
+
height: 100%;
|
|
470
|
+
width: var(--_feed-player--controls-width);
|
|
471
|
+
display: flex;
|
|
472
|
+
padding: 0.625rem 0 1.875rem;
|
|
473
|
+
pointer-events: none;
|
|
474
|
+
}
|
|
475
|
+
.feed-player__controls-spacer {
|
|
476
|
+
flex: 0 0 1rem;
|
|
477
|
+
}
|
|
478
|
+
.feed-player__controls-spacer--right {
|
|
479
|
+
flex: 1;
|
|
480
|
+
}
|
|
481
|
+
.feed-player__controls-left {
|
|
482
|
+
display: flex;
|
|
483
|
+
flex-direction: column;
|
|
484
|
+
gap: 2.3125rem;
|
|
485
|
+
justify-content: flex-end;
|
|
486
|
+
align-items: center;
|
|
487
|
+
pointer-events: auto;
|
|
488
|
+
}
|
|
489
|
+
.feed-player__navigation {
|
|
490
|
+
display: flex;
|
|
491
|
+
flex-direction: column;
|
|
492
|
+
gap: 1rem;
|
|
493
|
+
}
|
|
494
|
+
.feed-player__sidebar-container {
|
|
495
|
+
flex-shrink: 0;
|
|
496
|
+
display: flex;
|
|
497
|
+
flex-direction: column;
|
|
498
|
+
align-items: flex-end;
|
|
499
|
+
position: relative;
|
|
500
|
+
z-index: 1;
|
|
501
|
+
}
|
|
502
|
+
.feed-player__close {
|
|
503
|
+
position: absolute;
|
|
504
|
+
top: 0.75rem;
|
|
505
|
+
right: 1rem;
|
|
506
|
+
z-index: 1;
|
|
507
|
+
}
|
|
508
|
+
.feed-player__sidebar {
|
|
509
|
+
flex: 1;
|
|
510
|
+
min-height: 0;
|
|
511
|
+
width: var(--_--feed-player--sidebar--width);
|
|
512
|
+
padding: 0.625rem 0.625rem 0.625rem 0;
|
|
513
|
+
}
|
|
514
|
+
.feed-player__sidebar--with-close {
|
|
515
|
+
padding-top: 3.75rem;
|
|
516
|
+
}</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FeedPlayerProps } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Vertical post feed player with a tabbed sidebar for products, articles, and recommendations.
|
|
4
|
+
*
|
|
5
|
+
* ### CSS Custom Properties
|
|
6
|
+
* | Property | Description | Default |
|
|
7
|
+
* |---|---|---|
|
|
8
|
+
* | `--sc-fp--background` | Player background | `light-dark(white, black)` |
|
|
9
|
+
* | `--sc-fp--button--color` | Control button color | inherited |
|
|
10
|
+
* | `--sc-fp--button--color--inactive` | Inactive control button color | inherited |
|
|
11
|
+
*/
|
|
12
|
+
declare const Cmp: import("svelte").Component<FeedPlayerProps, {}, "">;
|
|
13
|
+
type Cmp = ReturnType<typeof Cmp>;
|
|
14
|
+
export default Cmp;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class FeedPlayerLocalization {
|
|
2
|
+
get tabInformation(): string;
|
|
3
|
+
get tabProduct(): string;
|
|
4
|
+
get tabArticle(): string;
|
|
5
|
+
get tabRecommended(): string;
|
|
6
|
+
get tabPlaylist(): string;
|
|
7
|
+
get show(): string;
|
|
8
|
+
get relatedPosts(): string;
|
|
9
|
+
get suggestedPlaylist(): string;
|
|
10
|
+
get suggestedProducts(): string;
|
|
11
|
+
get showList(): string;
|
|
12
|
+
get updatedLabel(): string;
|
|
13
|
+
postsCount(count: number): string;
|
|
14
|
+
postOf(current: number, total: number): string;
|
|
15
|
+
viewsLabel(count: number): string;
|
|
16
|
+
}
|