@playpilot/tpi 6.2.2 → 6.3.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": "6.2.2",
3
+ "version": "6.3.0-beta.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -291,6 +291,7 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
291
291
  injectionElement.dataset.playpilotInjectionKey = injection.key
292
292
 
293
293
  const linkElement = document.createElement('a')
294
+ linkElement.dataset.playpilotPosterUrl = injection.title_details?.standing_poster
294
295
  linkElement.innerText = injection.title
295
296
  linkElement.href = injection.playpilot_url
296
297
  linkElement.target = '_blank'
@@ -99,3 +99,34 @@ h1, h2, h3, h4, h5 {
99
99
  ::view-transition-new(playpilot-title-content) {
100
100
  height: 100%;
101
101
  }
102
+
103
+ @keyframes playpilot-poster-fly-in {
104
+ from {
105
+ opacity: 0;
106
+ transform: translateX(-50%) translateY(0.5rem) scale(0.5);
107
+ }
108
+ }
109
+
110
+ @keyframes playpilot-poster-fly-out {
111
+ to {
112
+ opacity: 0;
113
+ transform: translateX(-50%) translateY(0.5rem) scale(0.5);
114
+ }
115
+ }
116
+
117
+ .playpilot-floating-poster {
118
+ position: absolute;
119
+ bottom: calc(100% + 0.5rem);
120
+ left: 50%;
121
+ transform: translateX(-50%);
122
+ width: 40px;
123
+ height: auto;
124
+ border-radius: theme(border-radius-small);
125
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
126
+ animation: playpilot-poster-fly-in 150ms;
127
+ transform-origin: bottom center;
128
+
129
+ &.animating-out {
130
+ animation: playpilot-poster-fly-out 150ms forwards;
131
+ }
132
+ }
@@ -18,6 +18,7 @@
18
18
  import Consent from './components/Consent.svelte'
19
19
  import Debugger from './components/Debugger.svelte'
20
20
  import UserJourney from './components/UserJourney.svelte'
21
+ import PostersScrollReveal from './components/PostersScrollReveal.svelte'
21
22
 
22
23
  let parentElement: HTMLElement | null = $state(null)
23
24
  let elements: HTMLElement[] = $state([])
@@ -233,6 +234,7 @@
233
234
 
234
235
  {#key linkInjections}
235
236
  <UserJourney />
237
+ <PostersScrollReveal />
236
238
  {/key}
237
239
 
238
240
  <Consent onchange={afterConsent} />
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+
4
+ const linksWithPosters = Array.from(document.querySelectorAll<HTMLElement>('[data-playpilot-poster-url]'))
5
+ const linksWithCurrentlyVisiblePosters: Set<HTMLElement> = new Set()
6
+ const targetThreshold = 0.8
7
+ const posterSelector = '[data-playpilot-poster]'
8
+
9
+ let scrollTimeout: ReturnType<typeof setTimeout> | null = null
10
+
11
+ onMount(() => {
12
+ return destroyAllPosters
13
+ })
14
+
15
+ function onscroll(): void {
16
+ setScrollTimeout()
17
+
18
+ const windowHeight = window.innerHeight
19
+ const linksWithinTargetViewport: HTMLElement[] = []
20
+ const targetFromBottom = windowHeight * targetThreshold
21
+ const targetFromTop = windowHeight - targetFromBottom
22
+
23
+ for (const link of linksWithPosters) {
24
+ const { top } = link.getBoundingClientRect()
25
+
26
+ const isInsideTargetViewport = top < targetFromBottom && top > targetFromTop
27
+
28
+ if (!isInsideTargetViewport) continue
29
+
30
+ linksWithinTargetViewport.push(link)
31
+ }
32
+
33
+ for (const link of linksWithCurrentlyVisiblePosters) {
34
+ if (linksWithinTargetViewport.some(l => link === l)) continue
35
+
36
+ destroyPosterForLink(link)
37
+ linksWithCurrentlyVisiblePosters.delete(link)
38
+ }
39
+
40
+ for (const link of linksWithinTargetViewport) {
41
+ if (linksWithCurrentlyVisiblePosters.has(link)) continue
42
+
43
+ createPosterForLink(link)
44
+ linksWithCurrentlyVisiblePosters.add(link)
45
+ }
46
+ }
47
+
48
+ function createPosterForLink(link: HTMLElement): void {
49
+ const posterUrl = link.dataset.playpilotPosterUrl!
50
+
51
+ const element = document.createElement('img')
52
+ element.classList.add('playpilot-floating-poster')
53
+ element.dataset.playpilotPoster = ''
54
+ element.src = posterUrl
55
+ element.alt = ''
56
+
57
+ link.insertAdjacentElement('afterbegin', element)
58
+ }
59
+
60
+ function destroyPosterForLink(link: HTMLElement): void {
61
+ const poster = link.querySelector(posterSelector)
62
+
63
+ if (!poster) return
64
+
65
+ poster.classList.add('animating-out')
66
+
67
+ setTimeout(() => poster.remove(), 500)
68
+ }
69
+
70
+ function destroyAllPosters(): void {
71
+ for (const poster of Array.from(document.querySelectorAll(posterSelector))) {
72
+ poster.remove()
73
+ }
74
+ }
75
+
76
+ function setScrollTimeout() {
77
+ if (scrollTimeout) clearTimeout(scrollTimeout)
78
+
79
+ scrollTimeout = setTimeout(() => {
80
+ for (const link of linksWithCurrentlyVisiblePosters) {
81
+ destroyPosterForLink(link)
82
+ linksWithCurrentlyVisiblePosters.delete(link)
83
+ }
84
+ }, 500)
85
+ }
86
+ </script>
87
+
88
+ <svelte:window {onscroll} />
@@ -0,0 +1,107 @@
1
+ import { render } from '@testing-library/svelte'
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
3
+ import PostersScrollReveal from '../../../routes/components/PostersScrollReveal.svelte'
4
+
5
+ function createPosterLink(top = 0, url = 'poster.jpg') {
6
+ const link = document.createElement('a')
7
+ link.dataset.playpilotPosterUrl = url
8
+
9
+ // @ts-ignore
10
+ link.getBoundingClientRect = () => ({ top })
11
+
12
+ document.body.appendChild(link)
13
+ return link
14
+ }
15
+
16
+ describe('FloatingPoster', () => {
17
+ beforeEach(() => {
18
+ document.body.innerHTML = ''
19
+
20
+ Object.defineProperty(window, 'innerHeight', {
21
+ value: 1000,
22
+ writable: true,
23
+ })
24
+
25
+ vi.useFakeTimers()
26
+ vi.resetAllMocks()
27
+ })
28
+
29
+ it('Should create poster when link is in viewport', async () => {
30
+ createPosterLink(500)
31
+
32
+ render(PostersScrollReveal)
33
+
34
+ window.dispatchEvent(new Event('scroll'))
35
+
36
+ const poster = document.querySelector('[data-playpilot-poster]')
37
+ expect(poster).toBeTruthy()
38
+ expect(poster?.getAttribute('src')).toBe('poster.jpg')
39
+ })
40
+
41
+ it('Should not create poster when link is below in viewport', async () => {
42
+ createPosterLink(2000)
43
+
44
+ render(PostersScrollReveal)
45
+
46
+ window.dispatchEvent(new Event('scroll'))
47
+
48
+ expect(document.querySelector('[data-playpilot-poster]')).not.toBeTruthy()
49
+ })
50
+
51
+ it('Should not create poster when link is above in viewport', async () => {
52
+ createPosterLink(-100)
53
+
54
+ render(PostersScrollReveal)
55
+
56
+ window.dispatchEvent(new Event('scroll'))
57
+
58
+ expect(document.querySelector('[data-playpilot-poster]')).not.toBeTruthy()
59
+ })
60
+
61
+ it('Should not create poster when link is in viewport but before threshold', async () => {
62
+ createPosterLink(50)
63
+ createPosterLink(900)
64
+
65
+ render(PostersScrollReveal)
66
+
67
+ window.dispatchEvent(new Event('scroll'))
68
+
69
+ expect(document.querySelector('[data-playpilot-poster]')).not.toBeTruthy()
70
+ })
71
+
72
+ it('Should create poster for each link in viewport', async () => {
73
+ createPosterLink(10)
74
+ createPosterLink(500)
75
+ createPosterLink(500)
76
+ createPosterLink(550)
77
+ createPosterLink(600)
78
+ createPosterLink(2000)
79
+
80
+ render(PostersScrollReveal)
81
+
82
+ window.dispatchEvent(new Event('scroll'))
83
+
84
+ expect(document.querySelectorAll('[data-playpilot-poster]')).toHaveLength(4)
85
+ })
86
+
87
+ it('Should hide poster after not scrolling for given timeout', async () => {
88
+ createPosterLink(500)
89
+
90
+ render(PostersScrollReveal)
91
+
92
+ window.dispatchEvent(new Event('scroll'))
93
+ expect(document.querySelector('[data-playpilot-poster]')).toBeTruthy()
94
+
95
+ vi.advanceTimersByTime(200)
96
+ expect(document.querySelector('[data-playpilot-poster]')).toBeTruthy()
97
+
98
+ vi.advanceTimersByTime(400)
99
+ expect(document.querySelector('[data-playpilot-poster]')).toBeTruthy()
100
+
101
+ vi.advanceTimersByTime(500)
102
+ expect(document.querySelector('[data-playpilot-poster]')).not.toBeTruthy()
103
+
104
+ window.dispatchEvent(new Event('scroll'))
105
+ expect(document.querySelector('[data-playpilot-poster]')).toBeTruthy()
106
+ })
107
+ })