@playpilot/tpi 8.22.0-beta.1 → 8.23.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.22.0-beta.1",
3
+ "version": "8.23.0-beta.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -38,7 +38,7 @@
38
38
  "svelte": "5.55.7",
39
39
  "svelte-check": "^4.6.0",
40
40
  "svelte-preprocess": "^6.0.5",
41
- "svelte-tiny-slider": "^2.7.2",
41
+ "svelte-tiny-slider": "^2.8.0",
42
42
  "typescript": "^6.0.3",
43
43
  "typescript-eslint": "^8.62.0",
44
44
  "vite": "^5.4.21",
@@ -7,10 +7,9 @@ type Options = {
7
7
  headers?: Record<string, string>
8
8
  method?: 'POST' | 'GET'
9
9
  body?: null | Record<string, any>
10
- baseUrl?: string
11
10
  }
12
11
 
13
- export async function api<T>(path: string, { headers = {}, method = 'GET', body = null, baseUrl = apiBaseUrl }: Options = {}): Promise<T> {
12
+ export async function api<T>(path: string, { headers = {}, method = 'GET', body = null }: Options = {}): Promise<T> {
14
13
  // For GET methods we store all results in a cache object. This cache is super simple and has no invalidation at all.
15
14
  // Once something is stored it's there forever since the page is only short lived.
16
15
  // We never use the cache for editiorial mode since in the editor requests do change.
@@ -19,7 +18,7 @@ export async function api<T>(path: string, { headers = {}, method = 'GET', body
19
18
 
20
19
  const baseHeaders: Record<string, string> = method === 'GET' ? {} : { 'Content-Type': 'application/json' }
21
20
 
22
- const response = await fetch(baseUrl + path, {
21
+ const response = await fetch(apiBaseUrl + path, {
23
22
  method,
24
23
  headers: new Headers({ ...baseHeaders, ...headers }),
25
24
  // eslint-disable-next-line no-undef
package/src/lib/modal.ts CHANGED
@@ -11,11 +11,12 @@ import { playFallbackViewTransition } from './viewTransition'
11
11
  import { destroyLinkPopover } from './popover'
12
12
  import { prefersReducedMotion } from 'svelte/motion'
13
13
  import TitlesRailModal from '../routes/components/Modals/TitlesRailModal.svelte'
14
+ import TitlesReelModal from '../routes/components/Modals/TitlesReelModal.svelte'
14
15
 
15
16
  type Modal = {
16
17
  injection?: LinkInjection | null
17
18
  component?: object
18
- type: 'title' | 'participant' | 'titles-rail'
19
+ type: 'title' | 'participant' | 'titles-rail' | 'titles-reel'
19
20
  data: TitleData | TitleData[] | ParticipantData | null
20
21
  scrollPosition?: number
21
22
  }
@@ -52,6 +53,7 @@ function getModalComponentByType(
52
53
  { type: Modal['type'], target: Element, data: Modal['data'], props?: Record<string, any> }): ReturnType<typeof mount> {
53
54
  if (type === 'participant') return mount(ParticipantModal, { target, props: { participant: data as ParticipantData, ...props } })
54
55
  if (type === 'titles-rail') return mount(TitlesRailModal, { target, props: { titles: data as TitleData[], ...props } })
56
+ if (type === 'titles-reel') return mount(TitlesReelModal, { target, props: { titles: data as TitleData[], ...props } })
55
57
  return mount(TitleModal, { target, props: { title: data as TitleData, ...props } })
56
58
  }
57
59
 
@@ -18,6 +18,8 @@ declare global {
18
18
  declare module 'svelte/elements' {
19
19
  interface SvelteWindowAttributes {
20
20
  onupdateconsent?: (event: CustomEvent<{}>) => void
21
+ onplayembed?: (event: CustomEvent<{}>) => void
22
+ onpauseembed?: (event: CustomEvent<{}>) => void
21
23
  }
22
24
  }
23
25
 
@@ -1 +1 @@
1
- export type LanguageCode = 'en-US' | 'sv-SE' | 'da-DK'
1
+ export type LanguageCode = 'en-US' | 'sv-SE' | 'dk-DK'
@@ -2,23 +2,37 @@
2
2
  import type { TitleData } from '$lib/types/title'
3
3
  import { fetchTitles } from '$lib/api/titles'
4
4
  import { t } from '$lib/localization'
5
+ import { Sorting } from '$lib/enums/Sorting'
5
6
  import { track } from '$lib/tracking'
6
7
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
7
8
  import TitlesRail from '../../Rails/TitlesRail.svelte'
8
9
  import { mobileBreakpoint } from '$lib/constants'
9
- import { getLanguage } from '$lib/language'
10
- import type { LanguageCode } from '$lib/types/language'
11
- import { api } from '$lib/api/api'
12
10
 
13
11
  let expandedTitle: TitleData | null = $state(null)
14
12
  let expandedRailKey: string | null = $state(null)
15
13
  let windowWidth: number = $state(window.innerWidth)
16
14
 
17
- type Rail = { headings: Record<LanguageCode, string>, params: Record<string, any>, properties: Record<string, any> }
18
-
19
- async function fetchRails(): Promise<Rail[]> {
20
- return await api<Rail[]>('/streaming-guide/rails', { baseUrl: 'https://partner.playpilot.net/api' })
21
- }
15
+ const rails: { heading: string, params: Record<string, any>, properties: Record<string, any> }[] = [{
16
+ heading: 'List: Upcoming',
17
+ params: { from_playlist_sid: 'li42wf', region: null, no_region_filter: true },
18
+ properties: { expandable: true },
19
+ }, {
20
+ heading: 'List: Trending',
21
+ params: { ordering: Sorting.Popular },
22
+ properties: { expandable: true },
23
+ }, {
24
+ heading: 'List: New',
25
+ params: { from_playlist_sid: 'li42WR', include_playable_types: 'SVOD,FREE' },
26
+ properties: { aside: true, size: 'flexible' },
27
+ }, {
28
+ heading: 'List: Cinema',
29
+ params: { from_playlist_sid: 'li42WS', region: null, no_region_filter: true },
30
+ properties: { expandable: true },
31
+ }, {
32
+ heading: 'List: Demand',
33
+ params: { from_playlist_sid: 'licBS' },
34
+ properties: { aside: true, size: 'flexible' },
35
+ }]
22
36
 
23
37
  async function getListTitles(params: Record<string, any> = {}): Promise<TitleData[]> {
24
38
  try {
@@ -35,28 +49,20 @@
35
49
 
36
50
  <div data-testid="explore-home"></div>
37
51
 
38
- {#await fetchRails()}
39
- <TitlesRail titles={new Promise((res) => setTimeout(res, 10000))} size={windowWidth >= mobileBreakpoint ? 'flexible' : 'huge'} />
40
- {:then rails}
41
- {#each rails as { headings, params, properties }}
42
- {@const heading = headings[getLanguage()]}
43
-
44
- <div class="rail">
45
- <TitlesRail
46
- {heading}
47
- titles={getListTitles(params)}
48
- size={windowWidth >= mobileBreakpoint ? (properties.size || 'flexible') : 'huge'}
49
- minimumLength={7}
50
- {...properties}
51
- bind:expandedTitle
52
- bind:expandedRailKey
53
- onclick={() => track(TrackingEvent.ExploreTitleRailModalView, null, { rail: headings })}
54
- onexpand={(title) => track(TrackingEvent.ExploreTitleRailExpandClick, title, { rail: headings })} />
55
- </div>
56
- {/each}
57
- {:catch}
58
- {t('An Error Occurred')}
59
- {/await}
52
+ {#each rails as { heading, params, properties }}
53
+ <div class="rail">
54
+ <TitlesRail
55
+ heading={t(heading)}
56
+ titles={getListTitles(params)}
57
+ size={windowWidth >= mobileBreakpoint ? 'flexible' : 'huge'}
58
+ minimumLength={7}
59
+ {...properties}
60
+ bind:expandedTitle
61
+ bind:expandedRailKey
62
+ onclick={() => track(TrackingEvent.ExploreTitleRailModalView, null, { rail: heading })}
63
+ onexpand={(title) => track(TrackingEvent.ExploreTitleRailExpandClick, title, { rail: heading })} />
64
+ </div>
65
+ {/each}
60
66
 
61
67
  <style lang="scss">
62
68
  .rail {
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
3
+ import { mobileBreakpoint } from '$lib/constants'
3
4
  import { openModal } from '$lib/modal'
4
5
  import type { TitleData } from '$lib/types/title'
5
6
 
@@ -14,6 +15,7 @@
14
15
  async function openModalViaRoute(): Promise<void> {
15
16
  const currentUrl = new URL(document.location.toString())
16
17
  const sid = currentUrl.searchParams.get('sid')
18
+ const type = window.innerWidth > mobileBreakpoint ? 'titles-rail' : 'titles-reel'
17
19
 
18
20
  if (!sid) return
19
21
 
@@ -24,7 +26,7 @@
24
26
  ).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
25
27
 
26
28
  openModal({
27
- type: 'titles-rail',
29
+ type,
28
30
  data: [(title as TitleData), ...(railTitles as TitleData[])],
29
31
  props: {
30
32
  onclose: () => navigate('home'),
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import { scale } from 'svelte/transition'
3
+ import { destroyAllModals } from '$lib/modal'
4
+ import RoundButton from '../RoundButton.svelte'
5
+ import IconClose from '../Icons/IconClose.svelte'
6
+ import IconArrow from '../Icons/IconArrow.svelte'
7
+ import Button from '../Button.svelte'
8
+
9
+ interface Props {
10
+ label?: string
11
+ onclose?: () => void
12
+ }
13
+
14
+ const { label = '', onclose = () => null }: Props = $props()
15
+ </script>
16
+
17
+ {#if label}
18
+ <div class="back">
19
+ <Button variant="link" onclick={() => { onclose(); destroyAllModals() }}>
20
+ <IconArrow direction="left" />
21
+ {label}
22
+ </Button>
23
+ </div>
24
+ {/if}
25
+
26
+ <div class="close" transition:scale|global>
27
+ <RoundButton size="42px" onclick={() => { onclose(); destroyAllModals() }} aria-label="Close">
28
+ <IconClose size={24} />
29
+ </RoundButton>
30
+ </div>
31
+
32
+ <style lang="scss">
33
+ .back {
34
+ position: absolute;
35
+ top: margin(1.5);
36
+ left: var(--modal-close-overlay-left, margin(1));
37
+ font-family: theme(font-family);
38
+ transition: transform 50ms;
39
+
40
+ @include desktop {
41
+ left: margin(1);
42
+ }
43
+
44
+ &:hover {
45
+ transform: scale(1.025);
46
+
47
+ :global(button) {
48
+ text-decoration-thickness: 2px;
49
+ }
50
+ }
51
+
52
+ :global(button) {
53
+ text-decoration: underline;
54
+ color: white;
55
+ }
56
+ }
57
+
58
+ .close {
59
+ --playpilot-button-background: transparent;
60
+ --playpilot-button-text-color: white;
61
+ position: absolute;
62
+ top: margin(1);
63
+ right: margin(1);
64
+ }
65
+ </style>
@@ -1,13 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { onMount, type Snippet } from 'svelte'
3
3
  import TinySlider from 'svelte-tiny-slider'
4
- import { fade, scale } from 'svelte/transition'
5
- import { destroyAllModals } from '$lib/modal'
4
+ import { fade } from 'svelte/transition'
6
5
  import Modal from './Modal.svelte'
7
- import RoundButton from '../RoundButton.svelte'
8
- import IconClose from '../Icons/IconClose.svelte'
9
6
  import IconArrow from '../Icons/IconArrow.svelte'
10
- import Button from '../Button.svelte'
7
+ import ModalCloseOverlay from './ModalCloseOverlay.svelte'
11
8
 
12
9
  interface Props {
13
10
  items: Record<string, any>[]
@@ -76,19 +73,8 @@
76
73
  </TinySlider>
77
74
  </div>
78
75
 
79
- {#if window.location.href.includes('route=modal')}
80
- <div class="back">
81
- <Button variant="link" onclick={() => { onclose(); destroyAllModals() }}>
82
- <IconArrow direction="left" />
83
- Streaming guide
84
- </Button>
85
- </div>
86
- {/if}
87
-
88
- <div class="close" transition:scale|global>
89
- <RoundButton size="42px" onclick={() => { onclose(); destroyAllModals() }} aria-label="Close">
90
- <IconClose size={24} />
91
- </RoundButton>
76
+ <div class="close">
77
+ <ModalCloseOverlay {onclose} label={window.location.href.includes('route=modal') ? 'Streaming guide' : ''} />
92
78
  </div>
93
79
  {/snippet}
94
80
  </Modal>
@@ -178,39 +164,6 @@
178
164
  }
179
165
  }
180
166
 
181
- .close {
182
- --playpilot-button-background: transparent;
183
- --playpilot-button-text-color: white;
184
- position: absolute;
185
- top: margin(1);
186
- right: margin(1);
187
- }
188
-
189
- .back {
190
- position: absolute;
191
- top: margin(1.5);
192
- left: calc((100vw - $size) / 2);
193
- font-family: theme(font-family);
194
- transition: transform 50ms;
195
-
196
- @include desktop {
197
- left: margin(1);
198
- }
199
-
200
- &:hover {
201
- transform: scale(1.025);
202
-
203
- :global(button) {
204
- text-decoration-thickness: 2px;
205
- }
206
- }
207
-
208
- :global(button) {
209
- text-decoration: underline;
210
- color: white;
211
- }
212
- }
213
-
214
167
  .arrow {
215
168
  --offset: #{margin(-1.25)};
216
169
  --scale: 1;
@@ -254,4 +207,8 @@
254
207
  right: var(--offset);
255
208
  }
256
209
  }
210
+
211
+ .close {
212
+ --modal-close-overlay-left: calc((100vw - $size) / 2)
213
+ }
257
214
  </style>
@@ -0,0 +1,188 @@
1
+ <script lang="ts">
2
+ import type { TitleData } from '$lib/types/title'
3
+ import { onMount } from 'svelte'
4
+ import { heading } from '$lib/actions/heading'
5
+ import { t } from '$lib/localization'
6
+ import TinySlider from 'svelte-tiny-slider'
7
+ import Modal from './Modal.svelte'
8
+ import TitleBackground from '../TitleBackground.svelte'
9
+ import Description from '../Description.svelte'
10
+ import TitleInfo from '../TitleInfo.svelte'
11
+ import PlaylinksCompact from '../Playlinks/PlaylinksCompact.svelte'
12
+ import PlaylinksDisclaimer from '../Playlinks/PlaylinksDisclaimer.svelte'
13
+ import ModalCloseOverlay from './ModalCloseOverlay.svelte'
14
+
15
+ interface Props {
16
+ titles: TitleData[],
17
+ }
18
+
19
+ const { titles }: Props = $props()
20
+
21
+ let interval: ReturnType<typeof setInterval> | null = null
22
+
23
+ onMount(() => {
24
+ pauseAndPlay(0)
25
+
26
+ return () => {
27
+ if (interval) clearInterval(interval)
28
+ }
29
+ })
30
+
31
+ function pauseAndPlay(index: number): void {
32
+ ;[titles[index - 1], titles[index + 1]].forEach(title => {
33
+ window.dispatchEvent(new CustomEvent('pauseembed', { detail: title?.embeddable_url } ))
34
+ })
35
+
36
+ const currentTitle = titles[index]
37
+
38
+ if (interval) clearInterval(interval)
39
+
40
+ // This is on an interval because we don't know when the video loads, so we just continuously play and hope for the best.
41
+ interval = setInterval(() => {
42
+ window.dispatchEvent(new CustomEvent('playembed', { detail: currentTitle?.embeddable_url } ))
43
+ }, 500)
44
+ }
45
+ </script>
46
+
47
+ <div class="reel">
48
+ <Modal>
49
+ {#snippet dialog()}
50
+ <div class="slider">
51
+ <TinySlider vertical threshold={100} moveThreshold={20} onchange={pauseAndPlay}>
52
+ {#snippet children({ currentIndex, setIndex })}
53
+ {#each titles as title, index}
54
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
55
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
56
+ <div class="item" onclick={() => setIndex(index)}>
57
+ {#if Math.abs(index - currentIndex) <= 2 || currentIndex === index}
58
+ <div class="content">
59
+ <div class="header">
60
+ <div class="heading" use:heading={2}>{title.title}</div>
61
+
62
+ <TitleInfo {title} />
63
+ </div>
64
+
65
+ <div class="details">
66
+ <Description text={title.description!} blurb={title.blurb} limit={80} />
67
+
68
+ <div class="playlinks-heading" use:heading={3}>{t('Where To Stream Online')}</div>
69
+
70
+ <PlaylinksCompact playlinks={title.providers} size={24} />
71
+
72
+ <PlaylinksDisclaimer playlinks={title.providers} />
73
+ </div>
74
+ </div>
75
+
76
+ <TitleBackground {title} useVideoBackground autoplay={false} inline />
77
+
78
+ <div class="gradient"></div>
79
+ {/if}
80
+ </div>
81
+ {/each}
82
+ {/snippet}
83
+ </TinySlider>
84
+ </div>
85
+
86
+ <ModalCloseOverlay label="Streaming guide" />
87
+ {/snippet}
88
+ </Modal>
89
+ </div>
90
+
91
+ <style lang="scss">
92
+ $modal-height: 90dvh;
93
+ $item-height: calc(90dvh - margin(7));
94
+
95
+ .reel {
96
+ --playpilot-detail-background-height: #{$item-height};
97
+ --playpilot-detail-background-border-radius: 0;
98
+ --playpilot-detail-background-width: 400%;
99
+ --background-mask-image: unset;
100
+
101
+ :global(*) {
102
+ box-sizing: border-box;
103
+ }
104
+
105
+ :global(iframe) {
106
+ pointer-events: none;
107
+ z-index: 0;
108
+ }
109
+
110
+ :global(.modal) {
111
+ padding: 0;
112
+ }
113
+ }
114
+
115
+ .slider {
116
+ margin-top: 10dvh;
117
+ height: $modal-height;
118
+ width: 100%;
119
+ border-radius: theme(border-radius-large) theme(border-radius-large) 0 0;
120
+ overflow: hidden;
121
+ }
122
+
123
+ .item {
124
+ position: relative;
125
+ width: 100dvw;
126
+ height: $item-height;
127
+ background: theme(detail-background, light);
128
+ overflow: hidden;
129
+ }
130
+
131
+ .content {
132
+ position: relative;
133
+ display: flex;
134
+ flex-direction: column;
135
+ justify-content: space-between;
136
+ height: 100%;
137
+ color: theme(detail-text-color, text-color);
138
+ font-family: theme(detail-font-family, font-family);
139
+ font-weight: theme(detail-font-weight, normal);
140
+ font-size: theme(detail-font-size, font-size-base);
141
+ line-height: normal;
142
+ font-style: normal;
143
+ }
144
+
145
+ .header {
146
+ position: relative;
147
+ z-index: 2;
148
+ padding: theme(reel-item-padding, margin(2));
149
+ font-size: theme(reel-header-font-size, font-size-small);
150
+ }
151
+
152
+ .heading {
153
+ margin-bottom: margin(0.5);
154
+ color: theme(detail-title-text-color, text-color);
155
+ font-family: theme(detail-title-font-family, inherit);
156
+ font-weight: theme(detail-title-font-weight, lighter);
157
+ font-size: theme(detail-title-font-size, margin(1.25));
158
+ font-style: theme(detail-title-font-style, normal);
159
+ line-height: 1.2;
160
+ }
161
+
162
+ .details {
163
+ z-index: 1;
164
+ position: relative;
165
+ padding: theme(reel-item-padding, margin(2));
166
+ background: linear-gradient(to bottom, transparent, theme(detail-background, light) 4rem);
167
+ font-size: theme(detail-font-size, font-size-base);
168
+ }
169
+
170
+ .playlinks-heading {
171
+ margin: margin(0.5) 0;
172
+ color: theme(reel-playlinks-title-color, text-color-alt);
173
+ font-family: theme(reel-playlinks-title-font-family, inherit);
174
+ font-weight: theme(reel-playlinks-title-font-weight, lighter);
175
+ font-size: theme(reel-playlinks-title-font-size, font-size-small);
176
+ line-height: normal;
177
+ }
178
+
179
+ .gradient {
180
+ z-index: 1;
181
+ position: absolute;
182
+ top: 0;
183
+ left: 0;
184
+ width: 100%;
185
+ height: margin(7);
186
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8), transparent);
187
+ }
188
+ </style>
@@ -10,6 +10,7 @@
10
10
  import { getContext } from 'svelte'
11
11
  import Playlink from './Playlink.svelte'
12
12
  import Button from '../Button.svelte'
13
+ import PlaylinksDisclaimer from './PlaylinksDisclaimer.svelte'
13
14
 
14
15
  type Category = PlaylinkCategory | '' | 'Other'
15
16
  type CategorizedPlaylinks = Partial<Record<Category, PlaylinkData[]>>
@@ -79,16 +80,7 @@
79
80
 
80
81
  <div class="heading" use:heading={3}>{t('Where To Stream Online')}</div>
81
82
 
82
- {#if mergedPlaylinks.length}
83
- <div class="disclaimer" data-testid="commission-disclaimer">
84
- {window?.PlayPilotLinkInjections?.config?.playlinks_disclaimer_text || t('Commission Disclaimer')}
85
- <a href="https://playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>
86
- </div>
87
- {:else}
88
- <div class="empty" data-testid="playlinks-empty">
89
- {t('Title Unavailable')}
90
- </div>
91
- {/if}
83
+ <PlaylinksDisclaimer playlinks={mergedPlaylinks} />
92
84
 
93
85
  {#each sortedPlaylinks as [category, playlinks]}
94
86
  {#if category && playlinks.length}
@@ -146,35 +138,7 @@
146
138
  }
147
139
  }
148
140
 
149
- .disclaimer {
150
- margin: theme(playlinks-disclaimer-margin, margin(0.5)) 0;
151
- opacity: theme(playlinks-disclaimer-opacity, 0.65);
152
- color: theme(playlinks-disclaimer-text-color, text-color-alt);
153
- font-size: theme(playlinks-disclaimer-font-size, margin(0.625));
154
- font-style: theme(playlinks-disclaimer-font-style, normal);
155
-
156
- a {
157
- color: inherit;
158
- font-size: inherit;
159
- font-style: inherit;
160
-
161
- &:hover {
162
- color: theme(playlinks-disclaimer-text-color-hover, text-color);
163
- }
164
- }
165
- }
166
-
167
141
  .show-more {
168
142
  margin-top: margin(0.5);
169
143
  }
170
-
171
- .empty {
172
- padding: margin(0.75);
173
- margin-top: margin(0.5);
174
- background: theme(playlink-background, lighter);
175
- box-shadow: theme(playlink-shadow, shadow);
176
- border-radius: theme(playlink-border-radius, border-radius);
177
- white-space: initial;
178
- line-height: 1.35;
179
- }
180
144
  </style>
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import { t } from '$lib/localization'
3
+ import type { PlaylinkData } from '$lib/types/playlink'
4
+
5
+ interface Props {
6
+ playlinks: PlaylinkData[]
7
+ }
8
+
9
+ const { playlinks }: Props = $props()
10
+ </script>
11
+
12
+ {#if playlinks.length}
13
+ <div class="disclaimer" data-testid="commission-disclaimer">
14
+ {window?.PlayPilotLinkInjections?.config?.playlinks_disclaimer_text || t('Commission Disclaimer')}
15
+ <a href="https://playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>
16
+ </div>
17
+ {:else}
18
+ <div class="empty" data-testid="playlinks-empty">
19
+ {t('Title Unavailable')}
20
+ </div>
21
+ {/if}
22
+
23
+ <style lang="scss">
24
+ .disclaimer {
25
+ margin: theme(playlinks-disclaimer-margin, margin(0.5)) 0;
26
+ opacity: theme(playlinks-disclaimer-opacity, 0.65);
27
+ color: theme(playlinks-disclaimer-text-color, text-color-alt);
28
+ font-size: theme(playlinks-disclaimer-font-size, margin(0.625));
29
+ font-style: theme(playlinks-disclaimer-font-style, normal);
30
+
31
+ a {
32
+ color: inherit;
33
+ font-size: inherit;
34
+ font-style: inherit;
35
+
36
+ &:hover {
37
+ color: theme(playlinks-disclaimer-text-color-hover, text-color);
38
+ }
39
+ }
40
+ }
41
+
42
+ .empty {
43
+ padding: margin(0.75);
44
+ margin-top: margin(0.5);
45
+ background: theme(playlink-background, lighter);
46
+ box-shadow: theme(playlink-shadow, shadow);
47
+ border-radius: theme(playlink-border-radius, border-radius);
48
+ white-space: initial;
49
+ line-height: 1.35;
50
+ }
51
+ </style>