@playpilot/tpi 5.2.1 → 5.4.0-beta.topscroll-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/events.md ADDED
@@ -0,0 +1,60 @@
1
+ This document highlights the purpose of each tracking event.
2
+
3
+ ## Payload
4
+
5
+ All events share a common payload:
6
+
7
+ - `url`: The URL of the related article. This is a URL with only the protocol, base url, and pathname. No parameters are included
8
+ - `organization_sid`: The sid for the related organization
9
+ - `domain_sid`: The sid for the related domain
10
+
11
+ Events related to titles share an additional set of data (referred to below as `Title`):
12
+
13
+ - `original_title`
14
+ - `title_sid`
15
+ - `title_type`: "movie" or "series"
16
+ - `providers`: An array of provider names
17
+
18
+ Events may have additional data in their payload.
19
+
20
+ ### General
21
+ Event | Action | Info | Payload
22
+ --- | --- | --- | ---
23
+ `ali_article_page_view` | _Fires any time an article is visited_ | This event will fire right after all data is fetched and will fire regardless of if there are injections or not | It will fire even on pages where injections are disabled. | -
24
+ `ali_links_injected` | _Fires as long as any injections are injected into the article_ | Includes an object with the number of injections for this article with `manual` and `ai` as two separate numbers. | `manual` (number of manual injection), `ai` (number of ai injections)
25
+
26
+ ### Modal
27
+ Event | Action | Info | Payload
28
+ --- | --- | --- | ---
29
+ `ali_title_modal_view` | _Fires any time a title modal is viewed_ | The title modal opens when clicking an injection both on desktop and mobile | `Title`
30
+ `ali_title_modal_scroll` | _Fires the first time a user scrolls inside of a titel modal._ | | `Title`
31
+ `ali_title_modal_playlink_click` | _Fires any time a playlink is clicked inside of a title modal_ | Includes data on which playlink was clicked. | `Title`, `playlink` (name of the clicked playlink)
32
+ `ali_title_modal_save_click` | _Currently unused, there is no save functionality._ | | `Title`
33
+
34
+ ### Popover
35
+ Event | Action | Info | Payload
36
+ --- | --- | --- | ---
37
+ `ali_title_popover_view` | _Fires any time a title popover is viewed_ | The title popover opens when hovering an injection. This will only occur on desktop. | `Title`
38
+ `ali_title_popover_save_click` | _Currently unused, there is no save functionality._ | | `Title`
39
+ `ali_title_popover_playlink_click` | _Fires any time a playlink is clicked inside of a title popover_ | Includes data on which playlink was clicked. | `Title`, `playlink` (name of the clicked playlink)
40
+
41
+ ### After Article
42
+ Event | Action | Info | Payload
43
+ --- | --- | --- | ---
44
+ `ali_after_article_playlink_click` | _Fires any time a playlink is clicked inside of an after article block_ | This block will only appear injections are configured for after article, which no one is currently using. Includes data on which playlink was clicked. | `Title`, `playlink` (name of the clicked playlink)
45
+ `ali_after_article_modal_button_click` | _Fires any time an after article modal button is clicked_ | This button will only appear in after article blocks when configured, which, which no one is currently using | `Title`
46
+
47
+ ### Data
48
+ Event | Action | Info | Payload
49
+ --- | --- | --- | ---
50
+ `ali_injection_failed` | _Fires only inside of the Editor for each injection that failed_ | Only includes visible failures, for instance, it will ignore failures because of already existing links. If a user is shown a message about a failed injection, this event will fire. Includes data on the phrase and sentence for the injection. | `Title`, `phrase`, `sentence`
51
+ `ali_injection_count` | _Fires the first time the Editor is shown_ | This logs the total amount of injections, the total amount of failed and manual injections, and the total amount of successful injections. | `total` (number of failed + successsful injections), `failed_automatic`, `failed_manual`, `final_injected` (number of successful injections)
52
+ `ali_fetch_config_failed` | _Fires whenever the config object failed to fetch_ | When this happens, injections are aborted.
53
+ `ali_auth_failed` | _Fires whenever authentication for the Editor fails._
54
+
55
+ ### Reporting
56
+ Event | Action | Info | Payload
57
+ --- | --- | --- | ---
58
+ `ali_manual_report` | _Fires only through manual action when reporting issues with an injection via the Editor._ | | `Title`, `report_reason`, `sid` (of injection), `title` (of injection), `sentence`, `failed` (true or false), `failed_message` (reason for failure as given in the editor), `manual` (true or false)
59
+ `ali_editor_error` | _Fires whenever an error occurs within the Editor._ | | `Title`, `phrase`, `sentence`
60
+ `ali_injection_error` | _Fires whenever an error occurs during injection_ | This includes fetching the injections as well as actually injecting itself. Does not include fetching of the config object. | `message` (error message as given by the browser)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.2.1",
3
+ "version": "5.4.0-beta.topscroll-1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
package/src/lib/api.ts CHANGED
@@ -51,6 +51,9 @@ export async function fetchLinkInjections(
51
51
 
52
52
  const parsed = await response.json()
53
53
 
54
+ // This is used when debugging (using window.PlayPilotLinkInjections.debug())
55
+ window.PlayPilotLinkInjections.last_successful_fetch = parsed
56
+
54
57
  return parsed
55
58
  }
56
59
 
@@ -1,13 +1,16 @@
1
+ /** @see /events.md */
1
2
  export const TrackingEvent = Object.freeze({
2
3
  ArticlePageView: 'ali_article_page_view',
3
4
  ArticleInjected: 'ali_links_injected',
4
5
 
5
6
  TitleModalView: 'ali_title_modal_view',
7
+ TitleModalClose: 'ali_title_modal_close',
6
8
  TitleModalScroll: 'ali_title_modal_scroll',
7
9
  TitleModalPlaylinkClick: 'ali_title_modal_playlink_click',
8
10
  TitleModalSaveClick: 'ali_title_modal_save_click',
9
11
 
10
12
  TitlePopoverView: 'ali_title_popover_view',
13
+ TitlePopoverClose: 'ali_title_popover_close',
11
14
  TitlePopoverSaveClick: 'ali_title_popover_save_click',
12
15
  TitlePopoverPlaylinkClick: 'ali_title_popover_playlink_click',
13
16
 
@@ -22,4 +25,6 @@ export const TrackingEvent = Object.freeze({
22
25
  ManualReport: 'ali_manual_report',
23
26
  EditorError: 'ali_editor_error',
24
27
  InjectionError: 'ali_injection_error',
28
+
29
+ TopScrollClick: 'ali_top_scroll_click',
25
30
  })
@@ -103,6 +103,10 @@ export function getLinkInjectionsParentElement(): HTMLElement {
103
103
  return document.querySelector('article') || document.querySelector('main') || document.body
104
104
  }
105
105
 
106
+ export function getPageText(elements: HTMLElement[]): string {
107
+ return elements.map(element => element.innerText).join('\n\n')
108
+ }
109
+
106
110
  /**
107
111
  * Replace all found injections within all given elements on the page
108
112
  * @returns Returns an array of injections with injections that failed to be inserted marked as `failed`.
@@ -36,6 +36,14 @@ export async function track(event: string, title: TitleData | null = null, paylo
36
36
  source: 'ali',
37
37
  })),
38
38
  })
39
+
40
+ pushEventToWindow({ event, payload })
41
+ }
42
+
43
+ /** Save this event to the window object. Used when calling .debug() */
44
+ function pushEventToWindow(data: { event: string, payload: Record<string, any> }) {
45
+ if (!window.PlayPilotLinkInjections.tracked_events) window.PlayPilotLinkInjections.tracked_events = []
46
+ window.PlayPilotLinkInjections.tracked_events?.push(data)
39
47
  }
40
48
 
41
49
  /**
@@ -6,6 +6,7 @@ declare global {
6
6
  app: any | null
7
7
  initialize(config: ScriptConfig): void
8
8
  destroy(): void
9
+ debug(): void
9
10
  }
10
11
  }
11
12
  }
@@ -7,4 +7,6 @@ export type ScriptConfig = {
7
7
  after_article_selector?: string
8
8
  after_article_insert_position?: InsertPosition | ''
9
9
  language?: string | null
10
+ last_successful_fetch?: LinkInjectionResponse | null
11
+ tracked_events?: { event: string, payload: Record<string, any> }[]
10
12
  }
@@ -8,7 +8,7 @@ export type TitleData = {
8
8
  genres: string[]
9
9
  year: number
10
10
  imdb_score: number
11
- type: string
11
+ type: 'movie' | 'series'
12
12
  providers: PlaylinkData[]
13
13
  description: string
14
14
  small_poster: string
package/src/main.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { mount } from 'svelte'
2
2
  import App from './routes/+page.svelte'
3
- import { clearLinkInjections } from '$lib/linkInjection'
3
+ import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/linkInjection'
4
+ import { getPageMetaData } from '$lib/meta'
4
5
 
5
6
  window.PlayPilotLinkInjections = {
6
7
  token: '',
@@ -11,6 +12,8 @@ window.PlayPilotLinkInjections = {
11
12
  language: null,
12
13
  organization_sid: null,
13
14
  domain_sid: null,
15
+ last_successful_fetch: null,
16
+ tracked_events: [],
14
17
  app: null,
15
18
 
16
19
  initialize(config = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '' }): void {
@@ -48,6 +51,38 @@ window.PlayPilotLinkInjections = {
48
51
 
49
52
  clearLinkInjections()
50
53
  },
54
+
55
+ debug(): void {
56
+ const parentElement = getLinkInjectionsParentElement()
57
+ const elements = getLinkInjectionElements(parentElement)
58
+
59
+ console.group('PlayPilot Link Injection Debug')
60
+ console.groupCollapsed('Config')
61
+ console.table(Object.entries(this))
62
+ console.groupEnd()
63
+
64
+ console.groupCollapsed('Elements')
65
+ console.log('Parent element', parentElement)
66
+ console.log('Valid elements', elements)
67
+ console.groupEnd()
68
+
69
+ console.groupCollapsed('Last fetch')
70
+ console.log(this.last_successful_fetch)
71
+ console.groupEnd()
72
+
73
+ console.groupCollapsed('Meta')
74
+ console.log(getPageMetaData())
75
+ console.groupEnd()
76
+
77
+ console.groupCollapsed('Page text')
78
+ console.log(getPageText(elements))
79
+ console.groupEnd()
80
+
81
+ console.groupCollapsed('Tracked events')
82
+ console.log(this.tracked_events)
83
+ console.groupEnd()
84
+ console.groupEnd()
85
+ }
51
86
  }
52
87
 
53
88
  export default window.PlayPilotLinkInjections
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte'
3
3
  import { fetchConfig, pollLinkInjections } from '$lib/api'
4
- import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/linkInjection'
4
+ import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/linkInjection'
5
5
  import { setTrackingSids, track } from '$lib/tracking'
6
6
  import { getFullUrlPath } from '$lib/url'
7
7
  import { isCrawler } from '$lib/crawler'
@@ -26,7 +26,7 @@
26
26
  // @ts-ignore It's ok if the response is empty
27
27
  const { ai_injections: aiInjections = [], manual_injections: manualInjections = [] } = $derived(response || {})
28
28
 
29
- const pageText = $derived(elements.map(element => element.innerText).join('\n\n'))
29
+ const pageText = $derived(getPageText(elements))
30
30
 
31
31
  // Rerender link injections when linkInjections change. This is only relevant for editiorial mode.
32
32
  $effect(() => {
@@ -0,0 +1,251 @@
1
+ <script lang="ts">
2
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
+ import { track } from '$lib/tracking'
4
+ import { fade } from 'svelte/transition'
5
+
6
+ // Placeholder data, of course
7
+ const { campaign = {
8
+ campaign_format: 'top_scroll',
9
+ campaign_type: 'video',
10
+ campaign_platforms: ['web', 'android_app', 'ios_app'],
11
+ campaign_name: 'some_campaign_NL',
12
+ campaign_start: '2025-04-24T00:00:00Z',
13
+ campaign_end: '2025-04-30T23:59:00Z',
14
+ campaign_region: 'nl',
15
+ content: {
16
+ header: 'Some top scroll header',
17
+ header_logo: 'https://example.com/',
18
+ header_logo_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
19
+ subheader: null,
20
+ image: 'https://example.com/',
21
+ image_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
22
+ video: null,
23
+ format: null,
24
+ },
25
+ cta: {
26
+ header: 'Button',
27
+ subheader: null,
28
+ image: null,
29
+ image_uuid: null,
30
+ url: 'https://google.com/',
31
+ },
32
+ content_playlist: null,
33
+ provider: null,
34
+ impression_trackers: [],
35
+ target_title_uuid: null,
36
+ target_title_sid: null,
37
+ backfill_providers: [],
38
+ autogenerated: false,
39
+ enabled: true,
40
+ hide_imdb_score: false,
41
+ hide_sponsored_message: false,
42
+ disclaimer: 'Some disclaimer',
43
+ } } = $props()
44
+
45
+ const { disclaimer, content, cta } = $derived(campaign)
46
+ const { format, header, header_logo: logo, image_uuid: backgroundImageUUID } = $derived(content)
47
+ const { header: buttonLabel, url: href } = $derived(cta)
48
+
49
+ const simple = $derived(format === 'large')
50
+
51
+ let clientWidth = $state(0)
52
+ let tooltipVisible = $state(false)
53
+
54
+ function trackClick(): void {
55
+ track(TrackingEvent.TopScrollClick)
56
+ }
57
+
58
+ function toggleTooltip(event: MouseEvent): void {
59
+ event.preventDefault()
60
+ event.stopPropagation()
61
+
62
+ tooltipVisible = !tooltipVisible
63
+ }
64
+ </script>
65
+
66
+ <a
67
+ {href}
68
+ target="_blank"
69
+ class="top-scroll"
70
+ class:simple
71
+ tabindex="-1"
72
+ onclick={trackClick}
73
+ rel="sponsored"
74
+ style:--width="{clientWidth}px"
75
+ bind:clientWidth>
76
+ <div class="content">
77
+ {#if simple}
78
+ <img class="content-image" src="https://picsum.photos/seed/movie/728/90" alt={header} width="728" height="90" />
79
+ {:else}
80
+ {#if logo}
81
+ <img class="logo" src="https://picsum.photos/seed/disney/50/50" alt="" width="50" height="50" />
82
+ {/if}
83
+
84
+ <p class="tagline">{header}</p>
85
+
86
+ {#if buttonLabel}
87
+ <button class="button">{buttonLabel}</button>
88
+ {/if}
89
+ {/if}
90
+ </div>
91
+
92
+ {#if !simple}
93
+ {#if backgroundImageUUID}
94
+ <div class="background" style:--background="url('https://picsum.photos/seed/movie/728/90')"></div>
95
+ {/if}
96
+
97
+ {#if disclaimer}
98
+ <button class="tooltip" onclick={toggleTooltip}>
99
+ <div class="disclaimer">i</div>
100
+
101
+ {#if tooltipVisible}
102
+ <div class="tooltip-content" transition:fade={{ duration: 50 }}>
103
+ {disclaimer}
104
+ </div>
105
+ {/if}
106
+ </button>
107
+ {/if}
108
+ {/if}
109
+ </a>
110
+
111
+ <style lang="scss">
112
+ .top-scroll {
113
+ position: relative;
114
+ display: block;
115
+ width: 100%;
116
+ border-radius: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
117
+ background: black;
118
+ color: var(--playpilot-top-scroll-text-color, white);
119
+ font-family: var(--playpilot-top-scroll-font-family, var(--playpilot-detail-font-family, var(--playpilot-font-family)));
120
+ font-size: var(--playpilot-top-scroll-font-size, var(--playpilot-detail-font-size, 14px));
121
+ text-decoration: none;
122
+ line-height: 1.35;
123
+ }
124
+
125
+ .content {
126
+ display: flex;
127
+ z-index: 1;
128
+ position: relative;
129
+ align-items: center;
130
+ height: 100%;
131
+ min-height: margin(4.5);
132
+ padding: margin(0.25) margin(0.5);
133
+ gap: margin(0.5);
134
+ color: var(--playpilot-top-scroll-text-color, white);
135
+
136
+ .simple & {
137
+ justify-content: center;
138
+ min-height: 0;
139
+ padding: 0;
140
+ }
141
+ }
142
+
143
+ .logo {
144
+ border-radius: var(--playpilot-top-scroll-logo-border-radius, margin(0.5));
145
+ background: black;
146
+ }
147
+
148
+ .tagline {
149
+ margin: 0;
150
+ text-shadow: var(--playpilot-shadow);
151
+ }
152
+
153
+ .button {
154
+ margin-left: auto;
155
+ margin-right: margin(1);
156
+ padding: margin(0.25) margin(0.5);
157
+ background: var(--playpilot-top-scroll-button-background, white);
158
+ border: 0;
159
+ border-radius: var(--playpilot-top-scroll-button-border-radius, margin(0.25));
160
+ box-shadow: var(--playpilot-shadow);
161
+ color: var(--playpilot-top-scroll-button-text-color, black);
162
+ font-family: inherit;
163
+ font-size: inherit;
164
+ line-height: inherit;
165
+
166
+ &:hover {
167
+ outline: margin(0.25) solid var(--playpilot-top-scroll-button-background, rgba(white, 0.35));
168
+ background: var(--playpilot-top-scroll-button-background, white);
169
+ }
170
+
171
+ &:active {
172
+ background: var(--playpilot-top-scroll-button-background, white);
173
+ }
174
+ }
175
+
176
+ .background {
177
+ z-index: 0;
178
+ position: absolute;
179
+ top: 0;
180
+ right: 0;
181
+ bottom: 0;
182
+ left: 0;
183
+ border-radius: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
184
+ background-image: var(--background);
185
+ background-position: center;
186
+ background-size: cover;
187
+ opacity: 0.4;
188
+
189
+ .top-scroll:hover & {
190
+ filter: brightness(1.15);
191
+ }
192
+ }
193
+
194
+ .content-image {
195
+ display: block;
196
+ max-width: 100%;
197
+ height: auto;
198
+ background: black;
199
+ border-radius: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
200
+
201
+ .top-scroll:hover & {
202
+ filter: brightness(1.15);
203
+ }
204
+ }
205
+
206
+ .disclaimer {
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ border: var(--playpilot-top-scroll-disclaimer-border, 2px solid rgba(255, 255, 255, 0.5));
211
+ padding: margin(0.25);
212
+ height: margin(1.25);
213
+ width: margin(1.25);
214
+ border-radius: 99px;
215
+ color: var(--playpilot-top-scroll-disclaimer-text-color, rgba(255, 255, 255, 0.75));
216
+ font-weight: bold;
217
+ line-height: 1;
218
+
219
+ &:hover {
220
+ border-color: var(--playpilot-top-scroll-disclaimer-hover-border-color, white);
221
+ color: var(--playpilot-top-scroll-disclaimer-hover-text-color, white);
222
+ }
223
+ }
224
+
225
+ .tooltip {
226
+ z-index: 2;
227
+ position: absolute;
228
+ right: 0;
229
+ bottom: 0;
230
+ padding: margin(0.25);
231
+ background: transparent;
232
+ border: 0;
233
+ color: var(--playpilot-top-scroll-text-color, white);
234
+ cursor: pointer;
235
+ }
236
+
237
+ .tooltip-content {
238
+ display: block;
239
+ z-index: 20;
240
+ position: absolute;
241
+ right: 100%;
242
+ bottom: margin(0.25);
243
+ width: calc(var(--width) * 0.8);
244
+ padding: margin(0.5);
245
+ border-radius: margin(1);
246
+ background: var(--playpilot-content);
247
+ box-shadow: var(--playpilot-shadow);
248
+ line-height: 1.1;
249
+ text-align: right;
250
+ }
251
+ </style>
@@ -7,11 +7,12 @@
7
7
 
8
8
  interface Props {
9
9
  children: Snippet
10
+ bubble?: Snippet
10
11
  onclose?: () => void
11
12
  onscroll?: () => void
12
13
  }
13
14
 
14
- const { children, onclose = () => null, onscroll = () => null }: Props = $props()
15
+ const { children, bubble, onclose = () => null, onscroll = () => null }: Props = $props()
15
16
 
16
17
  setContext('scope', 'modal')
17
18
 
@@ -22,12 +23,12 @@
22
23
  return () => document.body.style.overflowY = baseOverflowStyle || ''
23
24
  })
24
25
 
25
- function scaleOrFly(node: Element): TransitionConfig {
26
+ function scaleOrFly(node: Element, options: { y: number } = { y: 0 }): TransitionConfig {
26
27
  if (prefersReducedMotion.current) return fade(node, { duration: 0 })
27
28
 
28
29
  const shouldFly = window.innerWidth < 600
29
30
 
30
- if (shouldFly) return fly(node, { duration: 250, y: window.innerHeight })
31
+ if (shouldFly) return fly(node, { duration: 250, ...options })
31
32
  return scale(node, { duration: 150, start: 0.85 })
32
33
  }
33
34
  </script>
@@ -35,7 +36,13 @@
35
36
  <svelte:window on:keydown={({ key }) => { if (key === 'Escape') onclose() }} />
36
37
 
37
38
  <div class="modal" transition:fade|global={{ duration: 150 }}>
38
- <div class="dialog" {onscroll} role="dialog" aria-labelledby="title" transition:scaleOrFly|global data-view-transition-new>
39
+ {#if bubble}
40
+ <div class="bubble" transition:scaleOrFly|global={{ y: -10 }}>
41
+ {@render bubble()}
42
+ </div>
43
+ {/if}
44
+
45
+ <div class="dialog" {onscroll} role="dialog" aria-labelledby="title" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new>
39
46
  <div class="close">
40
47
  <RoundButton onclick={() => onclose()}>
41
48
  <IconClose />
@@ -51,44 +58,46 @@
51
58
  </div>
52
59
 
53
60
  <style lang="scss">
61
+ $max-width: 600px;
62
+
54
63
  .modal {
55
64
  z-index: 2147483647; // As high as she goes
56
65
  box-sizing: border-box;
57
66
  position: fixed;
58
67
  display: flex;
59
- justify-content: center;
60
- align-items: flex-start;
68
+ flex-direction: column;
69
+ justify-content: flex-start;
70
+ align-items: center;
61
71
  top: 0;
62
72
  left: 0;
63
73
  width: 100%;
64
74
  height: 100%;
65
75
  background: var(--playpilot-detail-backdrop, rgba(0, 0, 0, 0.65));
66
76
 
67
- @media (min-width: 600px) {
77
+ @media (min-width: $max-width) {
68
78
  padding: margin(2);
79
+ gap: margin(1);
69
80
  }
70
81
  }
71
82
 
72
-
73
83
  .dialog {
74
84
  z-index: 1;
75
85
  position: relative;
76
86
  width: 100%;
77
- max-width: 600px;
87
+ max-width: $max-width;
78
88
  max-height: 80vh;
79
89
  overflow: auto;
80
90
  margin-top: auto;
81
91
  border-radius: var(--playpilot-detail-border-radius, margin(1) margin(1) 0 0);
82
92
  background: var(--playpilot-detail-background, var(--playpilot-light));
83
93
 
84
- @media (min-width: 600px) {
94
+ @media (min-width: $max-width) {
85
95
  margin-top: 0;
86
96
  border-radius: var(--playpilot-detail-border-radius, margin(1));
87
97
  max-height: 100%;
88
98
  }
89
99
  }
90
100
 
91
-
92
101
  .backdrop {
93
102
  z-index: 0;
94
103
  position: absolute;
@@ -112,4 +121,17 @@
112
121
  filter: brightness(1.1);
113
122
  }
114
123
  }
124
+
125
+ .bubble {
126
+ z-index: 1;
127
+ position: relative;
128
+ width: calc(100% - margin(1));
129
+ max-width: $max-width;
130
+ margin: margin(0.5);
131
+
132
+ @media (min-width: $max-width) {
133
+ width: 100%;
134
+ margin: 0;
135
+ }
136
+ }
115
137
  </style>
@@ -5,10 +5,11 @@
5
5
 
6
6
  interface Props {
7
7
  children: Snippet
8
+ bubble?: Snippet
8
9
  maxHeight?: number
9
10
  }
10
11
 
11
- let { children, maxHeight = $bindable() }: Props = $props()
12
+ let { children, bubble, maxHeight = $bindable() }: Props = $props()
12
13
 
13
14
  setContext('scope', 'popover')
14
15
 
@@ -63,11 +64,20 @@
63
64
  <div class="dialog" transition:fly|global={{ duration: prefersReducedMotion.current ? 0 : 100, y: 10 }} data-view-transition-old>
64
65
  {@render children()}
65
66
  </div>
67
+
68
+ {#if bubble}
69
+ <div transition:fly|global={{ duration: prefersReducedMotion.current ? 0 : 100, y: 10 }}>
70
+ {@render bubble()}
71
+ </div>
72
+ {/if}
66
73
  </div>
67
74
 
68
75
  <style lang="scss">
69
76
  .popover {
70
77
  --offset: #{margin(0.5)};
78
+ display: flex;
79
+ gap: margin(0.5);
80
+ flex-direction: column-reverse;
71
81
  position: absolute;
72
82
  top: calc((var(--offset) - 1px) * -1); /* Add 1 pixel to account for rounding errors */
73
83
  left: 0;
@@ -78,6 +88,7 @@
78
88
  z-index: 2147483647; // As high as she goes
79
89
 
80
90
  &.flip {
91
+ flex-direction: column;
81
92
  top: auto;
82
93
  bottom: calc(var(--offset) + 1px); /* Add 1 pixel to account for rounding errors */
83
94
  transform: translateY(calc(100% + var(--offset)));
@@ -2,8 +2,10 @@
2
2
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
3
  import { track } from '$lib/tracking'
4
4
  import type { TitleData } from '$lib/types/title'
5
+ import { onMount } from 'svelte'
5
6
  import Modal from './Modal.svelte'
6
7
  import Title from './Title.svelte'
8
+ import TopScroll from './Ads/TopScroll.svelte'
7
9
 
8
10
  interface Props {
9
11
  onclose: () => void,
@@ -16,6 +18,11 @@
16
18
 
17
19
  let hasTrackedScrolling = false
18
20
 
21
+ onMount(() => {
22
+ const openTimestamp = Date.now()
23
+ return () => track(TrackingEvent.TitleModalClose, title, { time_spent: Date.now() - openTimestamp })
24
+ })
25
+
19
26
  function onscroll(): void {
20
27
  if (hasTrackedScrolling) return
21
28
 
@@ -26,4 +33,8 @@
26
33
 
27
34
  <Modal {onclose} {onscroll}>
28
35
  <Title {title} />
36
+
37
+ {#snippet bubble()}
38
+ <TopScroll />
39
+ {/snippet}
29
40
  </Modal>