@playpilot/tpi 5.33.0-beta.explore.6 → 5.33.0-beta.in-text.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.
Files changed (50) hide show
  1. package/dist/link-injections.js +10 -10
  2. package/package.json +1 -1
  3. package/src/lib/api/ads.ts +1 -1
  4. package/src/lib/api/titles.ts +1 -13
  5. package/src/lib/color.ts +19 -0
  6. package/src/lib/data/translations.ts +0 -5
  7. package/src/lib/enums/SplitTest.ts +5 -0
  8. package/src/lib/fakeData.ts +0 -1
  9. package/src/lib/injection.ts +41 -0
  10. package/src/lib/modal.ts +6 -7
  11. package/src/lib/scss/global.scss +41 -0
  12. package/src/lib/types/config.d.ts +0 -12
  13. package/src/lib/types/title.d.ts +1 -4
  14. package/src/routes/+page.svelte +9 -8
  15. package/src/routes/components/Ads/TopScroll.svelte +18 -4
  16. package/src/routes/components/Debugger.svelte +8 -0
  17. package/src/routes/components/Icons/IconClose.svelte +1 -9
  18. package/src/routes/components/ListTitle.svelte +8 -7
  19. package/src/routes/components/Modal.svelte +24 -6
  20. package/src/routes/components/Share.svelte +23 -5
  21. package/src/routes/components/Title.svelte +22 -22
  22. package/src/routes/components/TitleModal.svelte +1 -4
  23. package/src/routes/elements/+page.svelte +2 -39
  24. package/src/tests/lib/api/ads.test.js +1 -0
  25. package/src/tests/routes/components/Share.test.js +12 -12
  26. package/src/tests/routes/components/Title.test.js +0 -13
  27. package/src/lib/explore.ts +0 -59
  28. package/src/lib/images/titles-list.webp +0 -0
  29. package/src/lib/trailer.ts +0 -22
  30. package/src/lib/types/api.d.ts +0 -6
  31. package/src/routes/components/Button.svelte +0 -73
  32. package/src/routes/components/Explore/Explore.svelte +0 -191
  33. package/src/routes/components/Explore/ExploreCallToAction.svelte +0 -58
  34. package/src/routes/components/Explore/ExploreModal.svelte +0 -15
  35. package/src/routes/components/Explore/Filter.svelte +0 -3
  36. package/src/routes/components/Explore/Search.svelte +0 -56
  37. package/src/routes/components/Icons/IconPlay.svelte +0 -3
  38. package/src/routes/components/Icons/IconSearch.svelte +0 -3
  39. package/src/routes/components/ListTitleSkeleton.svelte +0 -42
  40. package/src/routes/components/Trailer.svelte +0 -18
  41. package/src/routes/components/YouTubeEmbedOverlay.svelte +0 -96
  42. package/src/routes/explore/+page.svelte +0 -61
  43. package/src/tests/lib/api/titles.test.js +0 -55
  44. package/src/tests/lib/explore.test.js +0 -139
  45. package/src/tests/lib/trailer.test.js +0 -56
  46. package/src/tests/routes/components/Button.test.js +0 -28
  47. package/src/tests/routes/components/Explore/Explore.test.js +0 -133
  48. package/src/tests/routes/components/Explore/Search.test.js +0 -26
  49. package/src/tests/routes/components/Trailer.test.js +0 -20
  50. package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +0 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.33.0-beta.explore.6",
3
+ "version": "5.33.0-beta.in-text.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -14,7 +14,7 @@ export async function fetchAds(): Promise<Campaign[]> {
14
14
  if (!apiToken) throw new Error('No token was provided')
15
15
 
16
16
  try {
17
- const response = await api<Campaign[]>(`/ads/browse/?region=nl&api-token=${apiToken}`)
17
+ const response = await api<Campaign[]>(`/ads/browse/?api-token=${apiToken}`)
18
18
 
19
19
  return response
20
20
  } catch (error: any) {
@@ -1,21 +1,9 @@
1
1
  import { getApiToken } from '$lib/token'
2
- import type { APIPaginatedResult } from '$lib/types/api'
3
2
  import type { TitleData } from '../types/title'
4
3
  import { api } from './api'
5
4
 
6
- export async function fetchTitles(params: Record<string, string | number> = {}): Promise<APIPaginatedResult<TitleData>> {
7
- const apiToken = getApiToken()
8
-
9
- if (!apiToken) throw new Error('No token was provided')
10
-
11
- const paramsAsString = Object.entries(params).map(([key, value]) => `${key}=${value}`).join('&')
12
- const response = await api<APIPaginatedResult<TitleData>>(`/titles/browse?api-token=${apiToken}&` + paramsAsString)
13
-
14
- return response
15
- }
16
-
17
5
  export async function fetchSimilarTitles(title: TitleData): Promise<TitleData[]> {
18
- const response = await fetchTitles({ related_to_sid: title.sid })
6
+ const response = await api<{ results: TitleData[] }>(`/titles/browse?api-token=${getApiToken()}&related_to_sid=${title.sid}`)
19
7
 
20
8
  return response.results
21
9
  }
@@ -0,0 +1,19 @@
1
+ /** Adjust the lightness of an RGB color. */
2
+ export function colorLuminance(input: string, luminosity: number): string {
3
+ const match = input
4
+ .replace(/\s+/g, '')
5
+ .match(/^rgb\((\d+),(\d+),(\d+)\)$/i)
6
+
7
+ if (!match) return ''
8
+
9
+ const [, r, g, b] = match
10
+
11
+ const adjust = (v: string) => {
12
+ const color = parseInt(v, 10)
13
+ return Math.round(Math.min(Math.max(0, color + color * luminosity), 255))
14
+ }
15
+
16
+ return `rgb(${adjust(r)}, ${adjust(g)}, ${adjust(b)})`
17
+ }
18
+
19
+
@@ -126,11 +126,6 @@ export const translations = {
126
126
  [Language.Swedish]: 'Liknande filmer och serier',
127
127
  [Language.Danish]: 'Lignende film og serier',
128
128
  },
129
- 'Watch Trailer': {
130
- [Language.English]: 'Watch trailer',
131
- [Language.Swedish]: 'Se trailer',
132
- [Language.Danish]: 'Se trailer',
133
- },
134
129
 
135
130
  // Genres
136
131
  'All': {
@@ -9,4 +9,9 @@ export const SplitTest = {
9
9
  numberOfVariants: 2,
10
10
  variantNames: ['Image', 'Label'] as string[],
11
11
  },
12
+ InTextEngagement: {
13
+ key: 'in-text-engagement',
14
+ numberOfVariants: 4,
15
+ variantNames: ['Default', 'Clapper Icon', 'Play Icon', 'Animated Link'] as string[]
16
+ }
12
17
  } as const
@@ -38,7 +38,6 @@ export const title: TitleData = {
38
38
  standing_poster: 'https://img.playpilot.tech/6239ee86a58f11efb0b50a58a9feac02/src/img?optimizer=image&class=2by3x18',
39
39
  title: 'Dune: Prophecy',
40
40
  original_title: 'Dune: Prophecy',
41
- embeddable_url: null,
42
41
  }
43
42
 
44
43
  export const linkInjections: LinkInjection[] = [{
@@ -10,10 +10,15 @@ import { getNumberOfOccurrencesInArray } from './array'
10
10
  import { mobileBreakpoint } from './constants'
11
11
  import { destroyAllModals, openModal } from './modal'
12
12
  import InTextDisclaimer from '../routes/components/InTextDisclaimer.svelte'
13
+ import { getSplitTestVariantName, trackSplitTestAction } from './splitTest'
14
+ import { SplitTest } from './enums/SplitTest'
15
+ import { colorLuminance } from './color'
13
16
 
14
17
  export const keyDataAttribute = 'data-playpilot-injection-key'
15
18
  export const keySelector = `[${keyDataAttribute}]`
16
19
 
20
+ const linksIntersectionObserver = typeof window !== 'undefined' ? new IntersectionObserver(animateLink) : null
21
+
17
22
  let currentlyHoveredInjection: EventTarget | null = null
18
23
  let activePopoverInsertedComponent: object | null = null
19
24
  let afterArticlePlaylinkInsertedComponent: object | null = null
@@ -306,6 +311,19 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
306
311
  linkElement.target = '_blank'
307
312
  linkElement.rel = 'noopener nofollow noreferrer'
308
313
 
314
+ // Part of a split test, insert an absolute positioned icon in the top right of links.
315
+ if (getSplitTestVariantName(SplitTest.InTextEngagement) === 'Clapper Icon' || getSplitTestVariantName(SplitTest.InTextEngagement) === 'Play Icon') {
316
+ const iconElement = document.createElement('span')
317
+ iconElement.classList.add('playpilot-injection-info-icon')
318
+ iconElement.dataset.playpilotElement = 'true'
319
+
320
+ const playIcon = '<svg width="8" height="8" viewBox="0 0 8 8" fill="none"><path d="M7.66011 4.53958C8.1133 4.31131 8.1133 3.68869 7.6601 3.46042L0.930122 0.0706345C0.507292 -0.142339 8.99396e-08 0.151949 8.42451e-08 0.610212L0 7.38979C-5.69452e-09 7.84805 0.507291 8.14234 0.930122 7.92937L7.66011 4.53958Z" fill="currentColor"/></svg>'
321
+ const clapperIcon = '<svg width="10" height="12" viewBox="0 0 13 16" fill="none"><rect x="0.950195" y="6.56641" width="12.0498" height="8.80563" rx="1" fill="currentColor" /><rect y="5.92969" width="12" height="2.4" rx="0.5" transform="rotate(-29.6116 0 5.92969)" fill="currentColor" /><path d="M8.82314 11.4484C9.02159 11.3443 9.02159 11.0602 8.82314 10.956L5.87605 9.40918C5.69089 9.312 5.46875 9.44629 5.46875 9.6554L5.46875 12.749C5.46875 12.9582 5.69089 13.0924 5.87605 12.9953L8.82314 11.4484Z" fill="white" /></svg>'
322
+
323
+ iconElement.innerHTML = getSplitTestVariantName(SplitTest.InTextEngagement) === 'Clapper Icon' ? clapperIcon : playIcon
324
+ linkElement.insertAdjacentElement('beforeend', iconElement)
325
+ }
326
+
309
327
  injectionElement.insertAdjacentElement('beforeend', linkElement)
310
328
 
311
329
  return { injectionElement, linkElement }
@@ -428,6 +446,8 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
428
446
 
429
447
  event.preventDefault()
430
448
 
449
+ trackSplitTestAction(SplitTest.InTextEngagement, 'click')
450
+
431
451
  playFallbackViewTransition(() => {
432
452
  destroyLinkPopover(false)
433
453
  openModal({ event, injection, data: injection.title_details })
@@ -462,10 +482,28 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
462
482
  injectionElement.addEventListener('mouseleave', () => {
463
483
  currentlyHoveredInjection = null
464
484
  })
485
+
486
+ linksIntersectionObserver!.observe(injectionElement)
465
487
  })
466
488
  }
467
489
 
490
+ export function animateLink(entries: IntersectionObserverEntry[]): void {
491
+ if (getSplitTestVariantName(SplitTest.InTextEngagement) !== 'Animated Link') return
468
492
 
493
+ entries.forEach(entry => {
494
+ if (!entry.isIntersecting) return
495
+
496
+ const linkElement = entry.target.querySelector('a')
497
+ if (!linkElement) return
498
+
499
+ const linkColor = window.getComputedStyle(linkElement).color
500
+
501
+ ;(entry.target as HTMLElement).style = `--animation-color: ${colorLuminance(linkColor, -0.5)}`
502
+ entry.target.classList.add('animate-injection')
503
+
504
+ linksIntersectionObserver!.unobserve(entry.target)
505
+ })
506
+ }
469
507
 
470
508
  /**
471
509
  * When a link is hovered, it is shown as a popover. The component is mounted when a mouse enters the link,
@@ -590,6 +628,9 @@ export function clearLinkInjection(key: string): void {
590
628
  const element: HTMLAnchorElement | null = document.querySelector(`[${keyDataAttribute}="${key}"]`)
591
629
  if (!element) return
592
630
 
631
+ const playpilotElements = element.querySelectorAll('[data-playpilot-element]')
632
+ playpilotElements.forEach(element => element.remove())
633
+
593
634
  const linkContent = element.querySelector('a')?.innerHTML
594
635
  element.outerHTML = linkContent || ''
595
636
  }
package/src/lib/modal.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import { mount, unmount } from "svelte"
2
2
  import { isHoldingSpecialKey } from "./event"
3
+ import TitleModal from "../routes/components/TitleModal.svelte"
3
4
  import type { LinkInjection } from "./types/injection"
5
+ import ParticipantModal from "../routes/components/ParticipantModal.svelte"
4
6
  import type { TitleData } from "./types/title"
5
7
  import type { ParticipantData } from "./types/participant"
6
8
  import { mobileBreakpoint } from "./constants"
7
9
  import { getPlayPilotWrapperElement } from "./injection"
8
- import TitleModal from "../routes/components/TitleModal.svelte"
9
- import ParticipantModal from "../routes/components/ParticipantModal.svelte"
10
- import ExploreModal from "../routes/components/Explore/ExploreModal.svelte"
11
10
 
12
- type ModalType = 'title' | 'participant' | 'explore'
11
+ type ModalType = 'title' | 'participant'
13
12
 
14
13
  type Modal = {
15
14
  injection?: LinkInjection | null
@@ -47,9 +46,9 @@ export function openModal(
47
46
  }
48
47
 
49
48
  function getModalComponentByType({ type = 'title', target, data, props = {} }: { type: ModalType, target: Element, data: TitleData | ParticipantData | null, props?: Record<string, any> }) {
50
- if (type === 'participant') return mount(ParticipantModal, { target, props: { participant: data as ParticipantData, ...props } })
51
- if (type === 'explore') return mount(ExploreModal, { target, props: { ...props } })
52
- return mount(TitleModal, { target, props: { title: data as TitleData, ...props } })
49
+ return type === 'title' ?
50
+ mount(TitleModal, { target, props: { title: data as TitleData, ...props } }) :
51
+ mount(ParticipantModal, { target, props: { participant: data as ParticipantData, ...props } })
53
52
  }
54
53
 
55
54
  function addModalToList({ type = 'title', injection = null, data, scrollPosition = 0, component }: Modal) {
@@ -7,8 +7,47 @@
7
7
  }
8
8
  }
9
9
 
10
+ @keyframes animate-injection {
11
+ 0% {
12
+ background-position: 0% 0%;
13
+ }
14
+
15
+ 100% {
16
+ background-position: -300% 0%;
17
+ }
18
+ }
19
+
10
20
  [data-playpilot-injection-key] {
11
21
  position: relative;
22
+
23
+ &.animate-injection a {
24
+ background: linear-gradient(to right, currentColor 50%, var(--animation-color), currentcolor);
25
+ background-clip: text;
26
+ -webkit-background-clip: text;
27
+ -webkit-text-fill-color: transparent;
28
+ background-size: 200% auto;
29
+ animation: animate-injection 750ms 500ms ease-in-out forwards;
30
+ }
31
+ }
32
+
33
+ .playpilot-injection-info-icon {
34
+ position: relative;
35
+ display: inline-block;
36
+ height: 1em;
37
+
38
+ svg {
39
+ display: inline-flex;
40
+ justify-content: center;
41
+ align-items: center;
42
+ position: absolute;
43
+ top: -0.5em;
44
+ right: -0.25em;
45
+ font-size: 0.65em;
46
+ }
47
+
48
+ h1 &, h2 &, h3 &, h4 &, h5 &, h6 & {
49
+ display: none
50
+ }
12
51
  }
13
52
 
14
53
  .playpilot-injection-highlight {
@@ -21,7 +60,9 @@
21
60
  .playpilot-styled-scrollbar {
22
61
  scrollbar-color: var(--playpilot-content-light) var(--playpilot-lighter);
23
62
  scrollbar-width: thin;
63
+ }
24
64
 
65
+ .playpilot-styled-scrollbed {
25
66
  &::-webkit-scrollbar {
26
67
  width: margin(0.75);
27
68
  }
@@ -39,16 +39,4 @@ export type ConfigResponse = {
39
39
  in_text_disclaimer_text?: string
40
40
  in_text_disclaimer_selector?: string
41
41
  in_text_disclaimer_insert_position?: InsertPosition
42
-
43
- /**
44
- * These options are all relevant for the Explore component, which can be inserted as a widget on any page or as a modal.
45
- * `explore_navigation_selector` is used to select the navigation element that should be copied and inserted _after_.
46
- * `explore_navigation_label` will end up being the label of the navigation item, defaults to `Streaming Guide`.
47
- * `explore_navigation_path` is the path that the navigation item will lead to, this will be set up by the third party.
48
- * `explore_navigation_insert_position` is used to determine if the navigation item should be inserted before or after the given selector.
49
- */
50
- explore_navigation_selector?: string
51
- explore_navigation_label?: string
52
- explore_navigation_path?: string
53
- explore_navigation_insert_position?: InsertPosition
54
42
  }
@@ -1,8 +1,6 @@
1
1
  import type { ParticipantData } from './participant'
2
2
  import type { PlaylinkData } from './playlink'
3
3
 
4
- export type ContentType = 'movie' | 'series'
5
-
6
4
  export type TitleData = {
7
5
  sid: string
8
6
  slug: string
@@ -11,7 +9,7 @@ export type TitleData = {
11
9
  genres: string[]
12
10
  year: number
13
11
  imdb_score: number
14
- type: ContentType
12
+ type: 'movie' | 'series'
15
13
  providers: PlaylinkData[]
16
14
  description: string | null
17
15
  small_poster: string
@@ -19,7 +17,6 @@ export type TitleData = {
19
17
  standing_poster: string
20
18
  title: string
21
19
  original_title: string
22
- embeddable_url: string | null
23
20
  length?: number
24
21
  blurb?: string
25
22
  participants?: ParticipantData[]
@@ -8,8 +8,9 @@
8
8
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
9
9
  import { fetchAds } from '$lib/api/ads'
10
10
  import { fetchConfig } from '$lib/api/config'
11
- import { insertExplore, insertExploreIntoNavigation } from '$lib/explore'
12
11
  import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/api/auth'
12
+ import { trackSplitTestView } from '$lib/splitTest'
13
+ import { SplitTest } from '$lib/enums/SplitTest'
13
14
  import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
14
15
  import Editor from './components/Editorial/Editor.svelte'
15
16
  import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
@@ -43,8 +44,6 @@
43
44
  if (isEditorialMode && !loading) rerender()
44
45
  })
45
46
 
46
- insertExplore()
47
-
48
47
  onDestroy(clearLinkInjections)
49
48
 
50
49
  // This function is called when a user has properly consented via tcfapi or if no consent is required.
@@ -58,7 +57,11 @@
58
57
  fireQueuedTrackingEvents()
59
58
  track(TrackingEvent.ArticlePageView)
60
59
 
61
- if (aiInjections.length || manualInjections.length) window.PlayPilotLinkInjections.ads = await fetchAds()
60
+ if (!aiInjections.length && !manualInjections.length) return
61
+
62
+ window.PlayPilotLinkInjections.ads = await fetchAds()
63
+
64
+ trackSplitTestView(SplitTest.InTextEngagement)
62
65
  }
63
66
 
64
67
  async function initialize(): Promise<void> {
@@ -77,7 +80,6 @@
77
80
  if (isUrlExcluded) return
78
81
 
79
82
  if (config?.custom_style) insertCustomStyle(config.custom_style || '')
80
- if (config?.explore_navigation_selector) insertExploreIntoNavigation()
81
83
 
82
84
  setElements(config?.html_selector || '', config?.exclude_elements_selector || '')
83
85
  } catch(error) {
@@ -230,7 +232,7 @@
230
232
  </svelte:boundary>
231
233
  {/if}
232
234
 
233
- <Debugger />
235
+ <Debugger onrerender={rerender} />
234
236
  </div>
235
237
 
236
238
  {#if response?.pixels?.length}
@@ -246,8 +248,7 @@
246
248
  <style lang="scss">
247
249
  @import url('$lib/scss/global.scss');
248
250
 
249
- .playpilot-link-injections,
250
- :global([data-playpilot-explore]) {
251
+ .playpilot-link-injections {
251
252
  :global(*) {
252
253
  box-sizing: border-box;
253
254
  }
@@ -4,7 +4,7 @@
4
4
  import { SplitTest } from '$lib/enums/SplitTest'
5
5
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
6
6
  import { imageFromUUID } from '$lib/image'
7
- import { trackSplitTestView } from '$lib/splitTest'
7
+ import { isInSplitTestVariant, trackSplitTestView } from '$lib/splitTest'
8
8
  import { track } from '$lib/tracking'
9
9
  import type { Campaign } from '$lib/types/campaign'
10
10
  import Disclaimer from './Disclaimer.svelte'
@@ -22,6 +22,7 @@
22
22
  const { format, header, header_logo: logo, image_uuid: backgroundImageUUID } = $derived(content)
23
23
  const { header: buttonLabel, url: href } = $derived(cta)
24
24
 
25
+ const inline = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
25
26
  const simple = $derived(format === 'large')
26
27
 
27
28
  const backgroundImage = $derived(imageFromUUID(backgroundImageUUID, ImageDimensions.TopScrollBackground))
@@ -42,6 +43,7 @@
42
43
  target="_blank"
43
44
  class="top-scroll"
44
45
  class:simple
46
+ class:inline
45
47
  tabindex="-1"
46
48
  rel="sponsored"
47
49
  style:--width="{clientWidth}px">
@@ -79,13 +81,17 @@
79
81
  position: relative;
80
82
  display: block;
81
83
  width: 100%;
82
- border-radius: $border-radius-size $border-radius-size 0 0;
84
+ border-radius: $border-radius-size;
83
85
  background: black;
84
86
  color: theme(top-scroll-text-color, white);
85
87
  font-family: theme(top-scroll-font-family, font-family);
86
88
  font-size: theme(top-scroll-font-size, font-size-base);
87
89
  text-decoration: none;
88
90
  line-height: 1.35;
91
+
92
+ &.inline {
93
+ border-radius: $border-radius-size $border-radius-size 0 0;
94
+ }
89
95
  }
90
96
 
91
97
  .content {
@@ -146,7 +152,7 @@
146
152
  right: 0;
147
153
  bottom: 0;
148
154
  left: 0;
149
- border-radius: $border-radius-size $border-radius-size 0 0;
155
+ border-radius: $border-radius-size;
150
156
  background-image: var(--background);
151
157
  background-position: center;
152
158
  background-size: cover;
@@ -155,6 +161,10 @@
155
161
  .top-scroll:hover & {
156
162
  filter: brightness(1.15);
157
163
  }
164
+
165
+ .inline & {
166
+ border-radius: $border-radius-size $border-radius-size 0 0;
167
+ }
158
168
  }
159
169
 
160
170
  .content-image {
@@ -162,10 +172,14 @@
162
172
  max-width: 100%;
163
173
  height: auto;
164
174
  background: black;
165
- border-radius: $border-radius-size $border-radius-size 0 0;
175
+ border-radius: $border-radius-size;
166
176
 
167
177
  .top-scroll:hover & {
168
178
  filter: brightness(1.15);
169
179
  }
180
+
181
+ .inline & {
182
+ border-radius: $border-radius-size $border-radius-size 0 0;
183
+ }
170
184
  }
171
185
  </style>
@@ -3,6 +3,12 @@
3
3
  import { getFullUrlPath } from '$lib/url'
4
4
  import { onDestroy } from 'svelte'
5
5
 
6
+ interface Props {
7
+ onrerender: () => void
8
+ }
9
+
10
+ const { onrerender }: Props = $props()
11
+
6
12
  const secrets = ['tpidebug', 'debugtpi']
7
13
  const lastInputs: string[] = []
8
14
  const isUsingBetaScript = !!document.querySelector('script[src*="scripts.playpilot.com/link-injection@next"]')
@@ -151,6 +157,8 @@
151
157
 
152
158
  <hr />
153
159
 
160
+ <button onclick={onrerender}>Re-inject</button>
161
+
154
162
  {#if isUsingBetaScript}
155
163
  <small>You are using the beta version of TPI</small>
156
164
  {:else}
@@ -1,11 +1,3 @@
1
- <script lang="ts">
2
- interface Props {
3
- size?: number
4
- }
5
-
6
- const { size = 16 }: Props = $props()
7
- </script>
8
-
9
- <svg width={size} height={size} viewBox="0 0 16 16" fill="none">
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
10
2
  <path d="M14 2L2 14M2 2L14 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
11
3
  </svg>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { titleUrl } from '$lib/routes'
2
3
  import { SplitTest } from '$lib/enums/SplitTest'
3
4
  import { t } from '$lib/localization'
4
5
  import { mergePlaylinks } from '$lib/playlink'
@@ -28,7 +29,7 @@
28
29
  }
29
30
  </script>
30
31
 
31
- <button class="title" {onclick} data-testid="title">
32
+ <a class="title" href={titleUrl(title)} {onclick} data-testid="title">
32
33
  <div class="poster">
33
34
  <TitlePoster {title} width={30} height={43} lazy />
34
35
  </div>
@@ -80,7 +81,7 @@
80
81
  <div class="action">
81
82
  <IconArrow />
82
83
  </div>
83
- </button>
84
+ </a>
84
85
 
85
86
  <style lang="scss">
86
87
  .title {
@@ -89,18 +90,18 @@
89
90
  align-items: center;
90
91
  width: 100%;
91
92
  background: theme(list-item-background, lighter);
92
- padding: theme(list-item-padding, margin(0.5));
93
+ padding: margin(0.5);
93
94
  border: 0;
94
95
  border-radius: theme(list-item-border-radius, border-radius);
95
96
  text-decoration: none;
96
97
  font-style: normal !important;
97
- font-family: theme(font-family);
98
- cursor: pointer;
99
98
 
100
99
  &:hover {
101
100
  filter: brightness(theme(list-item-hover-filter-brightness, hover-filter-brightness));
102
- background: theme(list-item-hover-background, lighter);
103
- box-shadow: theme(list-item-hover-shadow, none);
101
+ }
102
+
103
+ &:last-child {
104
+ border-bottom: 0;
104
105
  }
105
106
  }
106
107
 
@@ -6,6 +6,8 @@
6
6
  import SwipeHandle from './SwipeHandle.svelte'
7
7
  import { onMount, setContext, type Snippet } from 'svelte'
8
8
  import { prefersReducedMotion } from 'svelte/motion'
9
+ import { isInSplitTestVariant } from '$lib/splitTest'
10
+ import { SplitTest } from '$lib/enums/SplitTest'
9
11
  import { mobileBreakpoint } from '$lib/constants'
10
12
  import { destroyAllModals, getPreviousModal, goBackToPreviousModal } from '$lib/modal'
11
13
  import { focustrap } from '$lib/actions/focustrap'
@@ -32,6 +34,7 @@
32
34
  onclose = () => null,
33
35
  }: Props = $props()
34
36
 
37
+ const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
35
38
  const historyHash = '#playpilot'
36
39
 
37
40
  let windowWidth = $state(0)
@@ -92,15 +95,15 @@
92
95
  onhashchange={close}
93
96
  bind:innerWidth={windowWidth} />
94
97
 
95
- <div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} use:focustrap class:has-bubble={!!bubble} class:has-prepend={!!prepend}>
98
+ <div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} use:focustrap class:has-bubble={!!bubble && inlineBubble} class:has-prepend={!!prepend}>
96
99
  {#if prepend}
97
- <div class="prepend" transition:scaleOrFly|global={{ y: -10 }} data-view-transition-new="playpilot-title-extra">
100
+ <div class="prepend" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
98
101
  {@render prepend()}
99
102
  </div>
100
103
  {/if}
101
104
 
102
105
  {#if bubble}
103
- <div class="bubble" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
106
+ <div class="bubble" class:inline={inlineBubble} transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
104
107
  {@render bubble()}
105
108
  </div>
106
109
  {/if}
@@ -282,13 +285,28 @@
282
285
  .bubble {
283
286
  z-index: 1;
284
287
  position: relative;
285
- width: 100%;
288
+ width: calc(100% - margin(1));
286
289
  max-width: $max-width;
287
- margin: auto 0 0;
290
+ margin: margin(0.5);
288
291
 
289
292
  @include desktop() {
290
293
  width: 100%;
291
- margin-top: 0;
294
+ margin: 0 0 margin(0.5);
295
+ }
296
+
297
+ &.inline {
298
+ width: 100%;
299
+ margin: auto 0 0;
300
+
301
+ @include desktop() {
302
+ margin-top: 0;
303
+ }
304
+ }
305
+
306
+ .prepend + & {
307
+ &:not(.inline) {
308
+ margin-top: 0;
309
+ }
292
310
  }
293
311
  }
294
312
  </style>
@@ -9,7 +9,6 @@
9
9
  import IconShare from './Icons/IconShare.svelte'
10
10
  import IconLink from './Icons/IconLink.svelte'
11
11
  import IconEmail from './Icons/IconEmail.svelte'
12
- import Button from './Button.svelte'
13
12
  import { onMount } from 'svelte'
14
13
 
15
14
  interface Props {
@@ -56,9 +55,9 @@
56
55
  <svelte:window onclick={() => showContextMenu = false} />
57
56
 
58
57
  <div class="share">
59
- <Button onclick={toggle}>
60
- <IconShare /> Share
61
- </Button>
58
+ <button class="button" onclick={toggle} aria-label={t('Share')}>
59
+ <IconShare />
60
+ </button>
62
61
 
63
62
  {#if showContextMenu}
64
63
  <div class="context-menu" transition:scale={{ duration: 50, start: 0.85 }}>
@@ -80,11 +79,30 @@
80
79
  position: relative;
81
80
  }
82
81
 
82
+ .button {
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ cursor: pointer;
87
+ appearance: none;
88
+ border: 0;
89
+ border-radius: margin(3);
90
+ aspect-ratio: 1 / 1;
91
+ background: transparent;
92
+ color: theme(detail-text-color-alt, text-color-alt);
93
+
94
+ &:hover {
95
+ color: theme(detail-text-color, text-color);
96
+ background: theme(share-button-hover-background, content);
97
+ box-shadow: 0 0 0 2px theme(share-button-hover-background, content);
98
+ }
99
+ }
100
+
83
101
  .context-menu {
84
102
  z-index: 10;
85
103
  position: absolute;
86
104
  bottom: calc(100% + margin(0.5));
87
- left: 0;
105
+ right: 0;
88
106
  max-width: margin(15);
89
107
  border-radius: $border-radius;
90
108
  background: theme(detail-background, lighter);