@playpilot/tpi 5.33.0 → 5.34.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": "5.33.0",
3
+ "version": "5.34.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -16,7 +16,7 @@ export async function api<T>(path: string, { headers = {}, method = 'GET', body
16
16
  const useCache = !isEditorialModeEnabled() && method === 'GET'
17
17
  if (useCache && (path in cache)) return cache[path]
18
18
 
19
- const baseHeaders = { 'Content-Type': 'application/json' }
19
+ const baseHeaders: Record<string, string> = method === 'GET' ? {} : { 'Content-Type': 'application/json' }
20
20
 
21
21
  const response = await fetch(apiBaseUrl + path, {
22
22
  method,
@@ -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
+
@@ -9,6 +9,11 @@ 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
  UserTimeSpent: {
13
18
  key: 'user_time_spent',
14
19
  numberOfVariants: 10,
@@ -1,9 +1,10 @@
1
- import { mount, unmount } from 'svelte'
2
1
  import { cleanPhrase, findNumberOfMatchesInString, findShortestMatchBetweenPhrases, findTextNodeContaining, getIndexOfPhraseInElement, getIndexOfPhraseInBoundary, getNumberOfLeadingAndTrailingSpaces, isNodeInLink, replaceBetween, replaceStartingFrom, findSurroundingPhrases } from './text'
3
2
  import type { LinkInjection, LinkInjectionTypes } from './types/injection'
4
3
  import { getNumberOfOccurrencesInArray } from './array'
5
4
  import { destroyAllModals, openModalForInjectedLink } from './modal'
6
- import InTextDisclaimer from '../routes/components/InTextDisclaimer.svelte'
5
+ import { getSplitTestVariantName } from './splitTest'
6
+ import { SplitTest } from './enums/SplitTest'
7
+ import { colorLuminance } from './color'
7
8
  import { clearCurrentlyHoveredInjection, destroyLinkPopover, destroyLinkPopoverOnMouseleave, isPopoverActive, openPopoverForInjectedLink } from './popover'
8
9
  import { clearAfterArticlePlaylinks, insertAfterArticlePlaylinks } from './afterArticle'
9
10
  import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
@@ -11,6 +12,8 @@ import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
11
12
  export const keyDataAttribute = 'data-playpilot-injection-key'
12
13
  export const keySelector = `[${keyDataAttribute}]`
13
14
 
15
+ const linksIntersectionObserver = typeof window !== 'undefined' ? new IntersectionObserver(animateLink) : null
16
+
14
17
  /**
15
18
  * Return a list of all valid text containing elements that may get injected into.
16
19
  * This excludes duplicates, empty elements, links, buttons, and header tags.
@@ -298,6 +301,19 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
298
301
  linkElement.target = '_blank'
299
302
  linkElement.rel = 'noopener nofollow noreferrer'
300
303
 
304
+ // Part of a split test, insert an absolute positioned icon in the top right of links.
305
+ if (getSplitTestVariantName(SplitTest.InTextEngagement) === 'Clapper Icon' || getSplitTestVariantName(SplitTest.InTextEngagement) === 'Play Icon') {
306
+ const iconElement = document.createElement('span')
307
+ iconElement.classList.add('playpilot-injection-info-icon')
308
+ iconElement.dataset.playpilotElement = 'true'
309
+
310
+ 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>'
311
+ 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>'
312
+
313
+ iconElement.innerHTML = getSplitTestVariantName(SplitTest.InTextEngagement) === 'Clapper Icon' ? clapperIcon : playIcon
314
+ linkElement.insertAdjacentElement('beforeend', iconElement)
315
+ }
316
+
301
317
  injectionElement.insertAdjacentElement('beforeend', linkElement)
302
318
 
303
319
  return { injectionElement, linkElement }
@@ -402,8 +418,10 @@ function addCSSVariablesToLinks(): void {
402
418
  * Add event listeners to all injected links. These events are for both the popover and the modal.
403
419
  */
404
420
  function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
405
- window.addEventListener('click', (event) => openModalForInjectedLink(event, injections))
406
421
  window.addEventListener('mousemove', destroyLinkPopoverOnMouseleave)
422
+ window.addEventListener('click', (event) => {
423
+ openModalForInjectedLink(event, injections)
424
+ })
407
425
 
408
426
  const createdInjectionElements = document.querySelectorAll<HTMLElement>(keySelector)
409
427
 
@@ -419,6 +437,26 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
419
437
  })
420
438
 
421
439
  injectionElement.addEventListener('mouseleave', clearCurrentlyHoveredInjection)
440
+
441
+ linksIntersectionObserver!.observe(injectionElement)
442
+ })
443
+ }
444
+
445
+ export function animateLink(entries: IntersectionObserverEntry[]): void {
446
+ if (getSplitTestVariantName(SplitTest.InTextEngagement) !== 'Animated Link') return
447
+
448
+ entries.forEach(entry => {
449
+ if (!entry.isIntersecting) return
450
+
451
+ const linkElement = entry.target.querySelector('a')
452
+ if (!linkElement) return
453
+
454
+ const linkColor = window.getComputedStyle(linkElement).color
455
+
456
+ ;(entry.target as HTMLElement).style = `--animation-color: ${colorLuminance(linkColor, -0.5)}`
457
+
458
+ entry.target.classList.remove('animate-injection')
459
+ setTimeout(() => entry.target.classList.add('animate-injection'))
422
460
  })
423
461
  }
424
462
 
@@ -443,6 +481,9 @@ export function clearLinkInjection(key: string): void {
443
481
  const element: HTMLAnchorElement | null = document.querySelector(`[${keyDataAttribute}="${key}"]`)
444
482
  if (!element) return
445
483
 
484
+ const playpilotElements = element.querySelectorAll('[data-playpilot-element]')
485
+ playpilotElements.forEach(element => element.remove())
486
+
446
487
  const linkContent = element.querySelector('a')?.innerHTML
447
488
  element.outerHTML = linkContent || ''
448
489
  }
package/src/lib/modal.ts CHANGED
@@ -10,6 +10,8 @@ import { getPlayPilotWrapperElement, keyDataAttribute, keySelector } from "./inj
10
10
  import { playFallbackViewTransition } from "./viewTransition"
11
11
  import { destroyLinkPopover } from "./popover"
12
12
  import { prefersReducedMotion } from "svelte/motion"
13
+ import { trackSplitTestAction } from "./splitTest"
14
+ import { SplitTest } from "./enums/SplitTest"
13
15
 
14
16
  type ModalType = 'title' | 'participant'
15
17
 
@@ -129,6 +131,8 @@ export function openModalForInjectedLink(event: MouseEvent, injections: LinkInje
129
131
 
130
132
  event.preventDefault()
131
133
 
134
+ trackSplitTestAction(SplitTest.InTextEngagement, 'click')
135
+
132
136
  playFallbackViewTransition(() => {
133
137
  destroyLinkPopover(false)
134
138
  openModal({ event, injection, data: injection.title_details })
@@ -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: -200% 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 650ms 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 {
@@ -9,7 +9,7 @@
9
9
  import { fetchAds } from '$lib/api/ads'
10
10
  import { fetchConfig } from '$lib/api/config'
11
11
  import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/api/auth'
12
- import { getSplitTestVariantName } from '$lib/splitTest'
12
+ import { trackSplitTestView, getSplitTestVariantName } from '$lib/splitTest'
13
13
  import { SplitTest } from '$lib/enums/SplitTest'
14
14
  import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
15
15
  import Editor from './components/Editorial/Editor.svelte'
@@ -57,7 +57,11 @@
57
57
  fireQueuedTrackingEvents()
58
58
  track(TrackingEvent.ArticlePageView)
59
59
 
60
- 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)
61
65
  }
62
66
 
63
67
  async function initialize(): Promise<void> {
@@ -234,7 +238,7 @@
234
238
  </svelte:boundary>
235
239
  {/if}
236
240
 
237
- <Debugger />
241
+ <Debugger onrerender={rerender} />
238
242
  </div>
239
243
 
240
244
  {#if response?.pixels?.length}
@@ -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}
@@ -82,8 +82,6 @@
82
82
  <style lang="scss">
83
83
  .header {
84
84
  padding: margin(4) margin(1) margin(2);
85
- background: linear-gradient(to bottom, theme(detail-background-light, lighter), transparent);
86
- border-radius: theme(detail-border-radius, margin(1) margin(1) 0 0);
87
85
  font-family: theme(detail-font-family, font-family);
88
86
  font-weight: theme(detail-font-weight, normal);
89
87
  font-size: theme(detail-font-size, font-size-base);