@streamscloud/blocks 0.1.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 (91) hide show
  1. package/dist/content/analytics/index.d.ts +1 -0
  2. package/dist/content/analytics/index.js +1 -0
  3. package/dist/content/analytics/types.d.ts +9 -0
  4. package/dist/content/analytics/types.js +1 -0
  5. package/dist/content/content-viewer/attachments-horizontal.svelte +210 -0
  6. package/dist/content/content-viewer/attachments-horizontal.svelte.d.ts +15 -0
  7. package/dist/content/content-viewer/cmp.content-viewer.svelte +143 -0
  8. package/dist/content/content-viewer/cmp.content-viewer.svelte.d.ts +23 -0
  9. package/dist/content/content-viewer/content-texts.svelte +96 -0
  10. package/dist/content/content-viewer/content-texts.svelte.d.ts +14 -0
  11. package/dist/content/content-viewer/content-viewer-localization.d.ts +4 -0
  12. package/dist/content/content-viewer/content-viewer-localization.js +16 -0
  13. package/dist/content/content-viewer/heading.svelte +66 -0
  14. package/dist/content/content-viewer/heading.svelte.d.ts +11 -0
  15. package/dist/content/content-viewer/index.d.ts +1 -0
  16. package/dist/content/content-viewer/index.js +1 -0
  17. package/dist/content/content-viewer/media/content-media.svelte +53 -0
  18. package/dist/content/content-viewer/media/content-media.svelte.d.ts +12 -0
  19. package/dist/content/content-viewer/ui-manager.svelte.d.ts +10 -0
  20. package/dist/content/content-viewer/ui-manager.svelte.js +18 -0
  21. package/dist/content/controls/content-actions-generator.svelte.d.ts +18 -0
  22. package/dist/content/controls/content-actions-generator.svelte.js +27 -0
  23. package/dist/content/controls/content-actions-handler.svelte.d.ts +23 -0
  24. package/dist/content/controls/content-actions-handler.svelte.js +55 -0
  25. package/dist/content/controls/index.d.ts +1 -0
  26. package/dist/content/controls/index.js +1 -0
  27. package/dist/content/model/content-media-model.svelte.d.ts +20 -0
  28. package/dist/content/model/content-media-model.svelte.js +16 -0
  29. package/dist/content/model/content-model.d.ts +24 -0
  30. package/dist/content/model/content-model.js +32 -0
  31. package/dist/content/model/index.d.ts +3 -0
  32. package/dist/content/model/index.js +2 -0
  33. package/dist/content/model/types.d.ts +61 -0
  34. package/dist/content/model/types.js +1 -0
  35. package/dist/content/model/utils.d.ts +4 -0
  36. package/dist/content/model/utils.js +7 -0
  37. package/dist/content/sharing/index.d.ts +1 -0
  38. package/dist/content/sharing/index.js +1 -0
  39. package/dist/content/sharing/types.d.ts +5 -0
  40. package/dist/content/sharing/types.js +1 -0
  41. package/dist/content/social-interactions/index.d.ts +1 -0
  42. package/dist/content/social-interactions/index.js +1 -0
  43. package/dist/content/social-interactions/types.d.ts +8 -0
  44. package/dist/content/social-interactions/types.js +1 -0
  45. package/dist/core/enums.d.ts +4 -0
  46. package/dist/core/enums.js +1 -0
  47. package/dist/cta/cta-card/cmp.cta-card.svelte +259 -0
  48. package/dist/cta/cta-card/cmp.cta-card.svelte.d.ts +24 -0
  49. package/dist/cta/cta-card/index.d.ts +2 -0
  50. package/dist/cta/cta-card/index.js +1 -0
  51. package/dist/cta/cta-card/types.d.ts +18 -0
  52. package/dist/cta/cta-card/types.js +1 -0
  53. package/dist/feed-player/cmp.close-button.svelte +43 -0
  54. package/dist/feed-player/cmp.close-button.svelte.d.ts +19 -0
  55. package/dist/feed-player/cmp.feed-player.svelte +510 -0
  56. package/dist/feed-player/cmp.feed-player.svelte.d.ts +13 -0
  57. package/dist/feed-player/feed-player-localization.d.ts +16 -0
  58. package/dist/feed-player/feed-player-localization.js +70 -0
  59. package/dist/feed-player/index.d.ts +7 -0
  60. package/dist/feed-player/index.js +2 -0
  61. package/dist/feed-player/sidebar/article-tab.svelte +90 -0
  62. package/dist/feed-player/sidebar/article-tab.svelte.d.ts +10 -0
  63. package/dist/feed-player/sidebar/information-tab.svelte +85 -0
  64. package/dist/feed-player/sidebar/information-tab.svelte.d.ts +15 -0
  65. package/dist/feed-player/sidebar/panel-surface.svelte +13 -0
  66. package/dist/feed-player/sidebar/panel-surface.svelte.d.ts +7 -0
  67. package/dist/feed-player/sidebar/playlist-tab.svelte +90 -0
  68. package/dist/feed-player/sidebar/playlist-tab.svelte.d.ts +11 -0
  69. package/dist/feed-player/sidebar/post-card.svelte +92 -0
  70. package/dist/feed-player/sidebar/post-card.svelte.d.ts +14 -0
  71. package/dist/feed-player/sidebar/recommended-tab.svelte +161 -0
  72. package/dist/feed-player/sidebar/recommended-tab.svelte.d.ts +8 -0
  73. package/dist/feed-player/sidebar/sidebar-panel.svelte +69 -0
  74. package/dist/feed-player/sidebar/sidebar-panel.svelte.d.ts +26 -0
  75. package/dist/feed-player/sidebar/sidebar-tab-bar.svelte +44 -0
  76. package/dist/feed-player/sidebar/sidebar-tab-bar.svelte.d.ts +12 -0
  77. package/dist/feed-player/sidebar/types.d.ts +4 -0
  78. package/dist/feed-player/sidebar/types.js +1 -0
  79. package/dist/feed-player/types.d.ts +65 -0
  80. package/dist/feed-player/types.js +1 -0
  81. package/dist/index.d.ts +1 -0
  82. package/dist/index.js +1 -0
  83. package/dist/products/product-card/cmp.product-card.svelte +282 -0
  84. package/dist/products/product-card/cmp.product-card.svelte.d.ts +30 -0
  85. package/dist/products/product-card/index.d.ts +2 -0
  86. package/dist/products/product-card/index.js +1 -0
  87. package/dist/products/product-card/product-card-localization.d.ts +4 -0
  88. package/dist/products/product-card/product-card-localization.js +19 -0
  89. package/dist/products/product-card/types.d.ts +12 -0
  90. package/dist/products/product-card/types.js +1 -0
  91. package/package.json +92 -0
@@ -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 relatedContent() {
22
+ return loc.relatedContent[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
+ relatedContent: { en: 'Related', no: 'Relatert' },
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} item${count !== 1 ? 's' : ''}`,
60
+ no: (count) => `${count} element${count !== 1 ? 'er' : ''}`
61
+ },
62
+ postOf: {
63
+ en: (current, total) => `${current} of ${total}`,
64
+ no: (current, total) => `${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,7 @@
1
+ export type { IContentAnalyticsHandler as FeedPlayerAnalyticsHandler } from '../content/analytics';
2
+ export type { IContentCtaCardModel as FeedPlayerCtaCardModel, IContentHeadingModel as FeedPlayerHeadingModel, IContentMediaItemModel as FeedPlayerMediaItemModel, IContentModel as FeedPlayerContentModel, IContentProductCardModel as FeedPlayerProductCardModel } from '../content/model';
3
+ export type { IContentSharingHandler as FeedPlayerSharingHandler } from '../content/sharing';
4
+ export type { IContentSocialInteractionsHandler as FeedPlayerSocialInteractionsHandler } from '../content/social-interactions';
5
+ export { default as CloseButton } from './cmp.close-button.svelte';
6
+ export { default as FeedPlayer } from './cmp.feed-player.svelte';
7
+ export type { FeedPlayerContentPreview, FeedPlayerPlaylistData, FeedPlayerProps, FeedPlayerRecommendedData, FeedPlayerSettings, FeedPlayerSuggestedPlaylist, IFeedPlayerPlaylistHandler as FeedPlayerPlaylistHandler, IFeedPlayerRecommendedHandler as FeedPlayerRecommendedHandler } from './types';
@@ -0,0 +1,2 @@
1
+ export { default as CloseButton } from './cmp.close-button.svelte';
2
+ export { default as FeedPlayer } from './cmp.feed-player.svelte';
@@ -0,0 +1,90 @@
1
+ <script lang="ts">import { FeedPlayerLocalization } from '../feed-player-localization';
2
+ import { default as PanelSurface } from './panel-surface.svelte';
3
+ import { Image } from '@streamscloud/kit/ui/image';
4
+ const localization = new FeedPlayerLocalization();
5
+ let { model, on } = $props();
6
+ const heroImage = $derived(model.media.items[0]?.isImage ? model.media.items[0].url : null);
7
+ </script>
8
+
9
+ <PanelSurface>
10
+ <div class="article-tab">
11
+ {#if heroImage}
12
+ <div class="article-tab__hero">
13
+ <Image src={heroImage} />
14
+ </div>
15
+ {/if}
16
+
17
+ <div class="article-tab__content">
18
+ {#if model.texts.kicker}
19
+ <div class="article-tab__category">{model.texts.kicker}</div>
20
+ {/if}
21
+
22
+ {#if model.texts.title}
23
+ <div class="article-tab__title">{model.texts.title}</div>
24
+ {/if}
25
+
26
+ {#if model.texts.text}
27
+ <div class="article-tab__text">{model.texts.text}</div>
28
+ {/if}
29
+
30
+ {#if model.articleId && on?.readMore}
31
+ <button type="button" class="article-tab__show-button" onclick={() => model.articleId && on?.readMore && on.readMore(model.articleId)}
32
+ >{localization.show}</button>
33
+ {/if}
34
+ </div>
35
+ </div>
36
+ </PanelSurface>
37
+
38
+ <style>.article-tab {
39
+ --_article-tab--button-background: var(--sc-blocks--article-tab--button-background, var(--sc-kit--color--bg--element));
40
+ --_article-tab--button-border: var(--sc-blocks--article-tab--button-border, var(--sc-kit--color--border));
41
+ display: flex;
42
+ flex-direction: column;
43
+ --sc-kit--image--object-fit: cover;
44
+ --sc-kit--image--border-radius: var(--sc-kit--radius--md) var(--sc-kit--radius--md) 0 0;
45
+ }
46
+ .article-tab__hero {
47
+ width: 100%;
48
+ aspect-ratio: 392/245;
49
+ flex-shrink: 0;
50
+ }
51
+ .article-tab__content {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: var(--sc-kit--space--1);
55
+ padding: var(--sc-kit--space--4);
56
+ }
57
+ .article-tab__category {
58
+ font-size: var(--sc-kit--font-size--lg);
59
+ font-weight: var(--sc-kit--font-weight--medium);
60
+ color: var(--sc-kit--color--text--secondary);
61
+ }
62
+ .article-tab__title {
63
+ font-size: var(--sc-kit--font-size--3xl);
64
+ font-weight: var(--sc-kit--font-weight--semibold);
65
+ line-height: var(--sc-kit--leading--tight);
66
+ }
67
+ .article-tab__text {
68
+ font-size: 1.0625rem;
69
+ font-weight: var(--sc-kit--font-weight--regular);
70
+ line-height: 1.375rem;
71
+ display: -webkit-box;
72
+ -webkit-line-clamp: 10;
73
+ line-clamp: 10;
74
+ -webkit-box-orient: vertical;
75
+ overflow: hidden;
76
+ }
77
+ .article-tab__show-button {
78
+ margin-top: var(--sc-kit--space--2);
79
+ width: 100%;
80
+ height: 2.375rem;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ font-size: var(--sc-kit--font-size--md);
85
+ font-weight: var(--sc-kit--font-weight--semibold);
86
+ background: var(--_article-tab--button-background);
87
+ border: 1px solid var(--_article-tab--button-border);
88
+ border-radius: var(--sc-kit--radius--lg);
89
+ cursor: pointer;
90
+ }</style>
@@ -0,0 +1,10 @@
1
+ import type { ContentModel } from '../../content/model';
2
+ type Props = {
3
+ model: ContentModel;
4
+ on?: {
5
+ readMore?: (articleId: string) => void;
6
+ };
7
+ };
8
+ declare const ArticleTab: import("svelte").Component<Props, {}, "">;
9
+ type ArticleTab = ReturnType<typeof ArticleTab>;
10
+ export default ArticleTab;
@@ -0,0 +1,85 @@
1
+ <script lang="ts">import { CtaCard } from '../../cta/cta-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, selectedProductId, on } = $props();
6
+ </script>
7
+
8
+ {#if model.attachments}
9
+ <div class="information-tab">
10
+ {#if model.attachments.ctas.length}
11
+ <div class="information-tab__ctas">
12
+ {#each model.attachments.ctas as cta (cta.id)}
13
+ <div class="information-tab__cta-wrapper">
14
+ <CtaCard
15
+ cta={cta}
16
+ on={{
17
+ click: on?.ctaClick,
18
+ impression: on?.ctaImpression
19
+ }} />
20
+ </div>
21
+ {/each}
22
+ </div>
23
+ {/if}
24
+
25
+ {#if model.attachments.products.length}
26
+ <div class="information-tab__products">
27
+ {#each model.attachments.products as product (product.id)}
28
+ <div class="information-tab__product-wrapper" onclick={() => on?.productSelect?.(product.id)} onkeydown={() => {}} role="none">
29
+ {#if selectedProductId === product.id}
30
+ <div class="information-tab__checkmark">
31
+ <Icon src={IconCheckmarkCircle24} />
32
+ </div>
33
+ {/if}
34
+ <ProductCard
35
+ product={product}
36
+ inert={true}
37
+ on={{
38
+ impression: on?.productImpression,
39
+ buy: on?.productBuy
40
+ }} />
41
+ </div>
42
+ {/each}
43
+ </div>
44
+ {/if}
45
+ </div>
46
+ {/if}
47
+
48
+ <style>.information-tab {
49
+ --_information-tab--checkmark-color: var(--sc-blocks--information-tab--checkmark-color, var(--sc-kit--color--text--on-accent));
50
+ --_information-tab--gap: var(--sc-blocks--information-tab--gap, var(--sc-kit--space--4));
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: var(--_information-tab--gap);
54
+ container-type: inline-size;
55
+ }
56
+ .information-tab__ctas {
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ justify-content: flex-start;
60
+ gap: var(--_information-tab--gap);
61
+ }
62
+ .information-tab__cta-wrapper {
63
+ width: calc(50% - var(--_information-tab--gap) / 2);
64
+ }
65
+ .information-tab__products {
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ justify-content: flex-start;
69
+ gap: var(--_information-tab--gap);
70
+ }
71
+ .information-tab__product-wrapper {
72
+ position: relative;
73
+ border-radius: var(--sc-kit--radius--lg);
74
+ cursor: pointer;
75
+ width: calc(50% - var(--_information-tab--gap) / 2);
76
+ }
77
+ .information-tab__checkmark {
78
+ --sc-kit--icon--size: 1.5rem;
79
+ --sc-kit--icon--color: var(--_information-tab--checkmark-color);
80
+ position: absolute;
81
+ top: var(--sc-kit--space--2);
82
+ right: var(--sc-kit--space--2);
83
+ z-index: 1;
84
+ filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 0.5));
85
+ }</style>
@@ -0,0 +1,15 @@
1
+ import type { ContentModel } from '../../content/model';
2
+ type Props = {
3
+ model: ContentModel;
4
+ selectedProductId: string | null;
5
+ on?: {
6
+ productImpression?: (productId: string) => void;
7
+ productBuy?: (productId: string) => void;
8
+ productSelect?: (productId: string) => void;
9
+ ctaClick?: (ctaId: string) => void;
10
+ ctaImpression?: (ctaId: string) => void;
11
+ };
12
+ };
13
+ declare const InformationTab: import("svelte").Component<Props, {}, "">;
14
+ type InformationTab = ReturnType<typeof InformationTab>;
15
+ export default InformationTab;
@@ -0,0 +1,13 @@
1
+ <script lang="ts">const { children } = $props();
2
+ export {};
3
+ </script>
4
+
5
+ <div class="sidebar-panel-surface">
6
+ {@render children()}
7
+ </div>
8
+
9
+ <style>.sidebar-panel-surface {
10
+ --_sidebar-panel-surface--background: var(--sc-blocks--sidebar-panel-surface--background, rgb(from var(--sc-kit--color--bg--panel) r g b / 0.58));
11
+ min-height: 100%;
12
+ background: var(--_sidebar-panel-surface--background);
13
+ }</style>
@@ -0,0 +1,7 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ children: Snippet;
4
+ };
5
+ declare const PanelSurface: import("svelte").Component<Props, {}, "">;
6
+ type PanelSurface = ReturnType<typeof PanelSurface>;
7
+ export default PanelSurface;
@@ -0,0 +1,90 @@
1
+ <script lang="ts">import { FeedPlayerLocalization } from '../feed-player-localization';
2
+ import { default as PanelSurface } from './panel-surface.svelte';
3
+ import { default as PostCard } from './post-card.svelte';
4
+ import { TimeAgo } from '@streamscloud/kit/ui/time-ago';
5
+ import { untrack } from 'svelte';
6
+ const { handler, activeContentId, on } = $props();
7
+ const localization = new FeedPlayerLocalization();
8
+ let data = $state.raw(null);
9
+ $effect(() => {
10
+ void handler;
11
+ untrack(() => {
12
+ const load = async () => {
13
+ data = await handler.getPlaylist();
14
+ };
15
+ void load();
16
+ });
17
+ });
18
+ const currentContentIndex = $derived.by(() => {
19
+ if (!data) {
20
+ return -1;
21
+ }
22
+ return data.content.findIndex((c) => c.id === activeContentId);
23
+ });
24
+ </script>
25
+
26
+ {#if data}
27
+ <PanelSurface>
28
+ <div class="playlist-tab">
29
+ <div class="playlist-tab__header">
30
+ <div class="playlist-tab__header-left">
31
+ <div class="playlist-tab__name">{data.name}</div>
32
+ <div class="playlist-tab__updated">{localization.updatedLabel} <TimeAgo date={data.updatedAt} /></div>
33
+ </div>
34
+ <div class="playlist-tab__post-indicator">{localization.postOf(currentContentIndex + 1, data.content.length)}</div>
35
+ </div>
36
+
37
+ <div class="playlist-tab__list">
38
+ {#each data.content as content (content.id)}
39
+ <PostCard
40
+ content={content}
41
+ thumbnailWidth={82}
42
+ thumbnailHeight={146}
43
+ thumbnailRadius={6}
44
+ active={content.id === activeContentId}
45
+ on={{ click: () => on?.contentActivate?.(content.id) }} />
46
+ {/each}
47
+ </div>
48
+ </div>
49
+ </PanelSurface>
50
+ {/if}
51
+
52
+ <style>.playlist-tab {
53
+ --_playlist-tab--meta-color: var(--sc-blocks--playlist-tab--meta-color, var(--sc-kit--color--text--secondary));
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: var(--sc-kit--space--4);
57
+ padding: var(--sc-kit--space--4);
58
+ }
59
+ .playlist-tab__header {
60
+ display: flex;
61
+ justify-content: space-between;
62
+ align-items: flex-start;
63
+ }
64
+ .playlist-tab__header-left {
65
+ display: flex;
66
+ flex-direction: column;
67
+ }
68
+ .playlist-tab__name {
69
+ font-size: var(--sc-kit--font-size--lg);
70
+ font-weight: var(--sc-kit--font-weight--medium);
71
+ line-height: var(--sc-kit--line-height--lg);
72
+ }
73
+ .playlist-tab__updated {
74
+ font-size: var(--sc-kit--font-size--sm);
75
+ font-weight: var(--sc-kit--font-weight--medium);
76
+ color: var(--_playlist-tab--meta-color);
77
+ line-height: var(--sc-kit--line-height--xs);
78
+ }
79
+ .playlist-tab__post-indicator {
80
+ font-size: var(--sc-kit--font-size--sm);
81
+ font-weight: var(--sc-kit--font-weight--medium);
82
+ color: var(--_playlist-tab--meta-color);
83
+ line-height: var(--sc-kit--line-height--xs);
84
+ white-space: nowrap;
85
+ }
86
+ .playlist-tab__list {
87
+ display: flex;
88
+ flex-direction: column;
89
+ gap: var(--sc-kit--space--4);
90
+ }</style>
@@ -0,0 +1,11 @@
1
+ import type { IFeedPlayerPlaylistHandler } from '../types';
2
+ type Props = {
3
+ handler: IFeedPlayerPlaylistHandler;
4
+ activeContentId: string;
5
+ on?: {
6
+ contentActivate?: (contentId: string) => void;
7
+ };
8
+ };
9
+ declare const PlaylistTab: import("svelte").Component<Props, {}, "">;
10
+ type PlaylistTab = ReturnType<typeof PlaylistTab>;
11
+ export default PlaylistTab;
@@ -0,0 +1,92 @@
1
+ <script lang="ts">import { FeedPlayerLocalization } from '../feed-player-localization';
2
+ import { Duration } from '@streamscloud/kit/ui/duration';
3
+ import { Image } from '@streamscloud/kit/ui/image';
4
+ import { TimeAgo } from '@streamscloud/kit/ui/time-ago';
5
+ const { content, thumbnailWidth, thumbnailHeight, thumbnailRadius = 6, active = false, on } = $props();
6
+ const localization = new FeedPlayerLocalization();
7
+ </script>
8
+
9
+ <div class="sidebar-post-card" onclick={on?.click} onkeydown={() => {}} role="none">
10
+ <div
11
+ class="sidebar-post-card__thumbnail"
12
+ class:sidebar-post-card__thumbnail--active={active}
13
+ style:width="{thumbnailWidth}px"
14
+ style:height="{thumbnailHeight}px"
15
+ style:--_sidebar-post-card--thumbnail-radius="{thumbnailRadius}px">
16
+ <Image src={content.thumbnailUrl} />
17
+ {#if content.durationSeconds}
18
+ <div class="sidebar-post-card__duration">
19
+ <Duration seconds={content.durationSeconds} variant="badge" />
20
+ </div>
21
+ {/if}
22
+ </div>
23
+ <div class="sidebar-post-card__info">
24
+ {#if content.title}
25
+ <div class="sidebar-post-card__title">{content.title}</div>
26
+ {/if}
27
+ {#if content.description}
28
+ <div class="sidebar-post-card__description">{content.description}</div>
29
+ {/if}
30
+ <div class="sidebar-post-card__meta"><TimeAgo date={content.displayDate} /> · {localization.viewsLabel(content.viewsCount)}</div>
31
+ </div>
32
+ </div>
33
+
34
+ <style>.sidebar-post-card {
35
+ --_sidebar-post-card--active-border-color: var(--sc-blocks--sidebar-post-card--active-border-color, var(--sc-kit--color--success));
36
+ --_sidebar-post-card--description-color: var(--sc-blocks--sidebar-post-card--description-color, var(--sc-kit--color--text--secondary));
37
+ --_sidebar-post-card--meta-color: var(--sc-blocks--sidebar-post-card--meta-color, var(--sc-kit--color--text--secondary));
38
+ display: flex;
39
+ gap: var(--sc-kit--space--4);
40
+ align-items: flex-end;
41
+ cursor: pointer;
42
+ }
43
+ .sidebar-post-card__thumbnail {
44
+ position: relative;
45
+ flex-shrink: 0;
46
+ border-radius: var(--_sidebar-post-card--thumbnail-radius);
47
+ overflow: hidden;
48
+ --sc-kit--image--object-fit: cover;
49
+ --sc-kit--image--border-radius: var(--_sidebar-post-card--thumbnail-radius);
50
+ }
51
+ .sidebar-post-card__thumbnail--active {
52
+ outline: 2px solid var(--_sidebar-post-card--active-border-color);
53
+ outline-offset: 0.125rem;
54
+ border-radius: calc(var(--_sidebar-post-card--thumbnail-radius) + 3px);
55
+ }
56
+ .sidebar-post-card__duration {
57
+ position: absolute;
58
+ bottom: var(--sc-kit--space--1);
59
+ right: var(--sc-kit--space--1);
60
+ }
61
+ .sidebar-post-card__info {
62
+ display: flex;
63
+ flex-direction: column;
64
+ justify-content: flex-end;
65
+ gap: var(--sc-kit--space--3);
66
+ min-width: 0;
67
+ height: 7.125rem;
68
+ }
69
+ .sidebar-post-card__title {
70
+ font-size: var(--sc-kit--font-size--md);
71
+ font-weight: var(--sc-kit--font-weight--semibold);
72
+ overflow: hidden;
73
+ text-overflow: ellipsis;
74
+ white-space: nowrap;
75
+ }
76
+ .sidebar-post-card__description {
77
+ font-size: var(--sc-kit--font-size--sm);
78
+ font-weight: var(--sc-kit--font-weight--regular);
79
+ color: var(--_sidebar-post-card--description-color);
80
+ line-height: 0.9375rem;
81
+ display: -webkit-box;
82
+ -webkit-line-clamp: 3;
83
+ line-clamp: 3;
84
+ -webkit-box-orient: vertical;
85
+ overflow: hidden;
86
+ }
87
+ .sidebar-post-card__meta {
88
+ font-size: 0.625rem;
89
+ font-weight: var(--sc-kit--font-weight--regular);
90
+ color: var(--_sidebar-post-card--meta-color);
91
+ white-space: nowrap;
92
+ }</style>
@@ -0,0 +1,14 @@
1
+ import type { FeedPlayerContentPreview } from '../types';
2
+ type Props = {
3
+ content: FeedPlayerContentPreview;
4
+ thumbnailWidth: number;
5
+ thumbnailHeight: number;
6
+ thumbnailRadius?: number;
7
+ active?: boolean;
8
+ on?: {
9
+ click?: () => void;
10
+ };
11
+ };
12
+ declare const PostCard: import("svelte").Component<Props, {}, "">;
13
+ type PostCard = ReturnType<typeof PostCard>;
14
+ export default PostCard;
@@ -0,0 +1,161 @@
1
+ <script lang="ts">import { ProductCard } from '../../products/product-card';
2
+ import { FeedPlayerLocalization } from '../feed-player-localization';
3
+ import { default as PanelSurface } from './panel-surface.svelte';
4
+ import { default as PostCard } from './post-card.svelte';
5
+ import { Image } from '@streamscloud/kit/ui/image';
6
+ import { untrack } from 'svelte';
7
+ const { handler, currentContentId } = $props();
8
+ const localization = new FeedPlayerLocalization();
9
+ let data = $state.raw(null);
10
+ $effect(() => {
11
+ const contentId = currentContentId;
12
+ untrack(() => {
13
+ data = null;
14
+ const load = async () => {
15
+ data = await handler.getRecommendations(contentId);
16
+ };
17
+ void load();
18
+ });
19
+ });
20
+ </script>
21
+
22
+ {#if data}
23
+ <PanelSurface>
24
+ <div class="recommended-tab">
25
+ {#if data.relatedContent.length}
26
+ <div class="recommended-tab__section">
27
+ <div class="recommended-tab__section-title">{localization.relatedContent}</div>
28
+ <div class="recommended-tab__posts">
29
+ {#each data.relatedContent as content (content.id)}
30
+ <PostCard
31
+ content={content}
32
+ thumbnailWidth={82}
33
+ thumbnailHeight={146}
34
+ thumbnailRadius={6}
35
+ on={{ click: () => handler.onContentSelect?.(content.id) }} />
36
+ {/each}
37
+ </div>
38
+ </div>
39
+ {/if}
40
+
41
+ {#if data.suggestedPlaylists.length}
42
+ <div class="recommended-tab__section">
43
+ <div class="recommended-tab__section-title">{localization.suggestedPlaylist}</div>
44
+ <div class="recommended-tab__playlists">
45
+ {#each data.suggestedPlaylists as playlist (playlist.id)}
46
+ <div class="recommended-tab__playlist-card" onclick={() => handler.onPlaylistSelect?.(playlist.id)} onkeydown={() => {}} role="none">
47
+ <div class="recommended-tab__playlist-thumbnail">
48
+ <Image src={playlist.thumbnailUrl} />
49
+ </div>
50
+ <div class="recommended-tab__playlist-info">
51
+ <div class="recommended-tab__playlist-name">{playlist.name}</div>
52
+ <div class="recommended-tab__playlist-count">{localization.postsCount(playlist.postsCount)}</div>
53
+ <div
54
+ class="recommended-tab__playlist-link"
55
+ onclick={(e: MouseEvent) => {
56
+ e.stopPropagation();
57
+ handler.onPlaylistShow?.(playlist.id);
58
+ }}
59
+ onkeydown={() => {}}
60
+ role="none">
61
+ {localization.showList}
62
+ </div>
63
+ </div>
64
+ </div>
65
+ {/each}
66
+ </div>
67
+ </div>
68
+ {/if}
69
+
70
+ {#if data.suggestedProducts.length}
71
+ <div class="recommended-tab__section">
72
+ <div class="recommended-tab__section-title">{localization.suggestedProducts}</div>
73
+ <div class="recommended-tab__products">
74
+ {#each data.suggestedProducts as product (product.id)}
75
+ <div class="recommended-tab__product-wrapper">
76
+ <ProductCard product={product} on={{ click: handler.onProductSelect, buy: handler.onProductBuy }} />
77
+ </div>
78
+ {/each}
79
+ </div>
80
+ </div>
81
+ {/if}
82
+ </div>
83
+ </PanelSurface>
84
+ {/if}
85
+
86
+ <style>.recommended-tab {
87
+ --_recommended-tab--section-gap: var(--sc-blocks--recommended-tab--section-gap, var(--sc-kit--space--6));
88
+ --_recommended-tab--link-color: var(--sc-blocks--recommended-tab--link-color, var(--sc-kit--color--accent));
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: var(--_recommended-tab--section-gap);
92
+ padding: var(--sc-kit--space--4) var(--sc-kit--space--4) var(--sc-kit--space--6);
93
+ }
94
+ .recommended-tab__section {
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: var(--sc-kit--space--2);
98
+ }
99
+ .recommended-tab__section-title {
100
+ font-size: var(--sc-kit--font-size--lg);
101
+ font-weight: var(--sc-kit--font-weight--medium);
102
+ }
103
+ .recommended-tab__posts {
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: var(--sc-kit--space--3);
107
+ }
108
+ .recommended-tab__playlists {
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: var(--sc-kit--space--3);
112
+ }
113
+ .recommended-tab__playlist-card {
114
+ display: flex;
115
+ gap: var(--sc-kit--space--4);
116
+ align-items: flex-end;
117
+ background: var(--sc-kit--color--bg--panel);
118
+ border-radius: var(--sc-kit--radius--md);
119
+ overflow: hidden;
120
+ cursor: pointer;
121
+ }
122
+ .recommended-tab__playlist-thumbnail {
123
+ flex-shrink: 0;
124
+ width: 6.0625rem;
125
+ height: 9.125rem;
126
+ --sc-kit--image--object-fit: cover;
127
+ --sc-kit--image--border-radius: var(--sc-kit--radius--sm);
128
+ }
129
+ .recommended-tab__playlist-info {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: var(--sc-kit--space--1);
133
+ padding: var(--sc-kit--space--2) 0;
134
+ min-width: 0;
135
+ height: 9.125rem;
136
+ }
137
+ .recommended-tab__playlist-name {
138
+ font-size: var(--sc-kit--font-size--md);
139
+ font-weight: var(--sc-kit--font-weight--semibold);
140
+ }
141
+ .recommended-tab__playlist-count {
142
+ font-size: var(--sc-kit--font-size--sm);
143
+ font-weight: var(--sc-kit--font-weight--regular);
144
+ color: var(--sc-kit--color--text--secondary);
145
+ flex: 1;
146
+ }
147
+ .recommended-tab__playlist-link {
148
+ font-size: var(--sc-kit--font-size--sm);
149
+ font-weight: var(--sc-kit--font-weight--medium);
150
+ color: var(--_recommended-tab--link-color);
151
+ cursor: pointer;
152
+ }
153
+ .recommended-tab__products {
154
+ display: flex;
155
+ gap: var(--sc-kit--space--3);
156
+ overflow-x: auto;
157
+ scrollbar-width: none;
158
+ }
159
+ .recommended-tab__product-wrapper {
160
+ flex: 0 0 10.0625rem;
161
+ }</style>