@playpilot/tpi 6.6.4 → 6.7.0

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.6.4",
3
+ "version": "6.7.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -55,6 +55,8 @@ export const TrackingEvent = {
55
55
 
56
56
  // Various
57
57
  ShareTitle: 'ali_share_title',
58
+ SaveTitle: 'ali_save_title',
59
+ NotifyTitle: 'ali_notify_title',
58
60
  TrailerClick: 'ali_trailer_button_click',
59
61
  RegionRequestFailed: 'ali_region_request_failed',
60
62
 
@@ -3,16 +3,17 @@
3
3
 
4
4
  interface Props {
5
5
  variant?: 'filled' | 'border' | 'link'
6
- size?: 'base' | 'large'
6
+ size?: 'base' | 'large' | 'wide'
7
+ label?: string | null
7
8
  active?: boolean
8
9
  onclick?: (event: MouseEvent) => void
9
10
  children?: Snippet
10
11
  }
11
12
 
12
- const { variant = 'filled', size = 'base', active = false, onclick, children }: Props = $props()
13
+ const { variant = 'filled', size = 'base', label = null, active = false, onclick, children }: Props = $props()
13
14
  </script>
14
15
 
15
- <button class="button {variant} {size}" class:active {onclick}>
16
+ <button class="button {variant} {size}" class:active {onclick} aria-label={label}>
16
17
  {@render children?.()}
17
18
  </button>
18
19
 
@@ -35,13 +36,13 @@
35
36
  cursor: pointer;
36
37
 
37
38
  :global(svg) {
38
- width: 1.5em;
39
- height: 1.5em;
39
+ --icon-size: 1.5em;
40
+ width: var(--icon-size);
41
+ height: var(--icon-size);
40
42
  opacity: 0.75;
41
43
 
42
44
  &:last-child {
43
- width: 0.85em;
44
- height: 0.85em;
45
+ --icon-size: 0.85em;
45
46
  }
46
47
  }
47
48
  }
@@ -96,5 +97,14 @@
96
97
  .large {
97
98
  padding: 0.5em 1em;
98
99
  }
100
+
101
+ .wide {
102
+ font-size: margin(1);
103
+ padding: 0.5em 1.5em;
104
+
105
+ :global(svg:first-child) {
106
+ --icon-size: #{margin(0.95)};
107
+ }
108
+ }
99
109
  </style>
100
110
 
@@ -1,40 +1,44 @@
1
1
  <script lang="ts">
2
- import { fly } from 'svelte/transition'
2
+ import { scale } from 'svelte/transition'
3
3
  import IconDots from './Icons/IconDots.svelte'
4
4
  import type { Snippet } from 'svelte'
5
5
 
6
6
  interface Props {
7
- ariaLabel: string
7
+ label?: string
8
+ align?: 'right' | 'center'
9
+ button?: Snippet<[any]>
8
10
  children: Snippet
9
11
  }
10
12
 
11
- const { ariaLabel, children }: Props = $props()
13
+ const { label = '', align = 'right', button, children }: Props = $props()
12
14
 
13
15
  let active = $state(false)
14
- let buttonElement: HTMLElement | null = $state(null)
16
+ let element: Element | null = $state(null)
15
17
 
16
- /**
17
- * Close the context menu when clicking anything but the toggle button.
18
- * @param {MouseEvent} event
19
- */
20
- function closeOnOutsideClick(event: MouseEvent): void {
21
- const target = event.target as HTMLElement
18
+ function toggle(event: MouseEvent): void {
19
+ const target = event.currentTarget
22
20
 
23
- if (target === buttonElement || buttonElement?.contains(target)) return
21
+ if (target === element?.querySelector('button')) {
22
+ event.stopPropagation()
23
+ }
24
24
 
25
- active = false
25
+ active = !active
26
26
  }
27
27
  </script>
28
28
 
29
- <svelte:window onclick={closeOnOutsideClick} />
29
+ <svelte:window onclick={() => active = false} />
30
30
 
31
- <div class="context-menu">
32
- <button aria-label={ariaLabel} aria-expanded={active} onclick={() => active = !active} bind:this={buttonElement}>
33
- <IconDots />
34
- </button>
31
+ <div class="context-menu {align}" bind:this={element}>
32
+ {#if button}
33
+ {@render button({ toggle })}
34
+ {:else}
35
+ <button aria-label={label} aria-expanded={active} onclick={toggle}>
36
+ <IconDots />
37
+ </button>
38
+ {/if}
35
39
 
36
40
  {#if active}
37
- <div class="content" in:fly={{ duration: 150, y: 5 }}>
41
+ <div class="content" in:scale={{ duration: 50, start: 0.85 }}>
38
42
  {@render children()}
39
43
  </div>
40
44
  {/if}
@@ -59,12 +63,32 @@
59
63
  }
60
64
 
61
65
  .content {
62
- z-index: 1;
66
+ z-index: 10;
63
67
  position: absolute;
64
- top: 100%;
68
+ bottom: calc(100% + margin(0.5));
65
69
  right: 0;
66
70
  max-width: margin(15);
67
- border-radius: margin(0.5);
68
- background: theme(lighter);
71
+ border-radius: theme(context-menu-border-radius, margin(0.5));
72
+ background: theme(detail-background, lighter);
73
+ box-shadow: theme(shadow);
74
+
75
+ .center & {
76
+ right: auto;
77
+ left: 50%;
78
+ transform: translateX(-50%);
79
+ }
80
+
81
+ &::before {
82
+ content: "";
83
+ display: block;
84
+ position: absolute;
85
+ top: 0;
86
+ right: 0;
87
+ bottom: 0;
88
+ left: 0;
89
+ border-radius: theme(context-menu-border-radius, margin(0.5));
90
+ background: theme(context-menu-background-lightness, rgba(255, 255, 255, 0.05));
91
+ pointer-events: none;
92
+ }
69
93
  }
70
94
  </style>
@@ -131,7 +131,7 @@
131
131
  </div>
132
132
 
133
133
  <div class="context-menu">
134
- <ContextMenu ariaLabel="More options">
134
+ <ContextMenu label="More options">
135
135
  <button class="context-menu-action" onclick={onremove}>Remove</button>
136
136
  <button class="context-menu-action" onclick={showReportIssueModal}>Report issue</button>
137
137
  </ContextMenu>
@@ -0,0 +1,3 @@
1
+ <svg width="15" height="16" viewBox="0 0 15 16">
2
+ <path fill="currentColor" stroke-width="1" d="M5.06753 12.8367H2.01027C1.5832 12.8367 1.17362 12.667 0.871638 12.365C0.569653 12.0631 0.4 11.6535 0.4 11.2264C0.4 10.7993 0.569653 10.3898 0.871638 10.0878C1.17342 9.78599 1.58266 9.61636 2.00942 9.61613M5.06753 12.8367L2.01027 9.61613C2.00999 9.61613 2.00971 9.61613 2.00942 9.61613M5.06753 12.8367C5.09277 13.4454 5.34568 14.0241 5.77823 14.4566C6.23463 14.913 6.85364 15.1694 7.49908 15.1694C8.14452 15.1694 8.76352 14.913 9.21992 14.4566C9.65247 14.0241 9.90538 13.4454 9.93063 12.8367H12.9897C13.4168 12.8367 13.8264 12.667 14.1284 12.365C14.4303 12.0631 14.6 11.6535 14.6 11.2264C14.6 10.7993 14.4303 10.3898 14.1284 10.0878C13.8265 9.78595 13.4172 9.61631 12.9904 9.61613C12.981 9.61583 12.9721 9.61192 12.9655 9.60521C12.9588 9.59833 12.955 9.58906 12.9551 9.57944V9.57875V6.28436C12.9551 4.83782 12.3805 3.45053 11.3576 2.42767C10.3348 1.40481 8.94746 0.830176 7.50092 0.830176C6.05438 0.830176 4.66709 1.40481 3.64423 2.42767C2.62137 3.45053 2.04673 4.83782 2.04673 6.28436V9.57832M5.06753 12.8367L2.04673 9.57832M2.00942 9.61613C2.01927 9.61588 2.02866 9.61184 2.03561 9.60484C2.04262 9.59778 2.04661 9.58827 2.04673 9.57832M2.00942 9.61613L2.04673 9.57832M12.9868 10.6395L12.987 10.6404C13.1425 10.6404 13.2917 10.7022 13.4016 10.8122C13.5116 10.9222 13.5734 11.0713 13.5734 11.2269C13.5734 11.3824 13.5116 11.5316 13.4016 11.6415C13.2917 11.7515 13.1425 11.8133 12.987 11.8133H2.00751C1.85197 11.8133 1.70281 11.7515 1.59283 11.6415C1.48285 11.5316 1.42106 11.3824 1.42106 11.2269C1.42106 11.0713 1.48285 10.9222 1.59283 10.8122C1.70281 10.7022 1.85197 10.6404 2.00751 10.6404H2.0076C2.289 10.6402 2.55878 10.5282 2.75766 10.3291C2.95654 10.13 3.06826 9.86015 3.06826 9.57875V6.28436C3.06826 5.10936 3.53502 3.98248 4.36588 3.15163C5.19673 2.32077 6.32361 1.854 7.49862 1.854C8.67362 1.854 9.8005 2.32077 10.6314 3.15163C11.4622 3.98248 11.929 5.10936 11.929 6.28436L11.929 9.57764L11.929 9.57783M12.9868 10.6395L11.929 9.57783M12.9868 10.6395C12.8484 10.6392 12.7114 10.6119 12.5835 10.5589C12.4546 10.5056 12.3376 10.4274 12.239 10.3287C12.1405 10.2301 12.0624 10.113 12.0092 9.98408C11.956 9.85525 11.9287 9.7172 11.929 9.57783M12.9868 10.6395L11.929 9.57783M6.09242 12.8367H8.90574C8.88156 13.1738 8.73688 13.4924 8.49629 13.733C8.23181 13.9975 7.87311 14.146 7.49908 14.146C7.12505 14.146 6.76634 13.9975 6.50186 13.733C6.26127 13.4924 6.11659 13.1738 6.09242 12.8367Z" />
3
+ </svg>
@@ -1,3 +1,3 @@
1
- <svg width="24px" height="24px" viewBox="0 -960 960 960">
2
- <path fill="currentColor" d="m380-300 280-180-280-180v360ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
1
+ <svg width="12" height="12" viewBox="0 0 12 12">
2
+ <path stroke="currentColor" stroke-width="1.1" transform="translate(2 0)" d="M9.47461 5.37598C9.77461 5.54918 9.77461 5.98207 9.47461 6.15527L1.22461 10.9189C0.924693 11.0916 0.549804 10.8745 0.549804 10.5283L0.549805 1.00293C0.549805 0.656711 0.924691 0.439623 1.22461 0.612304L9.47461 5.37598Z" />
3
3
  </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16">
2
+ <path stroke="currentColor" stroke-width="1.33" d="M3.3335 5.2C3.3335 4.0799 3.3335 3.51984 3.55148 3.09202C3.74323 2.71569 4.04919 2.40973 4.42552 2.21799C4.85334 2 5.41339 2 6.5335 2H9.46683C10.5869 2 11.147 2 11.5748 2.21799C11.9511 2.40973 12.2571 2.71569 12.4488 3.09202C12.6668 3.51984 12.6668 4.0799 12.6668 5.2V14L8.00016 11.3333L3.3335 14V5.2Z" />
3
+ </svg>
@@ -1,3 +1,4 @@
1
- <svg viewBox="0 -960 960 960" width="18px" height="18px">
2
- <path d="M680-80q-50 0-85-35t-35-85q0-6 3-28L282-392q-16 15-37 23.5t-45 8.5q-50 0-85-35t-35-85q0-50 35-85t85-35q24 0 45 8.5t37 23.5l281-164q-2-7-2.5-13.5T560-760q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35q-24 0-45-8.5T598-672L317-508q2 7 2.5 13.5t.5 14.5q0 8-.5 14.5T317-452l281 164q16-15 37-23.5t45-8.5q50 0 85 35t35 85q0 50-35 85t-85 35Zm0-80q17 0 28.5-11.5T720-200q0-17-11.5-28.5T680-240q-17 0-28.5 11.5T640-200q0 17 11.5 28.5T680-160ZM200-440q17 0 28.5-11.5T240-480q0-17-11.5-28.5T200-520q-17 0-28.5 11.5T160-480q0 17 11.5 28.5T200-440Zm480-280q17 0 28.5-11.5T720-760q0-17-11.5-28.5T680-800q-17 0-28.5 11.5T640-760q0 17 11.5 28.5T680-720Zm0 520ZM200-480Zm480-280Z" fill="currentColor" />
1
+ <svg width="16" height="15" viewBox="0 0 16 15">
2
+ <path fill="currentColor" d="M6.70138 1.36838C6.70187 0.320494 7.75924 -0.233702 8.60177 0.0939687L8.76779 0.172094L8.82345 0.203344L8.87521 0.240453L14.4748 4.35666C15.273 4.89508 15.2742 6.06459 14.4807 6.60666L8.8879 10.8586L8.83029 10.9026L8.76779 10.9367C7.90544 11.4141 6.70148 10.8588 6.70138 9.74045V9.051C6.21939 9.09485 5.65912 9.18628 5.11056 9.37034C4.41863 9.60256 3.79163 9.96387 3.33907 10.5061C2.89934 11.0331 2.56088 11.8063 2.56075 12.9924C2.56075 13.1083 2.55351 13.2694 2.50802 13.4348C2.48558 13.5162 2.34386 14.0279 1.78439 14.1965C1.18154 14.3778 0.775385 13.9725 0.706262 13.9006C0.580913 13.7702 0.500537 13.626 0.452356 13.5305C0.257874 13.1448 0.107806 12.5395 0.0402463 11.841C-0.0994489 10.3951 0.0801391 8.23145 1.2922 5.90647C2.38232 3.81585 4.09969 2.85246 5.51095 2.42209C5.94157 2.29082 6.34701 2.20931 6.70138 2.15745V1.36838ZM8.28341 1.04709C8.02513 0.904267 7.70193 1.08266 7.70138 1.36838V3.07346C6.71677 3.09168 3.72663 3.40026 2.17892 6.36838L1.98165 6.76584C0.053228 10.8548 1.56075 14.193 1.56075 12.9924C1.56118 8.17098 6.39327 7.98172 7.70138 8.01877V9.74045C7.70147 9.9908 7.94881 10.1585 8.1838 10.1008L8.28341 10.0627L13.883 5.80686C14.1412 5.66368 14.1413 5.30543 13.883 5.16233L8.28341 1.04709Z" />
3
3
  </svg>
4
+
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import { track } from '$lib/tracking'
3
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
4
+ import { t } from '$lib/localization'
5
+ import { heading } from '$lib/actions/heading'
6
+ import type { TitleData } from '$lib/types/title'
7
+ import Button from './Button.svelte'
8
+ import ContextMenu from './ContextMenu.svelte'
9
+ import IconNotify from './Icons/IconNotify.svelte'
10
+
11
+ interface Props {
12
+ title: TitleData
13
+ }
14
+
15
+ const { title }: Props = $props()
16
+ </script>
17
+
18
+ <div class="notify">
19
+ <div class="heading" use:heading={3}>{title.title} is currently not available to stream.</div>
20
+
21
+ <ContextMenu align="center">
22
+ {#snippet button({ toggle })}
23
+ <Button onclick={(event) => {
24
+ toggle(event)
25
+ track(TrackingEvent.NotifyTitle, title)
26
+ }} size="wide" label={t('Save')}>
27
+ <IconNotify /> <span class="label">Notify me when available</span>
28
+ </Button>
29
+ {/snippet}
30
+
31
+ <div class="content">
32
+ This feature is coming soon! Thanks for checking it out. We're still putting the finishing touches on it. Stay tuned!
33
+ </div>
34
+ </ContextMenu>
35
+ </div>
36
+
37
+ <style lang="scss">
38
+ .notify {
39
+ display: flex;
40
+ flex-direction: column;
41
+ align-items: center;
42
+ gap: margin(0.5);
43
+ }
44
+
45
+ .heading {
46
+ color: theme(text-color-alt);
47
+ text-align: center;
48
+ }
49
+
50
+ .content {
51
+ width: margin(14);
52
+ padding: margin(1);
53
+ margin: 0;
54
+ font-size: theme(font-size-small);
55
+ font-weight: theme(font-bold);
56
+ }
57
+
58
+ .label {
59
+ margin-left: margin(0.25);
60
+ font-size: theme(font-size-base);
61
+ font-weight: theme(font-bold);
62
+ }
63
+ </style>
@@ -9,6 +9,7 @@
9
9
  import { campaignToPlaylink, getFirstAdOfType } from '$lib/api/ads'
10
10
  import { getContext } from 'svelte'
11
11
  import Playlink from './Playlink.svelte'
12
+ import Notify from '../Notify.svelte'
12
13
 
13
14
  interface Props {
14
15
  playlinks: PlaylinkData[]
@@ -34,31 +35,28 @@
34
35
  }
35
36
  </script>
36
37
 
37
- <div class="heading" use:heading={3}>{t('Where To Stream Online')}</div>
38
38
 
39
- {#if playlinks.length}
39
+ {#if mergedPlaylinks.length}
40
+ <div class="heading" use:heading={3}>{t('Where To Stream Online')}</div>
41
+
40
42
  <div class="disclaimer" data-testid="commission-disclaimer">
41
43
  {window?.PlayPilotLinkInjections?.config?.playlinks_disclaimer_text || t('Commission Disclaimer')}
42
44
  <a href="https://playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>
43
45
  </div>
44
- {/if}
45
46
 
46
- <div class="playlinks" class:list bind:clientWidth={outerWidth}>
47
- {#each mergedPlaylinks as playlink, index}
48
- <Playlink {playlink} onclick={() => onclick(playlink.name)} />
47
+ <div class="playlinks" class:list bind:clientWidth={outerWidth}>
48
+ {#each mergedPlaylinks as playlink, index}
49
+ <Playlink {playlink} onclick={() => onclick(playlink.name)} />
49
50
 
50
- <!-- A fake highlighted playlink as part of the display ad, to be shown after the first playlink -->
51
- {#if displayAd && (index === 0)}
52
- <Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => track(TrackingEvent.DisplayedAdPlaylickClick, title, { campaign_name: displayAd.campaign_name })} hideCategory disclaimer={displayAd.disclaimer || ''} />
53
- {/if}
54
- {/each}
55
-
56
- {#if !mergedPlaylinks.length}
57
- <div class="empty" data-testid="playlinks-empty">
58
- {t('Title Unavailable')}
59
- </div>
60
- {/if}
61
- </div>
51
+ <!-- A fake highlighted playlink as part of the display ad, to be shown after the first playlink -->
52
+ {#if displayAd && (index === 0)}
53
+ <Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => track(TrackingEvent.DisplayedAdPlaylickClick, title, { campaign_name: displayAd.campaign_name })} hideCategory disclaimer={displayAd.disclaimer || ''} />
54
+ {/if}
55
+ {/each}
56
+ </div>
57
+ {:else}
58
+ <Notify {title} />
59
+ {/if}
62
60
 
63
61
  <style lang="scss">
64
62
  .heading {
@@ -102,19 +100,4 @@
102
100
  }
103
101
  }
104
102
  }
105
-
106
- .empty {
107
- grid-column: span 2;
108
- padding: margin(0.75);
109
- margin-top: margin(0.5);
110
- background: theme(playlink-background, lighter);
111
- box-shadow: theme(playlink-shadow, shadow);
112
- border-radius: theme(playlink-border-radius, border-radius);
113
- white-space: initial;
114
- line-height: 1.35;
115
-
116
- .list & {
117
- grid-column: 1;
118
- }
119
- }
120
103
  </style>
@@ -71,6 +71,7 @@
71
71
  width: 100%;
72
72
  aspect-ratio: 2 / 3;
73
73
  background: theme(detail-background-light, lighter);
74
+ text-decoration: none;
74
75
  }
75
76
 
76
77
  .heading {
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import { track } from '$lib/tracking'
3
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
4
+ import { t } from '$lib/localization'
5
+ import type { TitleData } from '$lib/types/title'
6
+ import Button from './Button.svelte'
7
+ import ContextMenu from './ContextMenu.svelte'
8
+ import IconSave from './Icons/IconSave.svelte'
9
+
10
+ interface Props {
11
+ title: TitleData
12
+ }
13
+
14
+ const { title }: Props = $props()
15
+ </script>
16
+
17
+ <ContextMenu>
18
+ {#snippet button({ toggle })}
19
+ <Button onclick={(event) => {
20
+ toggle(event)
21
+ track(TrackingEvent.SaveTitle, title)
22
+ }} size="wide" label={t('Save')}>
23
+ <IconSave />
24
+ </Button>
25
+ {/snippet}
26
+
27
+ <div class="content">
28
+ This feature is coming soon! Thanks for checking it out. We're still putting the finishing touches on it. Stay tuned!
29
+ </div>
30
+ </ContextMenu>
31
+
32
+ <style lang="scss">
33
+ .content {
34
+ width: margin(14);
35
+ padding: margin(1);
36
+ margin: 0;
37
+ font-size: theme(font-size-small);
38
+ font-weight: theme(font-bold);
39
+ }
40
+ </style>
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import { scale } from 'svelte/transition'
3
2
  import { mobileBreakpoint } from '$lib/constants'
4
3
  import { copyToClipboard } from '$lib/clipboard'
5
4
  import { track } from '$lib/tracking'
@@ -11,6 +10,7 @@
11
10
  import IconEmail from './Icons/IconEmail.svelte'
12
11
  import Button from './Button.svelte'
13
12
  import { onMount } from 'svelte'
13
+ import ContextMenu from './ContextMenu.svelte'
14
14
 
15
15
  interface Props {
16
16
  title: string
@@ -22,24 +22,18 @@
22
22
  const isMobile = window.innerWidth < mobileBreakpoint
23
23
  const useShareApi = isMobile && typeof navigator.share !== 'undefined'
24
24
 
25
- let showContextMenu = $state(false)
26
-
27
25
  onMount(() => {
28
26
  url = url + '?utm_source=tpi'
29
27
  })
30
28
 
31
- async function toggle(event: MouseEvent): Promise<void> {
32
- event.stopPropagation()
33
-
34
- if (useShareApi) {
35
- await navigator.share({ title, url })
36
-
37
- track(TrackingEvent.ShareTitle, null, { title, url: getFullUrlPath(), method: 'native' })
38
-
29
+ function shareApiOrToggle(event: MouseEvent, toggle: Function): void {
30
+ if (!useShareApi) {
31
+ toggle(event)
39
32
  return
40
33
  }
41
34
 
42
- showContextMenu = !showContextMenu
35
+ track(TrackingEvent.ShareTitle, null, { title, url: getFullUrlPath(), method: 'native' })
36
+ navigator.share({ title, url })
43
37
  }
44
38
 
45
39
  function copy(): void {
@@ -53,57 +47,25 @@
53
47
  }
54
48
  </script>
55
49
 
56
- <svelte:window onclick={() => showContextMenu = false} />
57
-
58
- <div class="share">
59
- <Button onclick={toggle}>
60
- <IconShare /> Share
61
- </Button>
50
+ <ContextMenu>
51
+ {#snippet button({ toggle })}
52
+ <Button onclick={(event) => shareApiOrToggle(event, toggle)} size="wide" label={t('Share')}>
53
+ <IconShare />
54
+ </Button>
55
+ {/snippet}
62
56
 
63
- {#if showContextMenu}
64
- <div class="context-menu" transition:scale={{ duration: 50, start: 0.85 }}>
65
- <button class="item" onclick={copy}>
66
- <IconLink /> {t('Copy URL')}
67
- </button>
57
+ <button class="item" onclick={copy}>
58
+ <IconLink /> {t('Copy URL')}
59
+ </button>
68
60
 
69
- <button class="item" onclick={email}>
70
- <IconEmail /> {t('Email')}
71
- </button>
72
- </div>
73
- {/if}
74
- </div>
61
+ <button class="item" onclick={email}>
62
+ <IconEmail /> {t('Email')}
63
+ </button>
64
+ </ContextMenu>
75
65
 
76
66
  <style lang="scss">
77
67
  $border-radius: theme(context-menu-border-radius, margin(0.5));
78
68
 
79
- .share {
80
- position: relative;
81
- }
82
-
83
- .context-menu {
84
- z-index: 10;
85
- position: absolute;
86
- bottom: calc(100% + margin(0.5));
87
- left: 0;
88
- max-width: margin(15);
89
- border-radius: $border-radius;
90
- background: theme(detail-background, lighter);
91
- box-shadow: theme(shadow);
92
-
93
- &::before {
94
- content: "";
95
- display: block;
96
- position: absolute;
97
- top: 0;
98
- right: 0;
99
- bottom: 0;
100
- left: 0;
101
- border-radius: $border-radius;
102
- background: theme(context-menu-background-lightness, rgba(255, 255, 255, 0.05));
103
- pointer-events: none;
104
- }
105
- }
106
-
107
69
  .item {
108
70
  cursor: pointer;
109
71
  appearance: none;
@@ -6,6 +6,7 @@
6
6
  import ParticipantsRail from './Rails/ParticipantsRail.svelte'
7
7
  import SimilarRail from './Rails/SimilarRail.svelte'
8
8
  import TitlePoster from './TitlePoster.svelte'
9
+ import Save from './Save.svelte'
9
10
  import Share from './Share.svelte'
10
11
  import Trailer from './Trailer.svelte'
11
12
  import { t } from '$lib/localization'
@@ -55,9 +56,13 @@
55
56
 
56
57
  <div class="actions">
57
58
  {#if title.embeddable_url}
58
- <Trailer title={title} />
59
- <Share title={title.title} url={titleUrl(title)} />
59
+ <Trailer {title} />
60
60
  {/if}
61
+
62
+ <div class="right">
63
+ <Save {title} />
64
+ <Share title={title.title} url={titleUrl(title)} />
65
+ </div>
61
66
  </div>
62
67
  </div>
63
68
 
@@ -182,7 +187,13 @@
182
187
  .actions {
183
188
  display: flex;
184
189
  gap: margin(0.5);
185
- margin-top: margin(0.5);
190
+ margin-top: margin(1);
191
+ }
192
+
193
+ .right {
194
+ display: flex;
195
+ gap: inherit;
196
+ margin-left: auto;
186
197
  }
187
198
 
188
199
  .background {
@@ -19,7 +19,6 @@
19
19
  }
20
20
  </script>
21
21
 
22
- <Button {onclick}>
22
+ <Button {onclick} size="wide" label={t('Watch Trailer')}>
23
23
  <IconPlay />
24
- {t('Watch Trailer')}
25
24
  </Button>
@@ -25,4 +25,10 @@ describe('Button.svelte', () => {
25
25
 
26
26
  expect(onclick).toHaveBeenCalled()
27
27
  })
28
+
29
+ it('Should include given label as aria-label', () => {
30
+ const { getByLabelText } = render(Button, { label: 'Some label' })
31
+
32
+ expect(getByLabelText('Some label')).toBeTruthy()
33
+ })
28
34
  })
@@ -8,7 +8,7 @@ describe('ContextMenu.svelte', () => {
8
8
  const children = createRawSnippet(() => ({ render: () => '<p>Some snippet</p>' }))
9
9
 
10
10
  it('Should open the context menu on click', async () => {
11
- const { getByRole, queryByText } = render(ContextMenu, { ariaLabel: '', children })
11
+ const { getByRole, queryByText } = render(ContextMenu, { label: '', children })
12
12
 
13
13
  expect(queryByText('Some snippet')).not.toBeTruthy()
14
14
 
@@ -18,7 +18,7 @@ describe('ContextMenu.svelte', () => {
18
18
  })
19
19
 
20
20
  it('Should close the context menu on second click', async () => {
21
- const { getByRole, queryByText } = render(ContextMenu, { ariaLabel: '', children })
21
+ const { getByRole, queryByText } = render(ContextMenu, { label: '', children })
22
22
 
23
23
  await fireEvent.click(getByRole('button'))
24
24
  await fireEvent.click(getByRole('button'))
@@ -27,11 +27,23 @@ describe('ContextMenu.svelte', () => {
27
27
  })
28
28
 
29
29
  it('Should close the context menu when clicking outside of the button', async () => {
30
- const { getByRole, queryByText } = render(ContextMenu, { ariaLabel: '', children })
30
+ const { getByRole, queryByText } = render(ContextMenu, { label: '', children })
31
31
 
32
32
  await fireEvent.click(getByRole('button'))
33
33
  await fireEvent.click(document.body)
34
34
 
35
35
  expect(queryByText('Some snippet')).not.toBeTruthy()
36
36
  })
37
+
38
+ it('Should include center class when align is set to center', async () => {
39
+ const { container } = render(ContextMenu, { align: 'center', children })
40
+
41
+ expect(container.querySelector('.context-menu')?.classList).toContain('center')
42
+ })
43
+
44
+ it('Should include right class when align is not set to center', async () => {
45
+ const { container } = render(ContextMenu, { children })
46
+
47
+ expect(container.querySelector('.context-menu')?.classList).toContain('right')
48
+ })
37
49
  })
@@ -63,12 +63,13 @@ describe('Playlinks.svelte', () => {
63
63
  expect(getByText('Some disclaimer')).toBeTruthy()
64
64
  })
65
65
 
66
- it('Should show empty state without commission disclaimer when no playlinks were given', () => {
66
+ it('Should show empty state without commission disclaimer when no playlinks are given', () => {
67
67
  /** @type {import('$lib/types/playlink').PlaylinkData[]} */
68
68
  const playlinks = []
69
- const { queryByTestId } = render(Playlinks, { playlinks, title })
69
+ const { queryByTestId, getByText } = render(Playlinks, { playlinks, title })
70
70
 
71
- expect(queryByTestId('playlinks-empty')).toBeTruthy()
71
+ expect(getByText(`${title.title} is currently not available to stream.`)).toBeTruthy()
72
+ expect(getByText('Notify me when available')).toBeTruthy()
72
73
  expect(queryByTestId('commission-disclaimer')).not.toBeTruthy()
73
74
  })
74
75