@playpilot/tpi 5.6.0-beta.video.7 → 5.7.0-beta.display-2

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 (38) hide show
  1. package/dist/link-injections.js +9 -9
  2. package/events.md +8 -2
  3. package/package.json +1 -1
  4. package/src/lib/ads.ts +38 -0
  5. package/src/lib/constants.ts +2 -1
  6. package/src/lib/enums/ImageDimensions.ts +7 -0
  7. package/src/lib/enums/SplitTest.ts +23 -0
  8. package/src/lib/enums/TrackingEvent.ts +3 -0
  9. package/src/lib/image.ts +12 -0
  10. package/src/lib/linkInjection.ts +36 -2
  11. package/src/lib/splitTest.ts +19 -14
  12. package/src/lib/types/campaign.d.ts +45 -0
  13. package/src/lib/types/playlink.d.ts +2 -0
  14. package/src/lib/types/script.d.ts +5 -0
  15. package/src/main.ts +6 -0
  16. package/src/routes/+layout.svelte +4 -0
  17. package/src/routes/+page.svelte +13 -2
  18. package/src/routes/components/Ads/Display.svelte +140 -0
  19. package/src/routes/components/Ads/TopScroll.svelte +240 -0
  20. package/src/routes/components/DragHandle.svelte +105 -0
  21. package/src/routes/components/Modal.svelte +125 -30
  22. package/src/routes/components/Playlink.svelte +185 -0
  23. package/src/routes/components/Playlinks.svelte +34 -168
  24. package/src/routes/components/Popover.svelte +30 -2
  25. package/src/routes/components/Title.svelte +13 -43
  26. package/src/routes/components/TitleModal.svelte +24 -10
  27. package/src/routes/components/TitlePopover.svelte +12 -6
  28. package/src/tests/lib/ads.test.js +55 -0
  29. package/src/tests/lib/splitTest.test.js +97 -18
  30. package/src/tests/routes/+page.test.js +33 -0
  31. package/src/tests/routes/components/Ads/Display.test.js +109 -0
  32. package/src/tests/routes/components/Ads/TopScroll.test.js +104 -0
  33. package/src/tests/routes/components/Playlink.test.js +95 -0
  34. package/src/tests/routes/components/Playlinks.test.js +0 -30
  35. package/src/tests/routes/components/TitleModal.test.js +36 -0
  36. package/src/tests/routes/components/TitlePopover.test.js +20 -0
  37. package/src/routes/components/Ads/FloatingPlaylink.svelte +0 -62
  38. package/src/routes/components/Ads/VideoBackground.svelte +0 -28
@@ -0,0 +1,240 @@
1
+ <script lang="ts">
2
+ import { ImageDimensions } from '$lib/enums/ImageDimensions'
3
+ import { SplitTest } from '$lib/enums/SplitTest'
4
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
5
+ import { imageFromUUID } from '$lib/image'
6
+ import { isInSplitTestVariant, trackSplitTestView } from '$lib/splitTest'
7
+ import { track } from '$lib/tracking'
8
+ import type { Campaign } from '$lib/types/campaign'
9
+ import { fade } from 'svelte/transition'
10
+
11
+ interface Props {
12
+ campaign: Campaign
13
+ }
14
+
15
+ const { campaign }: Props = $props()
16
+
17
+ trackSplitTestView(SplitTest.TopScrollFormat)
18
+
19
+ const { disclaimer, content, cta } = $derived(campaign)
20
+ const { format, header, header_logo: logo, image_uuid: backgroundImageUUID } = $derived(content)
21
+ const { header: buttonLabel, url: href } = $derived(cta)
22
+
23
+ const inline = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
24
+ const simple = $derived(format === 'large')
25
+
26
+ const backgroundImage = $derived(imageFromUUID(backgroundImageUUID, ImageDimensions.TopScrollBackground))
27
+ const contentImage = $derived(imageFromUUID(backgroundImageUUID, ImageDimensions.TopScrollContent))
28
+
29
+ let clientWidth = $state(0)
30
+ let tooltipVisible = $state(false)
31
+
32
+ function trackClick(): void {
33
+ track(TrackingEvent.TopScrollClick, null, { campaign_name: campaign.campaign_name })
34
+ }
35
+
36
+ function toggleTooltip(event: MouseEvent): void {
37
+ event.preventDefault()
38
+ event.stopPropagation()
39
+
40
+ tooltipVisible = !tooltipVisible
41
+ }
42
+ </script>
43
+
44
+ <a
45
+ {href}
46
+ target="_blank"
47
+ class="top-scroll"
48
+ class:simple
49
+ class:inline
50
+ tabindex="-1"
51
+ onclick={trackClick}
52
+ rel="sponsored"
53
+ style:--width="{clientWidth}px"
54
+ bind:clientWidth>
55
+ <div class="content">
56
+ {#if simple}
57
+ <img class="content-image" src={contentImage} alt={header} width="728" height="90" />
58
+ {:else}
59
+ {#if logo}
60
+ <img class="logo" src={logo} alt="" width="50" height="50" />
61
+ {/if}
62
+
63
+ <p class="tagline">{header}</p>
64
+
65
+ {#if buttonLabel}
66
+ <button class="button">{buttonLabel}</button>
67
+ {/if}
68
+ {/if}
69
+ </div>
70
+
71
+ {#if !simple}
72
+ {#if backgroundImageUUID}
73
+ <div class="background" style:--background="url('{backgroundImage}')"></div>
74
+ {/if}
75
+
76
+ {#if disclaimer}
77
+ <button class="tooltip" onclick={toggleTooltip}>
78
+ <div class="disclaimer">i</div>
79
+
80
+ {#if tooltipVisible}
81
+ <div class="tooltip-content" transition:fade={{ duration: 50 }}>
82
+ {disclaimer}
83
+ </div>
84
+ {/if}
85
+ </button>
86
+ {/if}
87
+ {/if}
88
+ </a>
89
+
90
+ <style lang="scss">
91
+ $border-radius-size: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
92
+
93
+ .top-scroll {
94
+ position: relative;
95
+ display: block;
96
+ width: 100%;
97
+ border-radius: $border-radius-size;
98
+ background: black;
99
+ color: var(--playpilot-top-scroll-text-color, white);
100
+ font-family: var(--playpilot-top-scroll-font-family, var(--playpilot-detail-font-family, var(--playpilot-font-family)));
101
+ font-size: var(--playpilot-top-scroll-font-size, var(--playpilot-detail-font-size, 14px));
102
+ text-decoration: none;
103
+ line-height: 1.35;
104
+
105
+ &.inline {
106
+ border-radius: $border-radius-size $border-radius-size 0 0;
107
+ }
108
+ }
109
+
110
+ .content {
111
+ display: flex;
112
+ z-index: 1;
113
+ position: relative;
114
+ align-items: center;
115
+ height: 100%;
116
+ min-height: margin(4.5);
117
+ padding: margin(0.25) margin(0.5);
118
+ gap: margin(0.5);
119
+ color: var(--playpilot-top-scroll-text-color, white);
120
+
121
+ .simple & {
122
+ justify-content: center;
123
+ min-height: 0;
124
+ padding: 0;
125
+ }
126
+ }
127
+
128
+ .logo {
129
+ border-radius: var(--playpilot-top-scroll-logo-border-radius, margin(0.5));
130
+ background: black;
131
+ }
132
+
133
+ .tagline {
134
+ margin: 0;
135
+ text-shadow: var(--playpilot-shadow);
136
+ }
137
+
138
+ .button {
139
+ margin-left: auto;
140
+ margin-right: margin(1);
141
+ padding: margin(0.25) margin(0.5);
142
+ background: var(--playpilot-top-scroll-button-background, white);
143
+ border: 0;
144
+ border-radius: var(--playpilot-top-scroll-button-border-radius, margin(0.25));
145
+ box-shadow: var(--playpilot-shadow);
146
+ color: var(--playpilot-top-scroll-button-text-color, black);
147
+ font-family: inherit;
148
+ font-size: inherit;
149
+ line-height: inherit;
150
+
151
+ &:hover {
152
+ outline: margin(0.25) solid var(--playpilot-top-scroll-button-background, rgba(white, 0.35));
153
+ background: var(--playpilot-top-scroll-button-background, white);
154
+ }
155
+
156
+ &:active {
157
+ background: var(--playpilot-top-scroll-button-background, white);
158
+ }
159
+ }
160
+
161
+ .background {
162
+ z-index: 0;
163
+ position: absolute;
164
+ top: 0;
165
+ right: 0;
166
+ bottom: 0;
167
+ left: 0;
168
+ border-radius: $border-radius-size;
169
+ background-image: var(--background);
170
+ background-position: center;
171
+ background-size: cover;
172
+ opacity: 0.4;
173
+
174
+ .top-scroll:hover & {
175
+ filter: brightness(1.15);
176
+ }
177
+
178
+ .inline & {
179
+ border-radius: $border-radius-size $border-radius-size 0 0;
180
+ }
181
+ }
182
+
183
+ .content-image {
184
+ display: block;
185
+ max-width: 100%;
186
+ height: auto;
187
+ background: black;
188
+ border-radius: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
189
+
190
+ .top-scroll:hover & {
191
+ filter: brightness(1.15);
192
+ }
193
+ }
194
+
195
+ .disclaimer {
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ border: var(--playpilot-top-scroll-disclaimer-border, 2px solid rgba(255, 255, 255, 0.5));
200
+ padding: margin(0.25);
201
+ height: margin(1.25);
202
+ width: margin(1.25);
203
+ border-radius: 99px;
204
+ color: var(--playpilot-top-scroll-disclaimer-text-color, rgba(255, 255, 255, 0.75));
205
+ font-weight: bold;
206
+ line-height: 1;
207
+
208
+ &:hover {
209
+ border-color: var(--playpilot-top-scroll-disclaimer-hover-border-color, white);
210
+ color: var(--playpilot-top-scroll-disclaimer-hover-text-color, white);
211
+ }
212
+ }
213
+
214
+ .tooltip {
215
+ z-index: 2;
216
+ position: absolute;
217
+ right: 0;
218
+ bottom: 0;
219
+ padding: margin(0.25);
220
+ background: transparent;
221
+ border: 0;
222
+ color: var(--playpilot-top-scroll-text-color, white);
223
+ cursor: pointer;
224
+ }
225
+
226
+ .tooltip-content {
227
+ display: block;
228
+ z-index: 20;
229
+ position: absolute;
230
+ right: 100%;
231
+ bottom: margin(0.25);
232
+ width: calc(var(--width) * 0.8);
233
+ padding: margin(0.5);
234
+ border-radius: margin(1);
235
+ background: var(--playpilot-content);
236
+ box-shadow: var(--playpilot-shadow);
237
+ line-height: 1.1;
238
+ text-align: right;
239
+ }
240
+ </style>
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ target?: HTMLElement
4
+ threshold?: number
5
+ isDragging?: boolean
6
+ passedThreshold?: boolean
7
+ onpassed: () => void
8
+ }
9
+
10
+ let {
11
+ target,
12
+ threshold = 100,
13
+ isDragging = $bindable(false),
14
+ passedThreshold = $bindable(false),
15
+ onpassed = () => null,
16
+ }: Props = $props()
17
+
18
+ let movementStartY = $state(0)
19
+ let movementCurrentY = $state(0)
20
+ let handleElement: HTMLElement | null = $state(null)
21
+ let hasBeenDragged = $state(false) // This is used to prevent the element from transitioning into view on mount
22
+
23
+ const difference = $derived(Math.max(movementCurrentY - movementStartY, 0))
24
+
25
+ $effect(() => {
26
+ passedThreshold = difference > threshold
27
+ })
28
+
29
+ export function start(event: TouchEvent): void {
30
+ if (!event) return
31
+ if (!target) return
32
+
33
+ isDragging = true
34
+ hasBeenDragged = true
35
+
36
+ movementStartY = event.touches[0].pageY
37
+ target.style.transitionDuration = '0ms'
38
+ }
39
+
40
+ export function move(event: TouchEvent): void {
41
+ if (!event) return
42
+ if (!target) return
43
+ if (!isDragging) return
44
+
45
+ movementCurrentY = event.touches[0].pageY
46
+
47
+ target.style.transform = `translateY(${difference}px)`
48
+ handleElement!.style.transform = `translateY(${difference}px)`
49
+ }
50
+
51
+ export function end(): void {
52
+ if (!target) return
53
+ if (!isDragging) return
54
+
55
+ isDragging = false
56
+
57
+ if (passedThreshold) {
58
+ onpassed()
59
+ } else {
60
+ target.removeAttribute('style')
61
+ handleElement!.removeAttribute('style')
62
+ }
63
+
64
+ movementStartY = 0
65
+ movementCurrentY = 0
66
+ }
67
+ </script>
68
+
69
+ <svelte:window ontouchmove={move} ontouchend={end} />
70
+
71
+ <div class="handle" class:dragging={isDragging} class:dragged={hasBeenDragged} bind:this={handleElement} ontouchstart={start}></div>
72
+
73
+ <style lang="scss">
74
+ .handle {
75
+ position: relative;
76
+ width: 100%;
77
+ height: var(--playpilot-drag-handle-height, margin(3));
78
+
79
+ &::before {
80
+ content: '';
81
+ display: block;
82
+ position: absolute;
83
+ top: calc(50% - (var(--playpilot-drag-handle-bar-height, margin(0.25)) / 2));
84
+ width: 100%;
85
+ height: var(--playpilot-drag-handle-bar-height, margin(0.25));
86
+ border-radius: margin(1);
87
+ background: var(--playpilot-drag-handle-color, currentColor);
88
+ opacity: 0.65;
89
+ transition: opacity 100ms, scale 100ms;
90
+ }
91
+
92
+ &.dragged {
93
+ transition: transform 200ms;
94
+ }
95
+
96
+ &.dragging {
97
+ transition: none;
98
+
99
+ &::before {
100
+ scale: 1.5 1;
101
+ opacity: 1;
102
+ }
103
+ }
104
+ }
105
+ </style>
@@ -2,17 +2,32 @@
2
2
  import { fade, fly, scale, type TransitionConfig } from 'svelte/transition'
3
3
  import IconClose from './Icons/IconClose.svelte'
4
4
  import RoundButton from './RoundButton.svelte'
5
+ import DragHandle from './DragHandle.svelte'
5
6
  import { onMount, setContext, type Snippet } from 'svelte'
6
7
  import { prefersReducedMotion } from 'svelte/motion'
8
+ import { isInSplitTestVariant } from '$lib/splitTest'
9
+ import { SplitTest } from '$lib/enums/SplitTest'
7
10
 
8
11
  interface Props {
9
12
  children: Snippet
10
- mobileSize?: '' | 'tall' | 'taller'
13
+ bubble?: Snippet | null
14
+ prepend?: Snippet | null
11
15
  onclose?: () => void
12
16
  onscroll?: () => void
13
17
  }
14
18
 
15
- const { children, mobileSize = '', onclose = () => null, onscroll = () => null }: Props = $props()
19
+ const { children, bubble, prepend, onclose = () => null, onscroll = () => null }: Props = $props()
20
+
21
+ const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
22
+
23
+ let windowWidth = $state(0)
24
+ let dialogElement: HTMLElement | null = $state(null)
25
+ let dialogOffset: number = $state(0)
26
+
27
+ const isMobile = $derived(windowWidth < 600)
28
+
29
+ $effect(() => { if (windowWidth) dialogOffset = dialogElement?.offsetTop || 0 })
30
+ $effect(() => { setTimeout(() => dialogOffset = dialogElement?.offsetTop || 0) }) // Set after the dialog has shown to get the proper height
16
31
 
17
32
  setContext('scope', 'modal')
18
33
 
@@ -23,20 +38,36 @@
23
38
  return () => document.body.style.overflowY = baseOverflowStyle || ''
24
39
  })
25
40
 
26
- function scaleOrFly(node: Element): TransitionConfig {
41
+ function scaleOrFly(node: Element, options: { y: number } = { y: 0 }): TransitionConfig {
27
42
  if (prefersReducedMotion.current) return fade(node, { duration: 0 })
28
43
 
29
- const shouldFly = window.innerWidth < 600
30
-
31
- if (shouldFly) return fly(node, { duration: 250, y: window.innerHeight })
44
+ if (isMobile) return fly(node, { duration: 250, ...options })
32
45
  return scale(node, { duration: 150, start: 0.85 })
33
46
  }
34
47
  </script>
35
48
 
36
- <svelte:window on:keydown={({ key }) => { if (key === 'Escape') onclose() }} />
49
+ <svelte:window onkeydown={({ key }) => { if (key === 'Escape') onclose() }} bind:innerWidth={windowWidth} />
50
+
51
+ <div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} class:has-bubble={!!bubble && inlineBubble} class:has-prepend={!!prepend}>
52
+ {#if prepend}
53
+ <div class="prepend" transition:scaleOrFly|global={{ y: -10 }}>
54
+ {@render prepend()}
55
+ </div>
56
+ {/if}
37
57
 
38
- <div class="modal" transition:fade|global={{ duration: 150 }}>
39
- <div class="dialog {mobileSize}" {onscroll} role="dialog" aria-labelledby="title" transition:scaleOrFly|global data-view-transition-new>
58
+ {#if bubble}
59
+ <div class="bubble" class:inline={inlineBubble} transition:scaleOrFly|global={{ y: inlineBubble ? window.innerHeight : -10 }}>
60
+ {@render bubble()}
61
+ </div>
62
+ {/if}
63
+
64
+ {#if isMobile}
65
+ <div class="drag-handle" transition:scaleOrFly|global>
66
+ <DragHandle target={dialogElement!} onpassed={() => onclose()} />
67
+ </div>
68
+ {/if}
69
+
70
+ <div class="dialog" {onscroll} bind:this={dialogElement} role="dialog" aria-labelledby="title" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new>
40
71
  <div class="close">
41
72
  <RoundButton onclick={() => onclose()}>
42
73
  <IconClose />
@@ -52,56 +83,63 @@
52
83
  </div>
53
84
 
54
85
  <style lang="scss">
86
+ $max-width: 600px;
87
+
55
88
  .modal {
89
+ --playpilot-detail-background-border-radius: var(--playpilot-detail-border-radius, #{margin(1)} #{margin(1)} 0 0);
56
90
  z-index: 2147483647; // As high as she goes
57
91
  box-sizing: border-box;
58
92
  position: fixed;
59
93
  display: flex;
60
- justify-content: center;
61
- align-items: flex-start;
94
+ flex-direction: column;
95
+ justify-content: flex-start;
96
+ align-items: center;
97
+ overflow: auto;
62
98
  top: 0;
63
99
  left: 0;
64
100
  width: 100%;
65
101
  height: 100%;
66
102
  background: var(--playpilot-detail-backdrop, rgba(0, 0, 0, 0.65));
67
103
 
68
- @media (min-width: 600px) {
69
- padding: margin(2);
104
+ @media (min-width: $max-width) {
105
+ padding: margin(2) 0;
70
106
  }
71
- }
72
107
 
108
+ &.has-bubble {
109
+ --playpilot-detail-background-border-radius: 0;
110
+ }
111
+ }
73
112
 
74
113
  .dialog {
75
114
  z-index: 1;
76
115
  position: relative;
77
116
  width: 100%;
78
- max-width: 600px;
79
- max-height: 80dvh;
117
+ max-width: $max-width;
118
+ max-height: 80vh;
80
119
  overflow: auto;
81
120
  margin-top: auto;
82
121
  border-radius: var(--playpilot-detail-border-radius, margin(1) margin(1) 0 0);
83
122
  background: var(--playpilot-detail-background, var(--playpilot-light));
123
+ transition: transform 200ms;
84
124
 
85
- &.tall {
86
- @media (max-width: 599px) {
87
- max-height: 90dvh;
88
- }
125
+ @media (min-width: $max-width) {
126
+ margin-top: 0;
127
+ border-radius: var(--playpilot-detail-border-radius, margin(1));
128
+ overflow: visible;
129
+ max-height: unset;
89
130
  }
90
131
 
91
- &.taller {
92
- @media (max-width: 599px) {
93
- max-height: 95dvh;
94
- }
132
+ .has-prepend & {
133
+ margin-top: 0;
95
134
  }
96
135
 
97
- @media (min-width: 600px) {
136
+ .has-bubble & {
137
+ border-top-left-radius: 0;
138
+ border-top-right-radius: 0;
98
139
  margin-top: 0;
99
- border-radius: var(--playpilot-detail-border-radius, margin(1));
100
- max-height: 100%;
101
140
  }
102
141
  }
103
142
 
104
-
105
143
  .backdrop {
106
144
  z-index: 0;
107
145
  position: absolute;
@@ -111,18 +149,75 @@
111
149
  left: 0;
112
150
  }
113
151
 
152
+ .drag-handle {
153
+ z-index: 5;
154
+ position: absolute;
155
+ top: var(--dialog-offset);
156
+ left: 50%;
157
+ transform: translateY(margin(-0.5)) translateX(-50%);
158
+ width: margin(7);
159
+ color: var(--playpilot-text-color);
160
+ }
161
+
114
162
  .close {
115
163
  --playpilot-button-background: var(--playpilot-modal-close-button-background, var(--playpilot-content));
116
164
  --playpilot-button-shadow: var(--playpilot-modal-close-button-shadow, var(--playpilot-shadow));
117
165
  --playpilot-button-padding: var(--playpilot-modal-close-button-padding, 0.5rem);
118
166
  --playpilot-button-text-color: var(--playpilot-modal-close-button-text-color, var(--playpilot-text-color-alt));
119
167
  z-index: 5;
120
- position: absolute;
121
- top: margin(1);
168
+ position: fixed;
169
+ top: calc(var(--dialog-offset) + margin(1));
122
170
  right: margin(1);
123
171
 
172
+ @media (min-width: $max-width) {
173
+ position: absolute;
174
+ top: margin(1);
175
+ }
176
+
124
177
  &:hover {
125
178
  filter: brightness(1.1);
126
179
  }
127
180
  }
181
+
182
+ .prepend {
183
+ z-index: 1;
184
+ position: relative;
185
+ width: calc(100% - margin(1));
186
+ max-width: $max-width;
187
+ padding-top: margin(3);
188
+ margin: auto auto margin(0.5);
189
+
190
+ @media (min-width: $max-width) {
191
+ padding-top: 0;
192
+ margin-top: 0;
193
+ }
194
+ }
195
+
196
+ .bubble {
197
+ z-index: 1;
198
+ position: relative;
199
+ width: calc(100% - margin(1));
200
+ max-width: $max-width;
201
+ margin: margin(0.5);
202
+
203
+ @media (min-width: $max-width) {
204
+ width: 100%;
205
+ margin: 0 0 margin(0.5);
206
+ }
207
+
208
+ &.inline {
209
+ width: 100%;
210
+ margin: auto 0 0;
211
+
212
+ @media (min-width: $max-width) {
213
+ margin-top: 0;
214
+ }
215
+ }
216
+
217
+ .prepend + & {
218
+ &:not(.inline) {
219
+ margin-top: 0;
220
+ }
221
+ }
222
+ }
128
223
  </style>