@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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AppLocale } from '@streamscloud/kit/core/locale';
|
|
2
|
+
export class FeedPlayerLocalization {
|
|
3
|
+
get tabInformation() {
|
|
4
|
+
return loc.tabInformation[AppLocale.current];
|
|
5
|
+
}
|
|
6
|
+
get tabProduct() {
|
|
7
|
+
return loc.tabProduct[AppLocale.current];
|
|
8
|
+
}
|
|
9
|
+
get tabArticle() {
|
|
10
|
+
return loc.tabArticle[AppLocale.current];
|
|
11
|
+
}
|
|
12
|
+
get tabRecommended() {
|
|
13
|
+
return loc.tabRecommended[AppLocale.current];
|
|
14
|
+
}
|
|
15
|
+
get tabPlaylist() {
|
|
16
|
+
return loc.tabPlaylist[AppLocale.current];
|
|
17
|
+
}
|
|
18
|
+
get show() {
|
|
19
|
+
return loc.show[AppLocale.current];
|
|
20
|
+
}
|
|
21
|
+
get relatedPosts() {
|
|
22
|
+
return loc.relatedPosts[AppLocale.current];
|
|
23
|
+
}
|
|
24
|
+
get suggestedPlaylist() {
|
|
25
|
+
return loc.suggestedPlaylist[AppLocale.current];
|
|
26
|
+
}
|
|
27
|
+
get suggestedProducts() {
|
|
28
|
+
return loc.suggestedProducts[AppLocale.current];
|
|
29
|
+
}
|
|
30
|
+
get showList() {
|
|
31
|
+
return loc.showList[AppLocale.current];
|
|
32
|
+
}
|
|
33
|
+
get updatedLabel() {
|
|
34
|
+
return loc.updatedLabel[AppLocale.current];
|
|
35
|
+
}
|
|
36
|
+
postsCount(count) {
|
|
37
|
+
return loc.postsCount[AppLocale.current](count);
|
|
38
|
+
}
|
|
39
|
+
postOf(current, total) {
|
|
40
|
+
return loc.postOf[AppLocale.current](current, total);
|
|
41
|
+
}
|
|
42
|
+
viewsLabel(count) {
|
|
43
|
+
return loc.viewsLabel[AppLocale.current](count);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const loc = {
|
|
47
|
+
tabInformation: { en: 'Information', no: 'Informasjon' },
|
|
48
|
+
tabProduct: { en: 'Product', no: 'Produkt' },
|
|
49
|
+
tabArticle: { en: 'Article', no: 'Artikkel' },
|
|
50
|
+
tabRecommended: { en: 'Recommended', no: 'Anbefalt' },
|
|
51
|
+
tabPlaylist: { en: 'Playlist', no: 'Spilleliste' },
|
|
52
|
+
show: { en: 'Show', no: 'Vis' },
|
|
53
|
+
relatedPosts: { en: 'Related Posts', no: 'Relaterte innlegg' },
|
|
54
|
+
suggestedPlaylist: { en: 'Suggested Playlist', no: 'Foreslått spilleliste' },
|
|
55
|
+
suggestedProducts: { en: 'Suggested Products', no: 'Foreslåtte produkter' },
|
|
56
|
+
showList: { en: 'Show list', no: 'Vis liste' },
|
|
57
|
+
updatedLabel: { en: 'Updated:', no: 'Oppdatert:' },
|
|
58
|
+
postsCount: {
|
|
59
|
+
en: (count) => `${count} Post${count !== 1 ? 's' : ''}`,
|
|
60
|
+
no: (count) => `${count} innlegg`
|
|
61
|
+
},
|
|
62
|
+
postOf: {
|
|
63
|
+
en: (current, total) => `Post ${current} of ${total}`,
|
|
64
|
+
no: (current, total) => `Innlegg ${current} av ${total}`
|
|
65
|
+
},
|
|
66
|
+
viewsLabel: {
|
|
67
|
+
en: (count) => `${new Intl.NumberFormat('en').format(count)} Views`,
|
|
68
|
+
no: (count) => `${new Intl.NumberFormat('nb').format(count)} visninger`
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { default as CloseButton } from './cmp.close-button.svelte';
|
|
2
|
+
export { default as FeedPlayer } from './cmp.feed-player.svelte';
|
|
3
|
+
export type { FeedPlayerPlaylistData, FeedPlayerPostPreview, FeedPlayerProps, FeedPlayerRecommendedData, FeedPlayerSettings, FeedPlayerSuggestedPlaylist, IFeedPlayerPlaylistHandler, IFeedPlayerRecommendedHandler } from './types';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script lang="ts">import { FeedPlayerLocalization } from '../feed-player-localization';
|
|
2
|
+
import { Image } from '@streamscloud/kit/ui/image';
|
|
3
|
+
const localization = new FeedPlayerLocalization();
|
|
4
|
+
let { model, on } = $props();
|
|
5
|
+
const heroImage = $derived(model.media.items[0]?.isImage ? model.media.items[0].url : null);
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div class="article-tab">
|
|
9
|
+
{#if heroImage}
|
|
10
|
+
<div class="article-tab__hero">
|
|
11
|
+
<Image src={heroImage} />
|
|
12
|
+
</div>
|
|
13
|
+
{/if}
|
|
14
|
+
|
|
15
|
+
<div class="article-tab__content">
|
|
16
|
+
{#if model.texts.kicker}
|
|
17
|
+
<div class="article-tab__category">{model.texts.kicker}</div>
|
|
18
|
+
{/if}
|
|
19
|
+
|
|
20
|
+
{#if model.texts.title}
|
|
21
|
+
<div class="article-tab__title">{model.texts.title}</div>
|
|
22
|
+
{/if}
|
|
23
|
+
|
|
24
|
+
{#if model.texts.text}
|
|
25
|
+
<div class="article-tab__text">{model.texts.text}</div>
|
|
26
|
+
{/if}
|
|
27
|
+
|
|
28
|
+
{#if model.articleId && on?.readMore}
|
|
29
|
+
<button type="button" class="article-tab__show-button" onclick={() => model.articleId && on?.readMore && on.readMore(model.articleId)}
|
|
30
|
+
>{localization.show}</button>
|
|
31
|
+
{/if}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!--
|
|
36
|
+
@component
|
|
37
|
+
Article tab — preview of the article with hero image, title, text, and "Show" button.
|
|
38
|
+
|
|
39
|
+
### CSS Custom Properties
|
|
40
|
+
| Property | Description | Default |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `--article-tab--button-background` | Show button background | `#f9fafb` |
|
|
43
|
+
| `--article-tab--button-border` | Show button border | `#e5e7eb` |
|
|
44
|
+
-->
|
|
45
|
+
|
|
46
|
+
<style>.article-tab {
|
|
47
|
+
--_article-tab--button-background: var(--article-tab--button-background, #f9fafb);
|
|
48
|
+
--_article-tab--button-border: var(--article-tab--button-border, #e5e7eb);
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
--sc-kit--image--object-fit: cover;
|
|
52
|
+
--sc-kit--image--border-radius: 0.375rem 0.375rem 0 0;
|
|
53
|
+
}
|
|
54
|
+
.article-tab__hero {
|
|
55
|
+
width: 100%;
|
|
56
|
+
aspect-ratio: 392/245;
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
}
|
|
59
|
+
.article-tab__content {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
gap: 0.25rem;
|
|
63
|
+
padding: 1rem;
|
|
64
|
+
}
|
|
65
|
+
.article-tab__category {
|
|
66
|
+
font-size: 1rem;
|
|
67
|
+
font-weight: 500;
|
|
68
|
+
color: #9ca3af;
|
|
69
|
+
}
|
|
70
|
+
.article-tab__title {
|
|
71
|
+
font-size: 2rem;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
line-height: 1.2;
|
|
74
|
+
}
|
|
75
|
+
.article-tab__text {
|
|
76
|
+
font-size: 1.0625rem;
|
|
77
|
+
font-weight: 400;
|
|
78
|
+
line-height: 1.375rem;
|
|
79
|
+
display: -webkit-box;
|
|
80
|
+
-webkit-line-clamp: 10;
|
|
81
|
+
line-clamp: 10;
|
|
82
|
+
-webkit-box-orient: vertical;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
}
|
|
85
|
+
.article-tab__show-button {
|
|
86
|
+
margin-top: 0.5rem;
|
|
87
|
+
width: 100%;
|
|
88
|
+
height: 2.375rem;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
font-size: 0.875rem;
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
background: var(--_article-tab--button-background);
|
|
95
|
+
border: 1px solid var(--_article-tab--button-border);
|
|
96
|
+
border-radius: 0.5rem;
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
}</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PostModel } from '../../posts/model';
|
|
2
|
+
type Props = {
|
|
3
|
+
model: PostModel;
|
|
4
|
+
on?: {
|
|
5
|
+
readMore?: (articleId: string) => void;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Article tab — preview of the article with hero image, title, text, and "Show" button.
|
|
10
|
+
*
|
|
11
|
+
* ### CSS Custom Properties
|
|
12
|
+
* | Property | Description | Default |
|
|
13
|
+
* |---|---|---|
|
|
14
|
+
* | `--article-tab--button-background` | Show button background | `#f9fafb` |
|
|
15
|
+
* | `--article-tab--button-border` | Show button border | `#e5e7eb` |
|
|
16
|
+
*/
|
|
17
|
+
declare const ArticleTab: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type ArticleTab = ReturnType<typeof ArticleTab>;
|
|
19
|
+
export default ArticleTab;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">import { AdCard } from '../../ads/ad-card';
|
|
2
|
+
import { ProductCard } from '../../products/product-card';
|
|
3
|
+
import IconCheckmarkCircle24 from '@fluentui/svg-icons/icons/checkmark_circle_24_regular.svg?raw';
|
|
4
|
+
import { Icon } from '@streamscloud/kit/ui/icon';
|
|
5
|
+
let { model, trackingParams, selectedProductId, on } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
{#if model.attachments}
|
|
9
|
+
<div class="information-tab">
|
|
10
|
+
{#if model.attachments.ads.length}
|
|
11
|
+
<div class="information-tab__ads">
|
|
12
|
+
{#each model.attachments.ads as ad (ad.id)}
|
|
13
|
+
<div class="information-tab__ad-wrapper">
|
|
14
|
+
<AdCard
|
|
15
|
+
ad={ad}
|
|
16
|
+
{trackingParams}
|
|
17
|
+
on={{
|
|
18
|
+
click: on?.adClick,
|
|
19
|
+
impression: on?.adImpression
|
|
20
|
+
}} />
|
|
21
|
+
</div>
|
|
22
|
+
{/each}
|
|
23
|
+
</div>
|
|
24
|
+
{/if}
|
|
25
|
+
|
|
26
|
+
{#if model.attachments.products.length}
|
|
27
|
+
<div class="information-tab__products">
|
|
28
|
+
{#each model.attachments.products as product (product.id)}
|
|
29
|
+
<div
|
|
30
|
+
class="information-tab__product-wrapper"
|
|
31
|
+
onclick={() => on?.productSelect?.(product.id)}
|
|
32
|
+
onkeydown={() => {}}
|
|
33
|
+
role="none">
|
|
34
|
+
{#if selectedProductId === product.id}
|
|
35
|
+
<div class="information-tab__checkmark">
|
|
36
|
+
<Icon src={IconCheckmarkCircle24} />
|
|
37
|
+
</div>
|
|
38
|
+
{/if}
|
|
39
|
+
<ProductCard
|
|
40
|
+
product={product}
|
|
41
|
+
inert={true}
|
|
42
|
+
{trackingParams}
|
|
43
|
+
on={{
|
|
44
|
+
impression: on?.productImpression,
|
|
45
|
+
buy: on?.productBuy
|
|
46
|
+
}} />
|
|
47
|
+
</div>
|
|
48
|
+
{/each}
|
|
49
|
+
</div>
|
|
50
|
+
{/if}
|
|
51
|
+
</div>
|
|
52
|
+
{/if}
|
|
53
|
+
|
|
54
|
+
<!--
|
|
55
|
+
@component
|
|
56
|
+
Information tab — displays product and ad cards for the current post.
|
|
57
|
+
|
|
58
|
+
### CSS Custom Properties
|
|
59
|
+
| Property | Description | Default |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `--information-tab--checkmark-color` | Selected product checkmark color | `#0cce6b` |
|
|
62
|
+
| `--information-tab--gap` | Gap between cards | `16px` |
|
|
63
|
+
-->
|
|
64
|
+
|
|
65
|
+
<style>.information-tab {
|
|
66
|
+
--_information-tab--checkmark-color: var(--information-tab--checkmark-color, #ffffff);
|
|
67
|
+
--_information-tab--gap: var(--information-tab--gap, 1rem);
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
gap: var(--_information-tab--gap);
|
|
71
|
+
container-type: inline-size;
|
|
72
|
+
}
|
|
73
|
+
.information-tab__ads {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-wrap: wrap;
|
|
76
|
+
justify-content: flex-start;
|
|
77
|
+
gap: var(--_information-tab--gap);
|
|
78
|
+
}
|
|
79
|
+
.information-tab__ad-wrapper {
|
|
80
|
+
width: calc(50% - var(--_information-tab--gap) / 2);
|
|
81
|
+
}
|
|
82
|
+
.information-tab__products {
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-wrap: wrap;
|
|
85
|
+
justify-content: flex-start;
|
|
86
|
+
gap: var(--_information-tab--gap);
|
|
87
|
+
}
|
|
88
|
+
.information-tab__product-wrapper {
|
|
89
|
+
position: relative;
|
|
90
|
+
border-radius: 0.5rem;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
width: calc(50% - var(--_information-tab--gap) / 2);
|
|
93
|
+
}
|
|
94
|
+
.information-tab__checkmark {
|
|
95
|
+
--sc-kit--icon--size: 1.5rem;
|
|
96
|
+
--sc-kit--icon--color: var(--_information-tab--checkmark-color);
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: 0.5rem;
|
|
99
|
+
right: 0.5rem;
|
|
100
|
+
z-index: 1;
|
|
101
|
+
filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 0.5));
|
|
102
|
+
}</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TrackingParams } from '../../marketing-tracking';
|
|
2
|
+
import type { PostModel } from '../../posts/model';
|
|
3
|
+
type Props = {
|
|
4
|
+
model: PostModel;
|
|
5
|
+
trackingParams: TrackingParams;
|
|
6
|
+
selectedProductId: string | null;
|
|
7
|
+
on?: {
|
|
8
|
+
productImpression?: (productId: string) => void;
|
|
9
|
+
productBuy?: (productId: string) => void;
|
|
10
|
+
productSelect?: (productId: string) => void;
|
|
11
|
+
adClick?: (adId: string) => void;
|
|
12
|
+
adImpression?: (adId: string) => void;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Information tab — displays product and ad cards for the current post.
|
|
17
|
+
*
|
|
18
|
+
* ### CSS Custom Properties
|
|
19
|
+
* | Property | Description | Default |
|
|
20
|
+
* |---|---|---|
|
|
21
|
+
* | `--information-tab--checkmark-color` | Selected product checkmark color | `#0cce6b` |
|
|
22
|
+
* | `--information-tab--gap` | Gap between cards | `16px` |
|
|
23
|
+
*/
|
|
24
|
+
declare const InformationTab: import("svelte").Component<Props, {}, "">;
|
|
25
|
+
type InformationTab = ReturnType<typeof InformationTab>;
|
|
26
|
+
export default InformationTab;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">import { FeedPlayerLocalization } from '../feed-player-localization';
|
|
2
|
+
import { default as PostCard } from './post-card.svelte';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
const { handler, activePostId, on } = $props();
|
|
5
|
+
const localization = new FeedPlayerLocalization();
|
|
6
|
+
let data = $state.raw(null);
|
|
7
|
+
$effect(() => {
|
|
8
|
+
void handler;
|
|
9
|
+
untrack(() => {
|
|
10
|
+
handler.getPlaylist().then((result) => {
|
|
11
|
+
data = result;
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
const currentPostIndex = $derived.by(() => {
|
|
16
|
+
if (!data)
|
|
17
|
+
return -1;
|
|
18
|
+
return data.posts.findIndex((p) => p.id === activePostId);
|
|
19
|
+
});
|
|
20
|
+
const formatDate = (dateString) => {
|
|
21
|
+
const date = new Date(dateString);
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const diffMs = now.getTime() - date.getTime();
|
|
24
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
25
|
+
if (diffDays === 0)
|
|
26
|
+
return 'Today';
|
|
27
|
+
if (diffDays === 1)
|
|
28
|
+
return 'Yesterday';
|
|
29
|
+
if (diffDays < 7)
|
|
30
|
+
return `${diffDays} days ago`;
|
|
31
|
+
return date.toLocaleDateString();
|
|
32
|
+
};
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
{#if data}
|
|
36
|
+
<div class="playlist-tab">
|
|
37
|
+
<div class="playlist-tab__header">
|
|
38
|
+
<div class="playlist-tab__header-left">
|
|
39
|
+
<div class="playlist-tab__name">{data.name}</div>
|
|
40
|
+
<div class="playlist-tab__updated">{localization.updatedLabel} {formatDate(data.updatedAt)}</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="playlist-tab__post-indicator">{localization.postOf(currentPostIndex + 1, data.posts.length)}</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="playlist-tab__list">
|
|
46
|
+
{#each data.posts as post (post.id)}
|
|
47
|
+
<PostCard
|
|
48
|
+
{post}
|
|
49
|
+
thumbnailWidth={146}
|
|
50
|
+
thumbnailHeight={260}
|
|
51
|
+
thumbnailRadius={8}
|
|
52
|
+
active={post.id === activePostId}
|
|
53
|
+
on={{ click: () => on?.postActivate?.(post.id) }} />
|
|
54
|
+
{/each}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
{/if}
|
|
58
|
+
|
|
59
|
+
<!--
|
|
60
|
+
@component
|
|
61
|
+
Playlist tab — displays the current playlist's posts with active post highlighted.
|
|
62
|
+
|
|
63
|
+
### CSS Custom Properties
|
|
64
|
+
| Property | Description | Default |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `--playlist-tab--meta-color` | Header secondary text color | `#9ca3af` |
|
|
67
|
+
-->
|
|
68
|
+
|
|
69
|
+
<style>.playlist-tab {
|
|
70
|
+
--_playlist-tab--background: var(--playlist-tab--background, rgba(255, 255, 255, 0.58));
|
|
71
|
+
--_playlist-tab--meta-color: var(--playlist-tab--meta-color, #9ca3af);
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: 1rem;
|
|
75
|
+
background: var(--_playlist-tab--background);
|
|
76
|
+
padding: 1rem;
|
|
77
|
+
min-height: 100%;
|
|
78
|
+
}
|
|
79
|
+
.playlist-tab__header {
|
|
80
|
+
display: flex;
|
|
81
|
+
justify-content: space-between;
|
|
82
|
+
align-items: flex-start;
|
|
83
|
+
}
|
|
84
|
+
.playlist-tab__header-left {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
}
|
|
88
|
+
.playlist-tab__name {
|
|
89
|
+
font-size: 1rem;
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
line-height: 1.5rem;
|
|
92
|
+
}
|
|
93
|
+
.playlist-tab__updated {
|
|
94
|
+
font-size: 0.75rem;
|
|
95
|
+
font-weight: 500;
|
|
96
|
+
color: var(--_playlist-tab--meta-color);
|
|
97
|
+
line-height: 1rem;
|
|
98
|
+
}
|
|
99
|
+
.playlist-tab__post-indicator {
|
|
100
|
+
font-size: 0.75rem;
|
|
101
|
+
font-weight: 500;
|
|
102
|
+
color: var(--_playlist-tab--meta-color);
|
|
103
|
+
line-height: 1rem;
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
}
|
|
106
|
+
.playlist-tab__list {
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-direction: column;
|
|
109
|
+
gap: 1rem;
|
|
110
|
+
}</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IFeedPlayerPlaylistHandler } from '../types';
|
|
2
|
+
type Props = {
|
|
3
|
+
handler: IFeedPlayerPlaylistHandler;
|
|
4
|
+
activePostId: string;
|
|
5
|
+
on?: {
|
|
6
|
+
postActivate?: (postId: string) => void;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Playlist tab — displays the current playlist's posts with active post highlighted.
|
|
11
|
+
*
|
|
12
|
+
* ### CSS Custom Properties
|
|
13
|
+
* | Property | Description | Default |
|
|
14
|
+
* |---|---|---|
|
|
15
|
+
* | `--playlist-tab--meta-color` | Header secondary text color | `#9ca3af` |
|
|
16
|
+
*/
|
|
17
|
+
declare const PlaylistTab: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type PlaylistTab = ReturnType<typeof PlaylistTab>;
|
|
19
|
+
export default PlaylistTab;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script lang="ts">import { FeedPlayerLocalization } from '../feed-player-localization';
|
|
2
|
+
import { Image } from '@streamscloud/kit/ui/image';
|
|
3
|
+
const { post, thumbnailWidth, thumbnailHeight, thumbnailRadius = 6, active = false, on } = $props();
|
|
4
|
+
const localization = new FeedPlayerLocalization();
|
|
5
|
+
const formatDuration = (seconds) => {
|
|
6
|
+
const m = Math.floor(seconds / 60);
|
|
7
|
+
const s = seconds % 60;
|
|
8
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
9
|
+
};
|
|
10
|
+
const formatDate = (dateString) => {
|
|
11
|
+
const date = new Date(dateString);
|
|
12
|
+
const now = new Date();
|
|
13
|
+
const diffMs = now.getTime() - date.getTime();
|
|
14
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
15
|
+
if (diffDays === 0)
|
|
16
|
+
return 'Today';
|
|
17
|
+
if (diffDays === 1)
|
|
18
|
+
return 'Yesterday';
|
|
19
|
+
if (diffDays < 7)
|
|
20
|
+
return `${diffDays} days ago`;
|
|
21
|
+
return date.toLocaleDateString();
|
|
22
|
+
};
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div
|
|
26
|
+
class="post-card"
|
|
27
|
+
onclick={on?.click}
|
|
28
|
+
onkeydown={() => {}}
|
|
29
|
+
role="none">
|
|
30
|
+
<div
|
|
31
|
+
class="post-card__thumbnail"
|
|
32
|
+
class:post-card__thumbnail--active={active}
|
|
33
|
+
style:width="{thumbnailWidth}px"
|
|
34
|
+
style:height="{thumbnailHeight}px"
|
|
35
|
+
style:--_post-card--thumbnail-radius="{thumbnailRadius}px">
|
|
36
|
+
<Image src={post.thumbnailUrl} />
|
|
37
|
+
{#if post.durationSeconds != null}
|
|
38
|
+
<div class="post-card__duration">{formatDuration(post.durationSeconds)}</div>
|
|
39
|
+
{/if}
|
|
40
|
+
</div>
|
|
41
|
+
<div class="post-card__info">
|
|
42
|
+
{#if post.title}
|
|
43
|
+
<div class="post-card__title">{post.title}</div>
|
|
44
|
+
{/if}
|
|
45
|
+
{#if post.description}
|
|
46
|
+
<div class="post-card__description">{post.description}</div>
|
|
47
|
+
{/if}
|
|
48
|
+
<div class="post-card__meta">{formatDate(post.displayDate)} · {localization.viewsLabel(post.viewsCount)}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!--
|
|
53
|
+
@component
|
|
54
|
+
Horizontal post card with thumbnail, title, description, and meta info.
|
|
55
|
+
|
|
56
|
+
### CSS Custom Properties
|
|
57
|
+
| Property | Description | Default |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `--post-card--active-border-color` | Active post border color | `#0cce6b` |
|
|
60
|
+
| `--post-card--description-color` | Description text color | `#999` |
|
|
61
|
+
| `--post-card--meta-color` | Meta text color | `#999` |
|
|
62
|
+
-->
|
|
63
|
+
|
|
64
|
+
<style>.post-card {
|
|
65
|
+
--_post-card--active-border-color: var(--post-card--active-border-color, #0cce6b);
|
|
66
|
+
--_post-card--description-color: var(--post-card--description-color, #999);
|
|
67
|
+
--_post-card--meta-color: var(--post-card--meta-color, #999);
|
|
68
|
+
display: flex;
|
|
69
|
+
gap: 1rem;
|
|
70
|
+
align-items: flex-end;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
}
|
|
73
|
+
.post-card__thumbnail {
|
|
74
|
+
position: relative;
|
|
75
|
+
flex-shrink: 0;
|
|
76
|
+
border-radius: var(--_post-card--thumbnail-radius);
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
--sc-kit--image--object-fit: cover;
|
|
79
|
+
--sc-kit--image--border-radius: var(--_post-card--thumbnail-radius);
|
|
80
|
+
}
|
|
81
|
+
.post-card__thumbnail--active {
|
|
82
|
+
outline: 2px solid var(--_post-card--active-border-color);
|
|
83
|
+
outline-offset: 0.125rem;
|
|
84
|
+
border-radius: calc(var(--_post-card--thumbnail-radius) + 3px);
|
|
85
|
+
}
|
|
86
|
+
.post-card__duration {
|
|
87
|
+
position: absolute;
|
|
88
|
+
bottom: 0.25rem;
|
|
89
|
+
right: 0.25rem;
|
|
90
|
+
background: rgba(0, 0, 0, 0.75);
|
|
91
|
+
border-radius: 0.25rem;
|
|
92
|
+
padding: 0.125rem 0.25rem;
|
|
93
|
+
font-size: 0.625rem;
|
|
94
|
+
font-weight: 400;
|
|
95
|
+
color: white;
|
|
96
|
+
line-height: 1;
|
|
97
|
+
}
|
|
98
|
+
.post-card__info {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 0.75rem;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
height: 7.125rem;
|
|
104
|
+
}
|
|
105
|
+
.post-card__title {
|
|
106
|
+
font-size: 0.875rem;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
text-overflow: ellipsis;
|
|
110
|
+
white-space: nowrap;
|
|
111
|
+
}
|
|
112
|
+
.post-card__description {
|
|
113
|
+
font-size: 0.75rem;
|
|
114
|
+
font-weight: 400;
|
|
115
|
+
color: var(--_post-card--description-color);
|
|
116
|
+
line-height: 0.9375rem;
|
|
117
|
+
display: -webkit-box;
|
|
118
|
+
-webkit-line-clamp: 3;
|
|
119
|
+
line-clamp: 3;
|
|
120
|
+
-webkit-box-orient: vertical;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
}
|
|
123
|
+
.post-card__meta {
|
|
124
|
+
font-size: 0.625rem;
|
|
125
|
+
font-weight: 400;
|
|
126
|
+
color: var(--_post-card--meta-color);
|
|
127
|
+
white-space: nowrap;
|
|
128
|
+
}</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FeedPlayerPostPreview } from '../types';
|
|
2
|
+
type Props = {
|
|
3
|
+
post: FeedPlayerPostPreview;
|
|
4
|
+
thumbnailWidth: number;
|
|
5
|
+
thumbnailHeight: number;
|
|
6
|
+
thumbnailRadius?: number;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
on?: {
|
|
9
|
+
click?: () => void;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Horizontal post card with thumbnail, title, description, and meta info.
|
|
14
|
+
*
|
|
15
|
+
* ### CSS Custom Properties
|
|
16
|
+
* | Property | Description | Default |
|
|
17
|
+
* |---|---|---|
|
|
18
|
+
* | `--post-card--active-border-color` | Active post border color | `#0cce6b` |
|
|
19
|
+
* | `--post-card--description-color` | Description text color | `#999` |
|
|
20
|
+
* | `--post-card--meta-color` | Meta text color | `#999` |
|
|
21
|
+
*/
|
|
22
|
+
declare const PostCard: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
type PostCard = ReturnType<typeof PostCard>;
|
|
24
|
+
export default PostCard;
|