@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 @@
1
+ export type { IContentAnalyticsHandler } from './types';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { ContentType } from '../../core/enums';
2
+ export interface IContentAnalyticsHandler {
3
+ trackContentOpened: (contentType: ContentType, contentId: string) => void;
4
+ trackShortVideoView: (videoId: string) => void;
5
+ trackProductImpression: (productId: string, contentId: string) => void;
6
+ trackProductClick: (productId: string, contentId: string) => void;
7
+ trackCtaClick: (ctaId: string) => void;
8
+ trackCtaImpression: (ctaId: string) => void;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,210 @@
1
+ <script lang="ts">import IconTargetArrow from '@fluentui/svg-icons/icons/target_arrow_20_regular.svg?raw';
2
+ import { horizontalWheelScroll, swallowTouch } from '@streamscloud/kit/core/actions';
3
+ import { toPriceRepresentation } from '@streamscloud/kit/core/utils';
4
+ import { Icon } from '@streamscloud/kit/ui/icon';
5
+ import { ImageRounded } from '@streamscloud/kit/ui/image';
6
+ let { model, uiManager, on } = $props();
7
+ const attachmentsToShow = $derived.by(() => {
8
+ if (!model.attachments) {
9
+ return [];
10
+ }
11
+ const products = model.attachments.products
12
+ .filter((p) => !!p.image)
13
+ .map((p) => ({
14
+ isCta: false,
15
+ image: p.image,
16
+ link: p.link,
17
+ productId: p.id,
18
+ ctaId: null,
19
+ price: {
20
+ amount: p.salePrice ?? p.price,
21
+ currency: p.currency
22
+ },
23
+ title: p.title,
24
+ description: p.shortDescription
25
+ }));
26
+ const ctas = model.attachments.ctas
27
+ .filter((c) => !!c.image)
28
+ .map((c) => ({
29
+ isCta: true,
30
+ image: c.image,
31
+ link: c.ctaButton?.url ?? null,
32
+ productId: null,
33
+ ctaId: c.id,
34
+ price: c.price && c.currency
35
+ ? {
36
+ amount: c.price,
37
+ currency: c.currency,
38
+ label: c.priceInfoLabel
39
+ }
40
+ : null,
41
+ title: c.title,
42
+ description: c.description
43
+ }));
44
+ return [...ctas, ...products];
45
+ });
46
+ const handleAttachmentClick = (attachment) => {
47
+ if (attachment.productId && on?.productClick) {
48
+ on.productClick(attachment.productId);
49
+ }
50
+ if (attachment.ctaId && on?.ctaClick) {
51
+ on.ctaClick(attachment.ctaId);
52
+ }
53
+ if (attachment.link) {
54
+ window.open(attachment.link, '_blank', 'noopener noreferrer');
55
+ }
56
+ };
57
+ const trackImpression = (node, attachment) => {
58
+ if (!on?.productImpression && !on?.ctaImpression) {
59
+ return;
60
+ }
61
+ let current = attachment;
62
+ const observer = new IntersectionObserver((entries) => {
63
+ entries.forEach((entry) => {
64
+ if (entry.isIntersecting && entry.intersectionRatio >= 0.1) {
65
+ if (current.productId && on?.productImpression) {
66
+ on.productImpression(current.productId, model.id);
67
+ }
68
+ else if (current.ctaId && on?.ctaImpression) {
69
+ on.ctaImpression(current.ctaId);
70
+ }
71
+ observer.unobserve(entry.target);
72
+ }
73
+ });
74
+ }, { threshold: 0.1 });
75
+ observer.observe(node);
76
+ return {
77
+ update(next) {
78
+ current = next;
79
+ },
80
+ destroy() {
81
+ observer.disconnect();
82
+ }
83
+ };
84
+ };
85
+ const variables = $derived.by(() => {
86
+ const paddingRight = uiManager.controlsPanelWidth ? 10 : uiManager.infoPaddingInline;
87
+ return `--_content-viewer--attachments-horizontal--padding-inline: ${uiManager.infoPaddingInline}px ${paddingRight}px`;
88
+ });
89
+ </script>
90
+
91
+ <div class="attachments-horizontal" style={variables} use:horizontalWheelScroll use:swallowTouch>
92
+ {#each attachmentsToShow as attachment (attachment.productId ?? attachment.ctaId)}
93
+ <div
94
+ class="attachments-horizontal__item"
95
+ class:attachments-horizontal__item--single={attachmentsToShow.length === 1}
96
+ onclick={() => handleAttachmentClick(attachment)}
97
+ onkeydown={() => {}}
98
+ role="none"
99
+ use:trackImpression={attachment}>
100
+ <div class="attachments-card" class:attachments-card--single={attachmentsToShow.length === 1} class:attachments-card--dark={!attachment.isCta}>
101
+ <div class="attachments-card__thumb">
102
+ <ImageRounded src={attachment.image} alt="" />
103
+ </div>
104
+ <div class="attachments-card__content">
105
+ <div class="attachments-card__title" title={attachment.title}>
106
+ {attachment.title}
107
+ </div>
108
+ {#if attachment.price}
109
+ <div class="attachments-card__extra-info">
110
+ {#if attachment.price.label}
111
+ {attachment.price.label}<span>&nbsp;</span>
112
+ {/if}
113
+ {toPriceRepresentation({ amount: attachment.price.amount, currency: attachment.price.currency, options: { currencyMode: 'code' } })}
114
+ </div>
115
+ {:else if attachment.description}
116
+ <div class="attachments-card__extra-info" title={attachment.description}>
117
+ {attachment.description}
118
+ </div>
119
+ {/if}
120
+ </div>
121
+ </div>
122
+ {#if attachment.isCta}
123
+ <div class="attachments-horizontal__item-icon">
124
+ <Icon src={IconTargetArrow} />
125
+ </div>
126
+ {/if}
127
+ </div>
128
+ {/each}
129
+ </div>
130
+
131
+ <style>.attachments-horizontal {
132
+ pointer-events: auto;
133
+ display: flex;
134
+ gap: var(--sc-kit--space--2);
135
+ flex-wrap: nowrap;
136
+ width: 100%;
137
+ justify-content: flex-start;
138
+ overflow-x: auto;
139
+ padding-inline: var(--_content-viewer--attachments-horizontal--padding-inline);
140
+ scrollbar-width: none;
141
+ overscroll-behavior: none;
142
+ }
143
+ .attachments-horizontal__item {
144
+ position: relative;
145
+ cursor: pointer;
146
+ width: min-content;
147
+ }
148
+ .attachments-horizontal__item--single {
149
+ flex: 1 1 auto;
150
+ width: 100%;
151
+ }
152
+ .attachments-horizontal__item-icon {
153
+ position: absolute;
154
+ bottom: 2px;
155
+ right: 2px;
156
+ --sc-kit--icon--size: 1rem;
157
+ --sc-kit--icon--color: var(--sc-kit--color--text--on-accent);
158
+ }
159
+
160
+ .attachments-card {
161
+ --_attachments-card--background-color: #ffffff;
162
+ --_attachments-card--border-color: #e5e7eb;
163
+ --_attachments-card--text--color: #000000;
164
+ --_attachments-card--text--secondary--color: #6b7280;
165
+ display: grid;
166
+ grid-template-columns: 2.375rem 1fr;
167
+ grid-column-gap: var(--sc-kit--space--3);
168
+ align-items: center;
169
+ width: 15.625rem;
170
+ padding: 0.375rem;
171
+ background-color: rgb(from var(--_attachments-card--background-color) r g b/90%);
172
+ color: var(--_attachments-card--text--color);
173
+ border: 0.0625rem solid var(--_attachments-card--border-color);
174
+ border-radius: var(--sc-kit--radius--sm);
175
+ }
176
+ .attachments-card--dark {
177
+ --_attachments-card--background-color: #000000;
178
+ --_attachments-card--border-color: #000000;
179
+ --_attachments-card--text--color: #ffffff;
180
+ --_attachments-card--text--secondary--color: #d1d5db;
181
+ }
182
+ .attachments-card--single {
183
+ width: 100%;
184
+ }
185
+ .attachments-card__thumb {
186
+ --sc-kit--image--rounded--width: 2.375rem;
187
+ --sc-kit--image--rounded--height: 2.375rem;
188
+ overflow: hidden;
189
+ border-radius: 0.125rem;
190
+ }
191
+ .attachments-card__content {
192
+ min-width: 0;
193
+ display: flex;
194
+ flex-direction: column;
195
+ }
196
+ .attachments-card__title {
197
+ font-size: var(--sc-kit--font-size--md);
198
+ font-weight: var(--sc-kit--font-weight--semibold);
199
+ line-height: 1.0625rem;
200
+ white-space: nowrap;
201
+ overflow: hidden;
202
+ text-overflow: ellipsis;
203
+ }
204
+ .attachments-card__extra-info {
205
+ font-size: 0.625rem;
206
+ color: var(--_attachments-card--text--secondary--color);
207
+ white-space: nowrap;
208
+ overflow: hidden;
209
+ text-overflow: ellipsis;
210
+ }</style>
@@ -0,0 +1,15 @@
1
+ import type { ContentModel } from '../model';
2
+ import type { ContentViewerUiManager } from './ui-manager.svelte';
3
+ type Props = {
4
+ model: ContentModel;
5
+ uiManager: ContentViewerUiManager;
6
+ on?: {
7
+ productClick?: (productId: string) => void;
8
+ productImpression?: (productId: string, videoId: string) => void;
9
+ ctaClick?: (ctaId: string) => void;
10
+ ctaImpression?: (ctaId: string) => void;
11
+ };
12
+ };
13
+ declare const AttachmentsHorizontal: import("svelte").Component<Props, {}, "">;
14
+ type AttachmentsHorizontal = ReturnType<typeof AttachmentsHorizontal>;
15
+ export default AttachmentsHorizontal;
@@ -0,0 +1,143 @@
1
+ <script lang="ts">import { default as AttachmentsHorizontal } from './attachments-horizontal.svelte';
2
+ import { default as Texts } from './content-texts.svelte';
3
+ import { ContentViewerLocalization } from './content-viewer-localization';
4
+ import { default as Heading } from './heading.svelte';
5
+ import { default as ContentMedia } from './media/content-media.svelte';
6
+ import { ContentViewerUiManager } from './ui-manager.svelte';
7
+ import { ResponsivePlayerButtons } from '@streamscloud/kit/ui/player/buttons';
8
+ let { model, enableAttachments = true, controlActions, enableControls = true, autoplay = 'on-appearance', on, overlay } = $props();
9
+ const localization = new ContentViewerLocalization();
10
+ const uiManager = new ContentViewerUiManager();
11
+ $effect(() => {
12
+ uiManager.enableAttachments = enableAttachments;
13
+ uiManager.enableControls = enableControls;
14
+ });
15
+ const viewerMounted = (node) => {
16
+ const resizeObserver = new ResizeObserver(() => {
17
+ uiManager.isMobileView = node.clientWidth <= 576;
18
+ });
19
+ resizeObserver.observe(node);
20
+ return {
21
+ destroy() {
22
+ resizeObserver.unobserve(node);
23
+ }
24
+ };
25
+ };
26
+ const trackControlsPanelSize = (node) => {
27
+ const resizeObserver = new ResizeObserver(() => {
28
+ uiManager.controlsPanelWidth = node.clientWidth;
29
+ });
30
+ resizeObserver.observe(node);
31
+ return {
32
+ destroy() {
33
+ resizeObserver.unobserve(node);
34
+ uiManager.controlsPanelWidth = 0;
35
+ }
36
+ };
37
+ };
38
+ const handleArticleReadMore = $derived.by(() => {
39
+ const articleId = model.articleId;
40
+ const handler = on?.articleReadMore;
41
+ if (!articleId || !handler) {
42
+ return undefined;
43
+ }
44
+ return () => handler(articleId);
45
+ });
46
+ const variables = $derived(model.media.isGallery ? '--_content-viewer--information--padding-bottom: 45px' : '');
47
+ </script>
48
+
49
+ <div class="content-viewer" style={variables} use:viewerMounted>
50
+ <ContentMedia id={model.id} media={model.media} autoplay={autoplay} on={{ videoProgress: on?.progress }} />
51
+ {#if (uiManager.showAttachments && model.attachments) || model.heading || model.texts.kicker || model.texts.title || model.texts.text || model.texts.readMoreUrl || handleArticleReadMore}
52
+ <div class="content-viewer__information">
53
+ {#if model.heading}
54
+ <Heading model={model.heading} uiManager={uiManager} localization={localization} />
55
+ {/if}
56
+ <Texts model={model.texts} uiManager={uiManager} localization={localization} on={{ readMore: handleArticleReadMore }} />
57
+
58
+ {#if uiManager.showAttachments && model.attachments}
59
+ <AttachmentsHorizontal
60
+ model={model}
61
+ uiManager={uiManager}
62
+ on={{
63
+ productClick: on?.productClick,
64
+ productImpression: on?.productImpression,
65
+ ctaClick: on?.ctaClick,
66
+ ctaImpression: on?.ctaImpression
67
+ }} />
68
+ {/if}
69
+ </div>
70
+ {/if}
71
+
72
+ {#if uiManager.showControls}
73
+ <div class="content-viewer__controls-panel" use:trackControlsPanelSize>
74
+ <ResponsivePlayerButtons actions={controlActions} scaleEffect={true} />
75
+ </div>
76
+ {/if}
77
+ {#if overlay}
78
+ {@render overlay()}
79
+ {/if}
80
+ </div>
81
+
82
+ <!--
83
+ @component
84
+ Content viewer — renders a piece of channel content (video, image, text) with player controls and an overlay slot.
85
+ -->
86
+
87
+ <style>.content-viewer {
88
+ width: 100%;
89
+ min-width: 100%;
90
+ max-width: 100%;
91
+ height: 100%;
92
+ min-height: 100%;
93
+ max-height: 100%;
94
+ border-radius: var(--sc-kit--radius--md);
95
+ overflow: hidden;
96
+ position: relative;
97
+ /* Set 'container-type: inline-size;' to reference container*/
98
+ }
99
+ @container (width < 576px) {
100
+ .content-viewer {
101
+ border-radius: 0;
102
+ position: relative;
103
+ }
104
+ }
105
+ .content-viewer__controls-panel {
106
+ position: absolute;
107
+ left: auto;
108
+ right: 0;
109
+ bottom: 6.25rem;
110
+ gap: var(--sc-kit--space--10);
111
+ display: flex;
112
+ flex-direction: column;
113
+ justify-content: flex-end;
114
+ align-items: flex-end;
115
+ padding: 0 0.625rem;
116
+ /* Set 'container-type: inline-size;' to reference container*/
117
+ }
118
+ @container (width < 576px) {
119
+ .content-viewer__controls-panel {
120
+ padding-right: 0;
121
+ }
122
+ }
123
+ .content-viewer__information {
124
+ pointer-events: none;
125
+ position: absolute;
126
+ bottom: 0;
127
+ left: 0;
128
+ right: 0;
129
+ padding: 1.875rem 0;
130
+ padding-bottom: var(--_content-viewer--information--padding-bottom, 1.875rem);
131
+ display: flex;
132
+ flex-direction: column;
133
+ gap: var(--sc-kit--space--5);
134
+ background: linear-gradient(0deg, #000 0%, rgba(0, 0, 0, 0) 100%);
135
+ /* Set 'container-type: inline-size;' to reference container*/
136
+ }
137
+ @container (width < 576px) {
138
+ .content-viewer__information {
139
+ padding-block: var(--sc-kit--space--5);
140
+ padding-bottom: var(--_content-viewer--information--padding-bottom, var(--sc-kit--space--5));
141
+ gap: 0.625rem;
142
+ }
143
+ }</style>
@@ -0,0 +1,23 @@
1
+ import type { ContentModel } from '../model';
2
+ import { type PlayerButtonDef } from '@streamscloud/kit/ui/player/buttons';
3
+ import type { Snippet } from 'svelte';
4
+ type Props = {
5
+ model: ContentModel;
6
+ enableAttachments?: boolean;
7
+ enableControls?: boolean;
8
+ controlActions: PlayerButtonDef[];
9
+ autoplay?: true | false | 'on-appearance';
10
+ on?: {
11
+ progress?: (progress: number) => void;
12
+ productClick?: (productId: string) => void;
13
+ productImpression?: (productId: string, videoId: string) => void;
14
+ ctaClick?: (ctaId: string) => void;
15
+ ctaImpression?: (ctaId: string) => void;
16
+ articleReadMore?: (articleId: string) => void;
17
+ };
18
+ overlay?: Snippet;
19
+ };
20
+ /** Content viewer — renders a piece of channel content (video, image, text) with player controls and an overlay slot. */
21
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
22
+ type Cmp = ReturnType<typeof Cmp>;
23
+ export default Cmp;
@@ -0,0 +1,96 @@
1
+ <script lang="ts">import { LineClamp } from '@streamscloud/kit/ui/line-clamp';
2
+ let { model, uiManager, localization, on } = $props();
3
+ const variables = $derived(`--_content-viewer-texts--padding: 0 ${uiManager.controlsPanelWidth ? uiManager.controlsPanelWidth : uiManager.infoPaddingInline}px 0 ${uiManager.infoPaddingInline}px`);
4
+ </script>
5
+
6
+ <div class="content-viewer-texts" style={variables}>
7
+ {#if model.kicker}
8
+ <div class="content-viewer-texts__kicker">
9
+ <LineClamp maxLines={1} enableShowMore={false}>{model.kicker}</LineClamp>
10
+ </div>
11
+ {/if}
12
+ {#if model.title}
13
+ <div class="content-viewer-texts__title">
14
+ <LineClamp maxLines={2} enableShowMore={false}>{model.title}</LineClamp>
15
+ </div>
16
+ {/if}
17
+ {#if model.text}
18
+ <div class="content-viewer-texts__text">
19
+ <LineClamp maxLines={2} enableShowMore={true}>{model.text}</LineClamp>
20
+ </div>
21
+ {/if}
22
+ {#if model.readMoreUrl}
23
+ <a class="content-viewer-texts__read-more" href={model.readMoreUrl} target="_blank" rel="noopener noreferrer">
24
+ {localization.readMore}
25
+ </a>
26
+ {:else if on?.readMore}
27
+ <button type="button" class="content-viewer-texts__read-more" onclick={on.readMore}>
28
+ {localization.readMore}
29
+ </button>
30
+ {/if}
31
+ </div>
32
+
33
+ <style>.content-viewer-texts {
34
+ --_content-viewer-texts--color: var(--sc-kit--color--text--on-accent);
35
+ --_content-viewer-texts--shadow: 0 1px 0 rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 6px rgba(0, 0, 0, 0.05);
36
+ color: var(--_content-viewer-texts--color);
37
+ text-shadow: var(--_content-viewer-texts--shadow);
38
+ padding: var(--_content-viewer-texts--padding);
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: 0.625rem;
42
+ /* Set 'container-type: inline-size;' to reference container*/
43
+ }
44
+ @container (width < 576px) {
45
+ .content-viewer-texts {
46
+ gap: 0.375rem;
47
+ }
48
+ }
49
+ .content-viewer-texts__kicker {
50
+ font-size: var(--sc-kit--font-size--lg);
51
+ font-weight: var(--sc-kit--font-weight--regular);
52
+ /* Set 'container-type: inline-size;' to reference container*/
53
+ }
54
+ @container (width < 576px) {
55
+ .content-viewer-texts__kicker {
56
+ font-size: var(--sc-kit--font-size--md);
57
+ }
58
+ }
59
+ .content-viewer-texts__title {
60
+ font-size: var(--sc-kit--font-size--xl);
61
+ font-weight: var(--sc-kit--font-weight--medium);
62
+ /* Set 'container-type: inline-size;' to reference container*/
63
+ }
64
+ @container (width < 576px) {
65
+ .content-viewer-texts__title {
66
+ font-size: var(--sc-kit--font-size--lg);
67
+ }
68
+ }
69
+ .content-viewer-texts__text {
70
+ font-size: 1.125rem;
71
+ font-weight: var(--sc-kit--font-weight--regular);
72
+ /* Set 'container-type: inline-size;' to reference container*/
73
+ }
74
+ @container (width < 576px) {
75
+ .content-viewer-texts__text {
76
+ font-size: 0.9375rem;
77
+ }
78
+ }
79
+ .content-viewer-texts__read-more {
80
+ pointer-events: auto;
81
+ font-size: var(--sc-kit--font-size--lg);
82
+ font-weight: var(--sc-kit--font-weight--regular);
83
+ color: inherit;
84
+ cursor: pointer;
85
+ text-shadow: inherit;
86
+ background: none;
87
+ border: none;
88
+ padding: 0;
89
+ text-align: left;
90
+ /* Set 'container-type: inline-size;' to reference container*/
91
+ }
92
+ @container (width < 576px) {
93
+ .content-viewer-texts__read-more {
94
+ font-size: var(--sc-kit--font-size--md);
95
+ }
96
+ }</style>
@@ -0,0 +1,14 @@
1
+ import type { ContentModel } from '../model';
2
+ import type { ContentViewerLocalization } from './content-viewer-localization';
3
+ import type { ContentViewerUiManager } from './ui-manager.svelte';
4
+ type Props = {
5
+ model: ContentModel['texts'];
6
+ uiManager: ContentViewerUiManager;
7
+ localization: ContentViewerLocalization;
8
+ on?: {
9
+ readMore?: () => void;
10
+ };
11
+ };
12
+ declare const ContentTexts: import("svelte").Component<Props, {}, "">;
13
+ type ContentTexts = ReturnType<typeof ContentTexts>;
14
+ export default ContentTexts;
@@ -0,0 +1,4 @@
1
+ export declare class ContentViewerLocalization {
2
+ get viewsCount(): (count: number) => string;
3
+ get readMore(): string;
4
+ }
@@ -0,0 +1,16 @@
1
+ import { AppLocale } from '@streamscloud/kit/core/locale';
2
+ export class ContentViewerLocalization {
3
+ get viewsCount() {
4
+ return loc.viewsCount[AppLocale.current];
5
+ }
6
+ get readMore() {
7
+ return loc.readMore[AppLocale.current];
8
+ }
9
+ }
10
+ const loc = {
11
+ viewsCount: {
12
+ en: (count) => (count === 1 ? '1 view' : `${count} views`),
13
+ no: (count) => (count === 1 ? '1 visning' : `${count} visninger`)
14
+ },
15
+ readMore: { en: 'Read more', no: 'Les mer' }
16
+ };
@@ -0,0 +1,66 @@
1
+ <script lang="ts">import { ImageRound } from '@streamscloud/kit/ui/image';
2
+ import { TimeAgo } from '@streamscloud/kit/ui/time-ago';
3
+ let { model, uiManager, localization } = $props();
4
+ const variables = $derived(`--_content-viewer--heading--padding: 0 ${uiManager.controlsPanelWidth ? uiManager.controlsPanelWidth : uiManager.infoPaddingInline}px 14px ${uiManager.infoPaddingInline}px`);
5
+ </script>
6
+
7
+ <div class="content-viewer-heading" style={variables}>
8
+ <div class="content-viewer-heading__source-image">
9
+ <ImageRound src={model.image} alt={model.name} />
10
+ </div>
11
+
12
+ <div class="content-viewer-heading__info">
13
+ <div class="content-viewer-heading__source-name">{model.name}</div>
14
+ <p class="content-viewer-heading__metadata">
15
+ <TimeAgo date={model.displayDate} />
16
+ {#if Number.isInteger(model.viewsCount) && model.viewsCount}
17
+ <span>&middot;</span>
18
+ {localization.viewsCount(model.viewsCount)}
19
+ {/if}
20
+ </p>
21
+ </div>
22
+ </div>
23
+
24
+ <style>.content-viewer-heading {
25
+ --_content-viewer-heading-texts--color: var(--sc-kit--color--text--on-accent);
26
+ --_content-viewer-heading--text--shadow: 0 1px 0 rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 6px rgba(0, 0, 0, 0.05);
27
+ display: flex;
28
+ align-items: center;
29
+ padding: var(--_content-viewer--heading--padding);
30
+ min-width: 0;
31
+ }
32
+ .content-viewer-heading__source-image {
33
+ width: 2rem;
34
+ min-width: 2rem;
35
+ max-width: 2rem;
36
+ height: 2rem;
37
+ min-height: 2rem;
38
+ max-height: 2rem;
39
+ margin-right: 0.625rem;
40
+ --sc-kit--image--rounded--outer--border-radius: var(--sc-kit--radius--lg);
41
+ --sc-kit--image--rounded--inner--border-width: 0;
42
+ }
43
+ .content-viewer-heading__info {
44
+ display: flex;
45
+ flex-direction: column;
46
+ min-width: 0;
47
+ }
48
+ .content-viewer-heading__source-name {
49
+ font-size: var(--sc-kit--font-size--sm);
50
+ line-height: 0.9375rem;
51
+ font-weight: var(--sc-kit--font-weight--medium);
52
+ color: var(--_content-viewer-heading-texts--color);
53
+ text-shadow: var(--_content-viewer-heading--text--shadow);
54
+ text-overflow: ellipsis;
55
+ max-width: 100%;
56
+ white-space: nowrap;
57
+ overflow: hidden;
58
+ }
59
+ .content-viewer-heading__metadata {
60
+ margin: 0;
61
+ font-size: 0.625rem;
62
+ line-height: 0.75rem;
63
+ font-weight: var(--sc-kit--font-weight--regular);
64
+ color: var(--_content-viewer-heading-texts--color);
65
+ text-shadow: var(--_content-viewer-heading--text--shadow);
66
+ }</style>
@@ -0,0 +1,11 @@
1
+ import type { ContentViewerUiManager } from './ui-manager.svelte';
2
+ import type { ContentModel } from '../model';
3
+ import type { ContentViewerLocalization } from './content-viewer-localization';
4
+ type Props = {
5
+ model: NonNullable<ContentModel['heading']>;
6
+ uiManager: ContentViewerUiManager;
7
+ localization: ContentViewerLocalization;
8
+ };
9
+ declare const Heading: import("svelte").Component<Props, {}, "">;
10
+ type Heading = ReturnType<typeof Heading>;
11
+ export default Heading;
@@ -0,0 +1 @@
1
+ export { default as ContentViewer } from './cmp.content-viewer.svelte';
@@ -0,0 +1 @@
1
+ export { default as ContentViewer } from './cmp.content-viewer.svelte';
@@ -0,0 +1,53 @@
1
+ <script lang="ts">import { Image } from '@streamscloud/kit/ui/image';
2
+ import { Carousel } from '@streamscloud/kit/ui/player/carousel';
3
+ import { Video } from '@streamscloud/kit/ui/video';
4
+ let { id, media, autoplay = 'on-appearance', on } = $props();
5
+ </script>
6
+
7
+ {#snippet mediaItem(item: IContentMediaItemModel)}
8
+ {#if item.isImage}
9
+ <Image src={item.url} />
10
+ {:else}
11
+ <Video
12
+ src={item.url}
13
+ poster={null}
14
+ autoplay={autoplay}
15
+ loop={true}
16
+ id={id}
17
+ hideSpeaker={true}
18
+ on={{
19
+ progress: on?.videoProgress
20
+ }} />
21
+ {/if}
22
+ {/snippet}
23
+
24
+ <div class="content-media" class:content-media--fit={media.mediaFit === 'contain'} class:content-media--fill={media.mediaFit === 'cover'}>
25
+ {#if media.items.length === 1}
26
+ {@render mediaItem(media.items[0])}
27
+ {:else if media.items.length > 1}
28
+ <Carousel items={media.items} initialIndex={media.currentIndex} on={{ indexChanged: (index) => (media.currentIndex = index) }}>
29
+ {#snippet children(item)}
30
+ {@render mediaItem(item)}
31
+ {/snippet}
32
+ </Carousel>
33
+ {/if}
34
+ </div>
35
+
36
+ <style>.content-media {
37
+ width: 100%;
38
+ min-width: 100%;
39
+ max-width: 100%;
40
+ height: 100%;
41
+ min-height: 100%;
42
+ max-height: 100%;
43
+ }
44
+ .content-media--fit {
45
+ --sc-kit--video--media-fit: contain;
46
+ --sc-kit--image--object-fit: contain;
47
+ background-color: rgb(from light-dark(#000000, #ffffff) r g b/40%);
48
+ }
49
+ .content-media--fill {
50
+ --sc-kit--video--media-fit: cover;
51
+ --sc-kit--image--object-fit: cover;
52
+ --sc-kit--video--background-color: transparent;
53
+ }</style>