@streamscloud/embeddable 16.0.7-1772112133122 → 16.0.7-1772189105748

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 (45) hide show
  1. package/dist/articles/article/index.d.ts +2 -1
  2. package/dist/articles/article/types.d.ts +5 -0
  3. package/dist/articles/article-dialog/article-dialog-localization.d.ts +3 -0
  4. package/dist/articles/article-dialog/article-dialog-localization.js +9 -0
  5. package/dist/articles/article-dialog/cmp.article-dialog.svelte +98 -0
  6. package/dist/articles/article-dialog/cmp.article-dialog.svelte.d.ts +9 -0
  7. package/dist/articles/article-dialog/index.d.ts +6 -0
  8. package/dist/articles/article-dialog/index.js +10 -0
  9. package/dist/articles/article-dialog/types.d.ts +6 -0
  10. package/dist/articles/article-dialog/types.js +1 -0
  11. package/dist/articles/data-providers/index.d.ts +1 -0
  12. package/dist/articles/data-providers/index.js +1 -0
  13. package/dist/articles/data-providers/types.d.ts +14 -0
  14. package/dist/articles/data-providers/types.js +1 -0
  15. package/dist/external-api/article/cmp.embed-article.svelte +8 -27
  16. package/dist/external-api/data-providers/article/article-data-provider.d.ts +4 -0
  17. package/dist/external-api/data-providers/article/article-data-provider.js +32 -0
  18. package/dist/external-api/data-providers/article/index.d.ts +1 -0
  19. package/dist/external-api/data-providers/article/index.js +1 -0
  20. package/dist/external-api/{article → data-providers/article}/operations.generated.d.ts +1 -1
  21. package/dist/external-api/data-providers/internal-media-center-data-provider.svelte.js +3 -1
  22. package/dist/media-center/config/types.d.ts +3 -0
  23. package/dist/media-center/media-center/posts-feed/posts-feed-handler.svelte.js +1 -0
  24. package/dist/posts/model/post-model.d.ts +1 -0
  25. package/dist/posts/model/post-model.js +2 -0
  26. package/dist/posts/post-viewer/cmp.post-viewer.svelte +10 -2
  27. package/dist/posts/post-viewer/cmp.post-viewer.svelte.d.ts +1 -0
  28. package/dist/posts/post-viewer/post-texts.svelte +8 -1
  29. package/dist/posts/post-viewer/post-texts.svelte.d.ts +3 -0
  30. package/dist/posts/posts-player/cmp.posts-player.svelte +4 -1
  31. package/dist/posts/posts-player/cmp.posts-player.svelte.d.ts +2 -0
  32. package/dist/posts/posts-player/index.d.ts +8 -1
  33. package/dist/posts/posts-player/index.js +7 -1
  34. package/dist/posts/posts-player/posts-player-proxy.svelte +2 -1
  35. package/dist/posts/posts-player/posts-player-proxy.svelte.d.ts +2 -0
  36. package/dist/posts/posts-player/posts-player-view.svelte +18 -4
  37. package/dist/posts/posts-player/types.d.ts +3 -0
  38. package/dist/ui/shadow-dom/cmp.shadow-root.svelte +1 -0
  39. package/dist/ui/shadow-dom/index.d.ts +1 -1
  40. package/dist/ui/shadow-dom/index.js +1 -1
  41. package/dist/ui/shadow-dom/shadow-root-service.d.ts +1 -0
  42. package/dist/ui/shadow-dom/shadow-root-service.js +8 -0
  43. package/package.json +1 -1
  44. /package/dist/external-api/{article → data-providers/article}/operations.generated.js +0 -0
  45. /package/dist/external-api/{article → data-providers/article}/operations.graphql +0 -0
@@ -3,5 +3,6 @@ export { default as ArticleView } from './article-view.svelte';
3
3
  export { default as FactsContainer } from './cmp.facts-container.svelte';
4
4
  export { fieldIsFilled, layoutIsFilled, sectionIsFilled } from './helpers';
5
5
  export { ArticleStylesHelper } from './styles-transformer';
6
- export type { ArticleLayoutModel, ArticleLayoutStylesModel, ArticleMetadata, ArticleSectionModel, ArticleSectionStylesModel } from './types';
6
+ export type { ArticleData, ArticleDataProvider } from '../data-providers';
7
+ export type { ArticleLayoutModel, ArticleLayoutStylesModel, ArticleMetadata, ArticleSectionModel, ArticleSectionStylesModel, ArticleSeo } from './types';
7
8
  export type { ArticleFieldDataModel, ArticleFieldModel, ArticleFieldStylesModel, BylineFieldDataModel, FieldMetadata, ImageFieldDataModel, MediaFieldDataModel, MediaGalleryFieldDataModel, RichTextFieldDataModel, TextFieldDataModel, VideoFieldDataModel } from './fields/types';
@@ -2,6 +2,11 @@ import type { ArticleFieldModel } from './fields/types';
2
2
  export type ArticleMetadata = {
3
3
  displayDate: string;
4
4
  };
5
+ export type ArticleSeo = {
6
+ title: string;
7
+ description: string;
8
+ keywords: string[];
9
+ };
5
10
  export type ArticleSectionModel = {
6
11
  id: string;
7
12
  styles: ArticleSectionStylesModel | null;
@@ -0,0 +1,3 @@
1
+ export declare class ArticleDialogLocalization {
2
+ get failedToLoad(): string;
3
+ }
@@ -0,0 +1,9 @@
1
+ import { AppLocale } from '@streamscloud/kit/core/locale';
2
+ export class ArticleDialogLocalization {
3
+ get failedToLoad() {
4
+ return loc.failedToLoad[AppLocale.current];
5
+ }
6
+ }
7
+ const loc = {
8
+ failedToLoad: { en: 'Failed to load article', no: 'Kunne ikke laste artikkelen' }
9
+ };
@@ -0,0 +1,98 @@
1
+ <script lang="ts">import { default as ArticleView } from '../article/article-view.svelte';
2
+ import { ArticleDialogLocalization } from './article-dialog-localization';
3
+ import { DialogCloseButton } from '@streamscloud/kit/ui/dialog';
4
+ import { Loading } from '@streamscloud/kit/ui/loading';
5
+ import { untrack } from 'svelte';
6
+ let { controller, data } = $props();
7
+ const localization = new ArticleDialogLocalization();
8
+ $effect(() => untrack(() => {
9
+ controller.updateSettings({ closeOnEsc: true, closeOnClickOutside: true });
10
+ controller.updateContainerSettings({ position: 'full-screen' });
11
+ }));
12
+ let articleData = $state.raw(null);
13
+ let loading = $state(true);
14
+ let failed = $state(false);
15
+ const load = async () => {
16
+ loading = true;
17
+ failed = false;
18
+ const result = await data.fetchArticle({ id: data.articleId });
19
+ if (result) {
20
+ articleData = result;
21
+ }
22
+ else {
23
+ failed = true;
24
+ }
25
+ loading = false;
26
+ };
27
+ void load();
28
+ </script>
29
+
30
+ <div class="article-dialog">
31
+ <div class="article-dialog__close">
32
+ <DialogCloseButton controller={controller} />
33
+ </div>
34
+ <div class="article-dialog__content">
35
+ {#if loading}
36
+ <div class="article-dialog__loading">
37
+ <Loading positionAbsoluteCenter timeout={400} />
38
+ </div>
39
+ {:else if failed}
40
+ <div class="article-dialog__error">{localization.failedToLoad}</div>
41
+ {:else if articleData}
42
+ <ArticleView sections={articleData.sections} metadata={articleData.metadata} />
43
+ {/if}
44
+ </div>
45
+ </div>
46
+
47
+ <style>.article-dialog {
48
+ width: 100%;
49
+ height: 100%;
50
+ position: relative;
51
+ }
52
+ .article-dialog__close {
53
+ --sc-kit--dialog-close-button--color: #fff;
54
+ --sc-kit--icon--filter: drop-shadow(1px 1px 0 rgba(0, 0, 0, 0.2));
55
+ position: absolute;
56
+ top: 0.75rem;
57
+ right: 1rem;
58
+ z-index: 10;
59
+ }
60
+ .article-dialog__content {
61
+ --article--max-width: 56.25rem;
62
+ --article--container-background: light-dark(rgba(255, 255, 255, 0.8), rgba(0, 0, 0, 0.8));
63
+ width: 100%;
64
+ height: 100%;
65
+ overflow-y: auto;
66
+ --_cross-browser-scrollbar--thumb-color: var(--scrollbar--thumb-color, #7d7d7d);
67
+ --_cross-browser-scrollbar--track-color: var(--scrollbar--track-color, transparent);
68
+ }
69
+ .article-dialog__content::-webkit-scrollbar {
70
+ width: 6px;
71
+ height: 6px;
72
+ }
73
+ .article-dialog__content::-webkit-scrollbar-track {
74
+ background: var(--_cross-browser-scrollbar--track-color);
75
+ border-radius: 100vw;
76
+ }
77
+ .article-dialog__content::-webkit-scrollbar-thumb {
78
+ background: var(--_cross-browser-scrollbar--thumb-color);
79
+ border-radius: 100vw;
80
+ }
81
+ @supports (scrollbar-color: transparent transparent) {
82
+ .article-dialog__content {
83
+ scrollbar-color: var(--_cross-browser-scrollbar--thumb-color) var(--_cross-browser-scrollbar--track-color);
84
+ scrollbar-width: thin;
85
+ }
86
+ }
87
+ .article-dialog__loading {
88
+ position: relative;
89
+ min-height: 18.75rem;
90
+ }
91
+ .article-dialog__error {
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ min-height: 12.5rem;
96
+ color: light-dark(#9ca3af, #6b7280);
97
+ font-size: 1rem;
98
+ }</style>
@@ -0,0 +1,9 @@
1
+ import type { ArticleDialogViewData } from './types';
2
+ import { type DialogController } from '@streamscloud/kit/ui/dialog';
3
+ type Props = {
4
+ controller: DialogController;
5
+ data: ArticleDialogViewData;
6
+ };
7
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
8
+ type Cmp = ReturnType<typeof Cmp>;
9
+ export default Cmp;
@@ -0,0 +1,6 @@
1
+ import type { ArticleDataProvider } from '../data-providers';
2
+ export declare const openArticleDialog: (data: {
3
+ anchor: HTMLElement;
4
+ articleId: string;
5
+ fetchArticle: ArticleDataProvider["fetchArticle"];
6
+ }) => void;
@@ -0,0 +1,10 @@
1
+ import { default as ArticleDialog } from './cmp.article-dialog.svelte';
2
+ import { Dialogs } from '@streamscloud/kit/ui/dialog';
3
+ export const openArticleDialog = (data) => {
4
+ const { anchor, articleId, fetchArticle } = data;
5
+ void Dialogs.open({
6
+ view: ArticleDialog,
7
+ data: { articleId, fetchArticle },
8
+ host: anchor
9
+ });
10
+ };
@@ -0,0 +1,6 @@
1
+ import type { ArticleDataProvider } from '../data-providers';
2
+ /** Data passed to the article dialog view component. */
3
+ export type ArticleDialogViewData = {
4
+ articleId: string;
5
+ fetchArticle: ArticleDataProvider['fetchArticle'];
6
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export type { ArticleData, ArticleDataProvider } from './types';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { ArticleMetadata, ArticleSectionModel, ArticleSeo } from '../article';
2
+ /** Article data returned by a data provider. */
3
+ export type ArticleData = {
4
+ sections: ArticleSectionModel[];
5
+ metadata: ArticleMetadata;
6
+ seo: ArticleSeo;
7
+ };
8
+ /** Provider for fetching article content by ID. */
9
+ export type ArticleDataProvider = {
10
+ fetchArticle: (input: {
11
+ id?: string;
12
+ slug?: string;
13
+ }) => Promise<ArticleData | null>;
14
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,28 +1,11 @@
1
1
  <script lang="ts">import { Article } from '../../articles/article';
2
2
  import { createLocalGQLClient } from '../../core/graphql';
3
- import { GetEmbedArticleDocument } from './operations.generated';
3
+ import { createArticleDataProvider } from '../data-providers/article/article-data-provider';
4
4
  import { Loading } from '@streamscloud/kit/ui/loading';
5
5
  import { untrack } from 'svelte';
6
6
  let { id, slug, initiator, graphqlOrigin, theme, on } = $props();
7
- let sections = $state.raw(null);
8
- let metadata = $state.raw(null);
7
+ let data = $state.raw(null);
9
8
  let notFound = $state(false);
10
- const mapSections = (article) => article.sections.map((section) => ({
11
- id: section.id,
12
- facts: section.facts,
13
- styles: section.styles,
14
- layouts: section.layouts.map((layout) => ({
15
- id: layout.id,
16
- styles: layout.styles,
17
- fields: layout.fields.map((field) => ({
18
- id: field.id,
19
- name: field.name,
20
- description: field.description,
21
- styles: field.styles,
22
- fieldData: field.fieldData
23
- }))
24
- }))
25
- }));
26
9
  const extractCoverImageUrl = (article) => {
27
10
  for (const section of article.sections) {
28
11
  for (const layout of section.layouts) {
@@ -39,15 +22,13 @@ const extractCoverImageUrl = (article) => {
39
22
  };
40
23
  const load = async () => {
41
24
  const graphql = createLocalGQLClient(graphqlOrigin, { initiator });
42
- const result = await graphql.query(GetEmbedArticleDocument, { input: { id, slug } }).toPromise();
43
- if (!result.data?.article) {
25
+ const dataProvider = createArticleDataProvider(graphql);
26
+ data = await dataProvider.fetchArticle({ id, slug });
27
+ if (!data) {
44
28
  notFound = true;
45
29
  return;
46
30
  }
47
- const article = result.data.article;
48
- sections = mapSections(article);
49
- metadata = { displayDate: article.publishedAt ?? '' };
50
- on?.seoLoaded?.({ ...article.seo, imageUrl: extractCoverImageUrl(article) });
31
+ on?.seoLoaded?.({ ...data.seo, imageUrl: extractCoverImageUrl(data) });
51
32
  };
52
33
  $effect(() => {
53
34
  untrack(() => {
@@ -58,8 +39,8 @@ $effect(() => {
58
39
 
59
40
  {#if notFound}
60
41
  <div class="embed-article__not-found">¯\_(ツ)_/¯</div>
61
- {:else if sections && metadata}
62
- <Article sections={sections} metadata={metadata} theme={theme} />
42
+ {:else if data}
43
+ <Article sections={data.sections} metadata={data.metadata} theme={theme} />
63
44
  {:else}
64
45
  <div class="embed-article__loading">
65
46
  <Loading positionAbsoluteCenter timeout={600} />
@@ -0,0 +1,4 @@
1
+ import type { ArticleDataProvider } from '../../../articles/data-providers';
2
+ import type { Client } from '@urql/core';
3
+ /** Creates an ArticleDataProvider that fetches articles via the GetEmbedArticle GraphQL query. */
4
+ export declare const createArticleDataProvider: (graphql: Client) => ArticleDataProvider;
@@ -0,0 +1,32 @@
1
+ import { GetEmbedArticleDocument } from './operations.generated';
2
+ /** Creates an ArticleDataProvider that fetches articles via the GetEmbedArticle GraphQL query. */
3
+ export const createArticleDataProvider = (graphql) => ({
4
+ fetchArticle: async (input) => {
5
+ const { id, slug } = input;
6
+ const result = await graphql.query(GetEmbedArticleDocument, { input: { id, slug } }).toPromise();
7
+ const article = result.data?.article;
8
+ if (!article) {
9
+ return null;
10
+ }
11
+ return {
12
+ sections: article.sections.map((section) => ({
13
+ id: section.id,
14
+ facts: section.facts,
15
+ styles: section.styles,
16
+ layouts: section.layouts.map((layout) => ({
17
+ id: layout.id,
18
+ styles: layout.styles,
19
+ fields: layout.fields.map((field) => ({
20
+ id: field.id,
21
+ name: field.name,
22
+ description: field.description,
23
+ styles: field.styles,
24
+ fieldData: field.fieldData
25
+ }))
26
+ }))
27
+ })),
28
+ metadata: { displayDate: article.publishedAt ?? '' },
29
+ seo: article.seo
30
+ };
31
+ }
32
+ });
@@ -0,0 +1 @@
1
+ export { createArticleDataProvider } from './article-data-provider';
@@ -0,0 +1 @@
1
+ export { createArticleDataProvider } from './article-data-provider';
@@ -1,4 +1,4 @@
1
- import type * as SchemaTypes from '../../../gql/types';
1
+ import type * as SchemaTypes from '../../../../gql/types';
2
2
  import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
3
3
  export type GetEmbedArticleQueryVariables = SchemaTypes.Exact<{
4
4
  input: SchemaTypes.EmbedArticleInput;
@@ -1,4 +1,5 @@
1
1
  import { createLocalGQLClient } from '../../core/graphql';
2
+ import { createArticleDataProvider } from './article';
2
3
  import { InternalMediaCenterAnalyticsHandler } from './internal-media-center-analytics-handler';
3
4
  import { MockCategoryFollowingProvider, MockMediaCenterContentManagementHandler, MockMembershipHandler, MockNavigationHandler, MockPostSocialInteractionsHandler } from './mocks';
4
5
  import { GetMediaPageConfigDocument } from './operations.generated';
@@ -57,7 +58,8 @@ export class InternalMediaCenterDataProvider {
57
58
  limit,
58
59
  graphql: this.graphql
59
60
  });
60
- }
61
+ },
62
+ fetchArticle: createArticleDataProvider(this.graphql).fetchArticle
61
63
  };
62
64
  this.streamPlayer = {
63
65
  getStreamsCursor: async ({ filter, limit, continuationToken }) => {
@@ -1,3 +1,4 @@
1
+ import type { ArticleDataProvider } from '../../articles/data-providers';
1
2
  import type { PostType } from '../../core/enums';
2
3
  import type { IContentCategoryFollowingHandler } from '../categories-following';
3
4
  import type { IMediaCenterContentManagementHandler } from '../content-management';
@@ -36,6 +37,8 @@ export interface IMediaCenterDataProvider {
36
37
  items: PostPlayerModel[];
37
38
  continuationToken: string | null;
38
39
  }>;
40
+ /** Provider for fetching article content. Enables "Read more" button that opens an article dialog in posts player. */
41
+ fetchArticle?: ArticleDataProvider['fetchArticle'];
39
42
  };
40
43
  streamPlayer: {
41
44
  getStreamsCursor: (input: {
@@ -175,6 +175,7 @@ export class PostsFeedHandler {
175
175
  socialInteractionsHandler: this._dataProvider.handlers?.postSocialInteractionsHandler,
176
176
  sharingHandler: this._dataProvider.handlers?.postSharingHandler,
177
177
  analyticsHandler: this._dataProvider.handlers?.analyticsHandler,
178
+ articleDataProvider: this._dataProvider.postsPlayer.fetchArticle ? { fetchArticle: this._dataProvider.postsPlayer.fetchArticle } : undefined,
178
179
  playerSettings: this._mediaCenterSettingsHandler.playerSettings,
179
180
  closeOrchestrator: this._closeOrchestrator,
180
181
  on: {
@@ -5,6 +5,7 @@ export declare class PostModel {
5
5
  id: string;
6
6
  media: PostViewerMediaModel;
7
7
  postType: PostType | null;
8
+ articleId: string | null;
8
9
  texts: {
9
10
  kicker: string | null;
10
11
  title: string | null;
@@ -3,6 +3,7 @@ export class PostModel {
3
3
  id;
4
4
  media;
5
5
  postType;
6
+ articleId;
6
7
  texts;
7
8
  heading;
8
9
  enableSocialInteractions;
@@ -10,6 +11,7 @@ export class PostModel {
10
11
  constructor(init) {
11
12
  this.id = init.id;
12
13
  this.postType = init.postType;
14
+ this.articleId = init.articleId;
13
15
  this.media = new PostViewerMediaModel({ media: init.media, mediaFit: init.mediaFit, mediaIndex: init.mediaIndex });
14
16
  this.texts = {
15
17
  kicker: init.kicker,
@@ -45,6 +45,14 @@ const trackControlsPanelSize = (node) => {
45
45
  }
46
46
  };
47
47
  };
48
+ const handleArticleReadMore = $derived.by(() => {
49
+ const articleId = model.articleId;
50
+ const handler = on?.articleReadMore;
51
+ if (!articleId || !handler) {
52
+ return undefined;
53
+ }
54
+ return () => handler(articleId);
55
+ });
48
56
  const variables = $derived.by(() => {
49
57
  const values = [];
50
58
  if (model.media.isGallery) {
@@ -56,12 +64,12 @@ const variables = $derived.by(() => {
56
64
 
57
65
  <div class="post-viewer" style={variables} use:viewerMounted>
58
66
  <PostMedia id={model.id} media={model.media} autoplay={autoplay} on={{ videoProgress: on?.progress }} />
59
- {#if (uiManager.showAttachments && model.attachments) || model.heading || model.texts.kicker || model.texts.title || model.texts.text || model.texts.readMoreUrl}
67
+ {#if (uiManager.showAttachments && model.attachments) || model.heading || model.texts.kicker || model.texts.title || model.texts.text || model.texts.readMoreUrl || handleArticleReadMore}
60
68
  <div class="post-viewer__information">
61
69
  {#if model.heading}
62
70
  <Heading model={model.heading} uiManager={uiManager} localization={localization} />
63
71
  {/if}
64
- <Texts model={model.texts} uiManager={uiManager} localization={localization} />
72
+ <Texts model={model.texts} uiManager={uiManager} localization={localization} on={{ readMore: handleArticleReadMore }} />
65
73
 
66
74
  {#if uiManager.showAttachments && model.attachments}
67
75
  <AttachmentsHorizontal
@@ -15,6 +15,7 @@ type Props = {
15
15
  productImpression?: (productId: string, videoId: string) => void;
16
16
  adClick?: (adId: string) => void;
17
17
  adImpression?: (adId: string) => void;
18
+ articleReadMore?: (articleId: string) => void;
18
19
  };
19
20
  overlay?: Snippet;
20
21
  };
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">import { PostViewerLocalization } from './post-viewer-localization';
2
2
  import { PostViewerUiManager } from './ui-manager.svelte';
3
3
  import { LineClamp } from '@streamscloud/kit/ui/line-clamp';
4
- let { model, uiManager, localization } = $props();
4
+ let { model, uiManager, localization, on } = $props();
5
5
  const variables = $derived.by(() => {
6
6
  const values = [
7
7
  `--_post-viewer-texts--padding: 0 ${uiManager.controlsPanelWidth ? uiManager.controlsPanelWidth : uiManager.infoPaddingInline}px 0 ${uiManager.infoPaddingInline}px`
@@ -30,6 +30,10 @@ const variables = $derived.by(() => {
30
30
  <a class="post-viewer-texts__read-more" href={model.readMoreUrl} target="_blank" rel="noopener noreferrer">
31
31
  {localization.readMore}
32
32
  </a>
33
+ {:else if on?.readMore}
34
+ <button type="button" class="post-viewer-texts__read-more" onclick={on.readMore}>
35
+ {localization.readMore}
36
+ </button>
33
37
  {/if}
34
38
  </div>
35
39
 
@@ -83,6 +87,9 @@ const variables = $derived.by(() => {
83
87
  pointer-events: auto;
84
88
  font-size: 1rem;
85
89
  font-weight: 400;
90
+ color: inherit;
91
+ cursor: pointer;
92
+ text-shadow: inherit;
86
93
  /* Set 'container-type: inline-size;' to reference container*/
87
94
  }
88
95
  @container (width < 576px) {
@@ -5,6 +5,9 @@ type Props = {
5
5
  model: PostModel['texts'];
6
6
  uiManager: PostViewerUiManager;
7
7
  localization: PostViewerLocalization;
8
+ on?: {
9
+ readMore?: () => void;
10
+ };
8
11
  };
9
12
  declare const PostTexts: import("svelte").Component<Props, {}, "">;
10
13
  type PostTexts = ReturnType<typeof PostTexts>;
@@ -2,7 +2,7 @@
2
2
  import { createShadowRoot } from '../../ui/shadow-dom';
3
3
  import { default as PostsPlayerProxy } from './posts-player-proxy.svelte';
4
4
  import { mount, unmount } from 'svelte';
5
- let { dataProvider, socialInteractionsHandler, sharingHandler, playerSettings, analyticsHandler, on } = $props();
5
+ let { dataProvider, socialInteractionsHandler, sharingHandler, playerSettings, analyticsHandler, articleDataProvider, on } = $props();
6
6
  const initHost = (node) => {
7
7
  const shadowRoot = createShadowRoot(node);
8
8
  const mounted = mount(PostsPlayerProxy, {
@@ -23,6 +23,9 @@ const initHost = (node) => {
23
23
  get analyticsHandler() {
24
24
  return analyticsHandler;
25
25
  },
26
+ get articleDataProvider() {
27
+ return articleDataProvider;
28
+ },
26
29
  closeOrchestrator: new CloseOrchestrator({
27
30
  closeFn: async () => {
28
31
  await unmount(mounted);
@@ -1,3 +1,4 @@
1
+ import type { ArticleDataProvider } from '../../articles/data-providers';
1
2
  import type { IPostSocialInteractionsHandler } from '..';
2
3
  import type { IPostSharingHandler } from '../sharing';
3
4
  import type { IPostAnalyticsHandler, PostPlayerModel } from './types';
@@ -8,6 +9,7 @@ type Props = {
8
9
  socialInteractionsHandler?: IPostSocialInteractionsHandler;
9
10
  sharingHandler?: IPostSharingHandler;
10
11
  analyticsHandler?: IPostAnalyticsHandler;
12
+ articleDataProvider?: ArticleDataProvider;
11
13
  playerSettings?: {
12
14
  locale?: AppLocaleValue;
13
15
  showStreamsCloudWatermark?: boolean;
@@ -1,3 +1,4 @@
1
+ import type { ArticleDataProvider } from '../../articles/data-providers';
1
2
  import { type IMediaCenterDataProvider } from '../../media-center/config/types';
2
3
  import type { IPostSharingHandler } from '../sharing';
3
4
  import type { IPostSocialInteractionsHandler } from '../social-interactions';
@@ -5,7 +6,7 @@ import { default as PostsPlayer } from './cmp.posts-player.svelte';
5
6
  import type { IPostAnalyticsHandler, PostPlayerModel } from './types';
6
7
  import type { AppLocaleValue } from '@streamscloud/kit/core/locale';
7
8
  export { PostsPlayer, type PostPlayerModel };
8
- export type { IMediaCenterDataProvider, IPostAnalyticsHandler };
9
+ export type { ArticleDataProvider, IMediaCenterDataProvider, IPostAnalyticsHandler };
9
10
  /**
10
11
  * Opens the posts player modal.
11
12
  *
@@ -23,6 +24,11 @@ export type { IMediaCenterDataProvider, IPostAnalyticsHandler };
23
24
  * @param {IPostSocialInteractionsHandler} [init.socialInteractionsHandler]
24
25
  * Handler for social interactions (like, share, etc.).
25
26
  *
27
+ * Article data provider (optional)
28
+ * @param {ArticleDataProvider} [init.articleDataProvider]
29
+ * Provider for fetching article content. When provided, article posts will show
30
+ * a "Read more" button that opens an article dialog instead of navigating to a URL.
31
+ *
26
32
  * Player settings (optional)
27
33
  * @param {object} [init.playerSettings]
28
34
  * Player UI and behavior settings.
@@ -87,6 +93,7 @@ export declare function openPostsPlayer(init: {
87
93
  analyticsHandler?: IPostAnalyticsHandler;
88
94
  socialInteractionsHandler?: IPostSocialInteractionsHandler;
89
95
  sharingHandler?: IPostSharingHandler;
96
+ articleDataProvider?: ArticleDataProvider;
90
97
  playerSettings?: {
91
98
  disableBackground?: boolean;
92
99
  locale?: AppLocaleValue;
@@ -22,6 +22,11 @@ export { PostsPlayer };
22
22
  * @param {IPostSocialInteractionsHandler} [init.socialInteractionsHandler]
23
23
  * Handler for social interactions (like, share, etc.).
24
24
  *
25
+ * Article data provider (optional)
26
+ * @param {ArticleDataProvider} [init.articleDataProvider]
27
+ * Provider for fetching article content. When provided, article posts will show
28
+ * a "Read more" button that opens an article dialog instead of navigating to a URL.
29
+ *
25
30
  * Player settings (optional)
26
31
  * @param {object} [init.playerSettings]
27
32
  * Player UI and behavior settings.
@@ -82,7 +87,7 @@ export { PostsPlayer };
82
87
  * ```
83
88
  */
84
89
  export function openPostsPlayer(init) {
85
- const { postsProvider: dataProvider, analyticsHandler, socialInteractionsHandler, sharingHandler, playerSettings, on } = init;
90
+ const { postsProvider: dataProvider, analyticsHandler, socialInteractionsHandler, sharingHandler, articleDataProvider, playerSettings, on } = init;
86
91
  const shadowHost = new ModalShadowHost();
87
92
  let mounted = null;
88
93
  const makeCloseOrchestrator = () => new CloseOrchestrator({
@@ -102,6 +107,7 @@ export function openPostsPlayer(init) {
102
107
  socialInteractionsHandler,
103
108
  sharingHandler,
104
109
  analyticsHandler,
110
+ articleDataProvider,
105
111
  playerSettings,
106
112
  closeOrchestrator: makeCloseOrchestrator(),
107
113
  on: {
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">import {} from '../../ui/player/close-orchestrator';
2
2
  import { ShadowRoot } from '../../ui/shadow-dom';
3
3
  import { default as PostsPlayerView } from './posts-player-view.svelte';
4
- let { dataProvider, socialInteractionsHandler, sharingHandler, closeOrchestrator, playerSettings, analyticsHandler, on } = $props();
4
+ let { dataProvider, socialInteractionsHandler, sharingHandler, closeOrchestrator, playerSettings, analyticsHandler, articleDataProvider, on } = $props();
5
5
  let backgroundImageUrl = $state(null);
6
6
  const handleBackgroundImagedLoaded = (url) => {
7
7
  backgroundImageUrl = url;
@@ -19,6 +19,7 @@ const handleBackgroundImagedLoaded = (url) => {
19
19
  sharingHandler={sharingHandler}
20
20
  playerSettings={playerSettings}
21
21
  analyticsHandler={analyticsHandler}
22
+ articleDataProvider={articleDataProvider}
22
23
  closeOrchestrator={closeOrchestrator}
23
24
  on={{ postActivated: on?.postActivated, backgroundImageLoaded: playerSettings?.disableBackground === true ? undefined : handleBackgroundImagedLoaded }} />
24
25
  </ShadowRoot>
@@ -1,3 +1,4 @@
1
+ import type { ArticleDataProvider } from '../../articles/data-providers';
1
2
  import type { IPostSharingHandler } from '../sharing';
2
3
  import type { IPostSocialInteractionsHandler } from '../social-interactions';
3
4
  import { type ICloseOrchestrator } from '../../ui/player/close-orchestrator';
@@ -9,6 +10,7 @@ type Props = {
9
10
  socialInteractionsHandler?: IPostSocialInteractionsHandler;
10
11
  sharingHandler?: IPostSharingHandler;
11
12
  analyticsHandler?: IPostAnalyticsHandler;
13
+ articleDataProvider?: ArticleDataProvider;
12
14
  closeOrchestrator: ICloseOrchestrator;
13
15
  playerSettings?: {
14
16
  locale?: AppLocaleValue;
@@ -1,14 +1,27 @@
1
- <script lang="ts">import { PostAttachments } from '../attachments';
1
+ <script lang="ts">import { openArticleDialog } from '../../articles/article-dialog';
2
+ import { PostAttachments } from '../attachments';
2
3
  import { PostActionsGenerator } from '../controls';
3
4
  import { getPostCoverImage, PostModel } from '../model';
4
5
  import { PostViewer } from '../post-viewer';
5
6
  import { Player, PlayerConfig, PlayerSettings } from '../../ui/player';
7
+ import { getDialogAnchor } from '../../ui/shadow-dom';
6
8
  import IconDelete from '@fluentui/svg-icons/icons/delete_32_regular.svg?raw';
7
9
  import IconEdit from '@fluentui/svg-icons/icons/edit_32_regular.svg?raw';
8
10
  import { preloadImage } from '@streamscloud/kit/core/utils';
9
11
  import { initBufferFromProvider } from '@streamscloud/kit/ui/player/providers';
10
12
  import { untrack } from 'svelte';
11
- let { dataProvider, socialInteractionsHandler, sharingHandler, playerSettings, analyticsHandler, managementActions, closeOrchestrator, on } = $props();
13
+ let { dataProvider, socialInteractionsHandler, sharingHandler, playerSettings, analyticsHandler, managementActions, articleDataProvider, closeOrchestrator, on } = $props();
14
+ let playerHostElement = $state();
15
+ const handleArticleReadMore = (articleId) => {
16
+ if (!articleDataProvider || !playerHostElement) {
17
+ return;
18
+ }
19
+ const anchor = getDialogAnchor(playerHostElement);
20
+ if (!anchor) {
21
+ return;
22
+ }
23
+ openArticleDialog({ anchor, articleId, fetchArticle: articleDataProvider.fetchArticle });
24
+ };
12
25
  let buffer = $state.raw(null);
13
26
  let mappedPostsCache = {};
14
27
  $effect(() => {
@@ -145,7 +158,7 @@ const currentItemActions = $derived.by(() => {
145
158
  adImpression: (id) => onAdImpression(id)
146
159
  }} />
147
160
  {/snippet}
148
- <div class="player-host">
161
+ <div class="player-host" bind:this={playerHostElement}>
149
162
  <Player
150
163
  config={contentPlayerConfig}
151
164
  itemActions={currentItemActions}
@@ -164,7 +177,8 @@ const currentItemActions = $derived.by(() => {
164
177
  productClick: (productId) => onProductClick(productId, postModel.id),
165
178
  productImpression: (productId) => onProductImpression(productId, postModel.id),
166
179
  adClick: (adId) => onAdClick(adId),
167
- adImpression: (adId) => onAdImpression(adId)
180
+ adImpression: (adId) => onAdImpression(adId),
181
+ articleReadMore: articleDataProvider ? handleArticleReadMore : undefined
168
182
  }} />
169
183
  {/snippet}
170
184
  </Player>
@@ -1,3 +1,4 @@
1
+ import type { ArticleDataProvider } from '../../articles/data-providers';
1
2
  import type { PostType } from '../../core/enums';
2
3
  import type { IPostModel } from '..';
3
4
  import type { IPostSharingHandler } from '../sharing';
@@ -26,6 +27,8 @@ export type PostsPlayerProps = {
26
27
  sharingHandler?: IPostSharingHandler;
27
28
  analyticsHandler?: IPostAnalyticsHandler;
28
29
  managementActions?: PostManagementActions;
30
+ /** Provider for fetching article content. Enables "Read more" button that opens an article dialog. */
31
+ articleDataProvider?: ArticleDataProvider;
29
32
  playerSettings?: PostPlayerSettings;
30
33
  closeOrchestrator: ICloseOrchestrator;
31
34
  on?: {
@@ -45,6 +45,7 @@ const styles = $derived.by(() => {
45
45
  style:color-scheme={theme ?? 'light dark'}
46
46
  style={styles}>
47
47
  {@render children()}
48
+ <div data-dialog-anchor></div>
48
49
  </div>
49
50
 
50
51
  <style>@charset "UTF-8";
@@ -1,3 +1,3 @@
1
1
  export { ModalShadowHost } from './modal-shadow-host';
2
- export { createShadowRoot } from './shadow-root-service';
2
+ export { createShadowRoot, getDialogAnchor } from './shadow-root-service';
3
3
  export { default as ShadowRoot } from './cmp.shadow-root.svelte';
@@ -1,3 +1,3 @@
1
1
  export { ModalShadowHost } from './modal-shadow-host';
2
- export { createShadowRoot } from './shadow-root-service';
2
+ export { createShadowRoot, getDialogAnchor } from './shadow-root-service';
3
3
  export { default as ShadowRoot } from './cmp.shadow-root.svelte';
@@ -1 +1,2 @@
1
1
  export declare const createShadowRoot: (host: HTMLElement) => ShadowRoot;
2
+ export declare const getDialogAnchor: (element: HTMLElement) => HTMLElement | null;
@@ -11,3 +11,11 @@ export const createShadowRoot = (host) => {
11
11
  const prepareShadowRootHost = (host) => {
12
12
  host.style.all = 'unset';
13
13
  };
14
+ const DIALOG_ANCHOR_SELECTOR = '[data-dialog-anchor]';
15
+ export const getDialogAnchor = (element) => {
16
+ const root = element.getRootNode();
17
+ if (root instanceof ShadowRoot) {
18
+ return root.querySelector(DIALOG_ANCHOR_SELECTOR);
19
+ }
20
+ return null;
21
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/embeddable",
3
- "version": "16.0.7-1772112133122",
3
+ "version": "16.0.7-1772189105748",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",