@playpilot/tpi 1.0.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.
Files changed (101) hide show
  1. package/.github/workflows/tests.yml +22 -0
  2. package/.prettierignore +4 -0
  3. package/.prettierrc +16 -0
  4. package/README.md +38 -0
  5. package/dist/link-injections.js +7 -0
  6. package/eslint.config.js +33 -0
  7. package/index.html +11 -0
  8. package/jsconfig.json +19 -0
  9. package/package.json +35 -0
  10. package/src/app.d.ts +13 -0
  11. package/src/app.html +12 -0
  12. package/src/demo.spec.js +7 -0
  13. package/src/lib/api.js +160 -0
  14. package/src/lib/array.js +15 -0
  15. package/src/lib/auth.js +84 -0
  16. package/src/lib/constants.js +2 -0
  17. package/src/lib/enums/TrackingEvent.js +15 -0
  18. package/src/lib/fakeData.js +140 -0
  19. package/src/lib/genres.json +420 -0
  20. package/src/lib/global.css +37 -0
  21. package/src/lib/hash.js +15 -0
  22. package/src/lib/html.js +21 -0
  23. package/src/lib/index.js +1 -0
  24. package/src/lib/linkInjection.js +275 -0
  25. package/src/lib/search.js +24 -0
  26. package/src/lib/text.js +61 -0
  27. package/src/lib/tracking.js +32 -0
  28. package/src/lib/variables.css +16 -0
  29. package/src/main.js +45 -0
  30. package/src/routes/+layout.svelte +54 -0
  31. package/src/routes/+page.svelte +96 -0
  32. package/src/routes/components/AfterArticlePlaylinks.svelte +90 -0
  33. package/src/routes/components/ContextMenu.svelte +67 -0
  34. package/src/routes/components/Description.svelte +47 -0
  35. package/src/routes/components/Editorial/Alert.svelte +18 -0
  36. package/src/routes/components/Editorial/DragHandle.svelte +134 -0
  37. package/src/routes/components/Editorial/Editor.svelte +277 -0
  38. package/src/routes/components/Editorial/EditorItem.svelte +260 -0
  39. package/src/routes/components/Editorial/ManualInjection.svelte +192 -0
  40. package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +132 -0
  41. package/src/routes/components/Editorial/Search/TitleSearch.svelte +176 -0
  42. package/src/routes/components/Editorial/Switch.svelte +76 -0
  43. package/src/routes/components/Editorial/TextInput.svelte +29 -0
  44. package/src/routes/components/Genres.svelte +41 -0
  45. package/src/routes/components/Icons/IconAlign.svelte +12 -0
  46. package/src/routes/components/Icons/IconBack.svelte +3 -0
  47. package/src/routes/components/Icons/IconBookmark.svelte +3 -0
  48. package/src/routes/components/Icons/IconChevron.svelte +18 -0
  49. package/src/routes/components/Icons/IconClose.svelte +3 -0
  50. package/src/routes/components/Icons/IconContinue.svelte +3 -0
  51. package/src/routes/components/Icons/IconDots.svelte +5 -0
  52. package/src/routes/components/Icons/IconEnlarge.svelte +12 -0
  53. package/src/routes/components/Icons/IconIMDb.svelte +3 -0
  54. package/src/routes/components/Icons/IconNewTab.svelte +3 -0
  55. package/src/routes/components/Modal.svelte +106 -0
  56. package/src/routes/components/Participants.svelte +44 -0
  57. package/src/routes/components/Playlinks.svelte +155 -0
  58. package/src/routes/components/Popover.svelte +95 -0
  59. package/src/routes/components/RoundButton.svelte +38 -0
  60. package/src/routes/components/SkeletonText.svelte +33 -0
  61. package/src/routes/components/Title.svelte +180 -0
  62. package/src/routes/components/TitleModal.svelte +24 -0
  63. package/src/routes/components/TitlePopover.svelte +17 -0
  64. package/src/tests/helpers.js +18 -0
  65. package/src/tests/lib/api.test.js +162 -0
  66. package/src/tests/lib/array.test.js +14 -0
  67. package/src/tests/lib/auth.test.js +115 -0
  68. package/src/tests/lib/hash.test.js +28 -0
  69. package/src/tests/lib/html.test.js +16 -0
  70. package/src/tests/lib/linkInjection.test.js +754 -0
  71. package/src/tests/lib/search.test.js +42 -0
  72. package/src/tests/lib/text.test.js +94 -0
  73. package/src/tests/lib/tracking.test.js +71 -0
  74. package/src/tests/routes/+page.test.js +109 -0
  75. package/src/tests/routes/components/AfterArticlePlaylinks.test.js +115 -0
  76. package/src/tests/routes/components/ContextMenu.test.js +37 -0
  77. package/src/tests/routes/components/Description.test.js +58 -0
  78. package/src/tests/routes/components/Editorial/Alert.test.js +17 -0
  79. package/src/tests/routes/components/Editorial/DragHandle.test.js +55 -0
  80. package/src/tests/routes/components/Editorial/Editor.test.js +64 -0
  81. package/src/tests/routes/components/Editorial/EditorItem.test.js +142 -0
  82. package/src/tests/routes/components/Editorial/ManualInjection.test.js +114 -0
  83. package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +63 -0
  84. package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +58 -0
  85. package/src/tests/routes/components/Editorial/Switch.test.js +60 -0
  86. package/src/tests/routes/components/Editorial/TextInput.test.js +30 -0
  87. package/src/tests/routes/components/Genres.test.js +37 -0
  88. package/src/tests/routes/components/Modal.test.js +84 -0
  89. package/src/tests/routes/components/Participants.test.js +33 -0
  90. package/src/tests/routes/components/Playlinks.test.js +101 -0
  91. package/src/tests/routes/components/Popover.test.js +66 -0
  92. package/src/tests/routes/components/RoundButton.test.js +35 -0
  93. package/src/tests/routes/components/SkeletonText.test.js +12 -0
  94. package/src/tests/routes/components/Title.test.js +82 -0
  95. package/src/tests/routes/components/TitleModal.test.js +33 -0
  96. package/src/tests/routes/components/TitlePopover.test.js +23 -0
  97. package/src/tests/setup.js +53 -0
  98. package/src/typedefs.js +72 -0
  99. package/static/favicon.png +0 -0
  100. package/svelte.config.js +13 -0
  101. package/vite.config.js +61 -0
@@ -0,0 +1,176 @@
1
+ <script>
2
+ import { playPilotBaseUrl } from '$lib/constants'
3
+ import { searchTitles } from '$lib/search'
4
+ import IconIMDb from '../../Icons/IconIMDb.svelte'
5
+ import IconNewTab from '../../Icons/IconNewTab.svelte'
6
+ import TextInput from '../TextInput.svelte'
7
+
8
+ /** @type {{ onselect?: (title: TitleData) => void, query: string }} */
9
+ let { onselect = () => null, query = $bindable() } = $props()
10
+
11
+ /** @type {TitleData[]} */
12
+ let results = $state([])
13
+ /** @type {TitleData | null} */
14
+ let selectedResult = $state(null)
15
+ let loading = $state(false)
16
+
17
+ $effect(() => {
18
+ if (query) search(query)
19
+ })
20
+
21
+ /**
22
+ * @param {string} query
23
+ * @returns {Promise<void>}
24
+ */
25
+ async function search(query) {
26
+ loading = true
27
+ selectedResult = null
28
+
29
+ try {
30
+ results = await searchTitles(query)
31
+ } finally {
32
+ loading = false
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @param {TitleData} title
38
+ * @returns {void}
39
+ */
40
+ function select(title) {
41
+ query = title.title
42
+ selectedResult = title
43
+
44
+ onselect(title)
45
+ }
46
+ </script>
47
+
48
+ <div class="search">
49
+ <TextInput
50
+ bind:value={query}
51
+ name="search"
52
+ label="Search..." />
53
+
54
+ {#if query && !selectedResult}
55
+ <div class="results">
56
+ {#each results as title (title.sid)}
57
+ <button class="item" onclick={() => select(title)}>
58
+ <img class="poster" src={title.standing_poster} alt="" width="28" height="42" />
59
+
60
+ <div class="content">
61
+ <div class="name">{title.title}</div>
62
+
63
+ <div class="meta">
64
+ <div class="imdb">
65
+ <IconIMDb />
66
+ {title.imdb_score}
67
+ </div>
68
+
69
+ <div>{title.year}</div>
70
+ <div>{title.type}</div>
71
+
72
+ {#if title.length}
73
+ <div>{title.length} min</div>
74
+ {/if}
75
+ </div>
76
+ </div>
77
+
78
+ <a
79
+ href="{playPilotBaseUrl}/{title.type}/{title.slug}"
80
+ target="_blank"
81
+ class="open-in-new-tab"
82
+ onclick={event => event.stopImmediatePropagation()}>
83
+ <IconNewTab />
84
+ </a>
85
+ </button>
86
+ {:else}
87
+ {#if !loading}
88
+ <em class="empty">No results found</em>
89
+ {/if}
90
+ {/each}
91
+ </div>
92
+ {/if}
93
+ </div>
94
+
95
+ <style>
96
+ .search {
97
+ position: relative;
98
+ }
99
+
100
+ .results {
101
+ z-index: 1;
102
+ position: absolute;
103
+ top: calc(100% + 0.5rem);
104
+ left: 0;
105
+ right: 0;
106
+ height: 30vh;
107
+ max-height: 10rem;
108
+ border-radius: 0.5rem;
109
+ background: var(--playpilot-light);
110
+ scrollbar-width: thin;
111
+ overflow: auto;
112
+ }
113
+
114
+ .item {
115
+ cursor: pointer;
116
+ appearance: none;
117
+ display: flex;
118
+ align-items: flex-start;
119
+ gap: 1rem;
120
+ width: 100%;
121
+ background: transparent;
122
+ border: 0;
123
+ padding: 0.5rem;
124
+ border-bottom: 1px solid var(--playpilot-content);
125
+ color: var(--playpilot-text-color-alt);
126
+ font-family: inherit;
127
+ text-align: left;
128
+ }
129
+
130
+ .item:hover {
131
+ background: var(--playpilot-lighter);
132
+ }
133
+
134
+ .item:last-child {
135
+ border-bottom: 0;
136
+ }
137
+
138
+ .poster {
139
+ width: 1.75rem;
140
+ border-radius: 0.25rem;
141
+ height: auto;
142
+ background: var(--playpilot-dark);
143
+ }
144
+
145
+ .name {
146
+ color: var(--playpilot-text-color);
147
+ }
148
+
149
+ .meta {
150
+ display: flex;
151
+ flex-wrap: wrap;
152
+ gap: 0 0.5rem;
153
+ font-size: 0.75rem;
154
+ }
155
+
156
+ .imdb {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 0.25rem;
160
+ }
161
+
162
+ .empty {
163
+ padding: 0.5rem;
164
+ font-size: 0.75rem;
165
+ color: var(--playpilot-text-color-alt);
166
+ }
167
+
168
+ .open-in-new-tab {
169
+ margin-left: auto;
170
+ color: var(--playpilot-text-color-alt);
171
+ }
172
+
173
+ .open-in-new-tab:hover {
174
+ color: vaR(--playpilot-text-color);
175
+ }
176
+ </style>
@@ -0,0 +1,76 @@
1
+ <script>
2
+ /** @type {{ active: boolean, fullwidth?: boolean, label?: string, onclick?: (active: boolean) => void, children: import('svelte').Snippet }} */
3
+ let { active = $bindable(), fullwidth = false, label = '', onclick = () => null, children } = $props()
4
+
5
+ function toggle() {
6
+ active = !active
7
+ onclick(active)
8
+ }
9
+ </script>
10
+
11
+ <button class="switch" class:active class:fullwidth onclick={toggle} aria-label="Toggle {label} {active ? 'off' : 'on'}">
12
+ <div class="label">{@render children()}</div>
13
+
14
+ <div class="track">
15
+ <div class="knob"></div>
16
+ </div>
17
+ </button>
18
+
19
+ <style>
20
+ .switch {
21
+ appearance: none;
22
+ display: flex;
23
+ align-items: center;
24
+ border: 0;
25
+ padding: 0;
26
+ background: transparent;
27
+ cursor: pointer;
28
+ }
29
+
30
+ .fullwidth {
31
+ width: 100%;
32
+ justify-content: space-between;
33
+ }
34
+
35
+ .label {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 0.5rem;
39
+ margin-right: 0.5rem;
40
+ font-family: var(--playpilot-font-family);
41
+ color: var(--playpilot-text-color-alt);
42
+ font-size: 0.75rem;
43
+ }
44
+
45
+ .switch:hover .label {
46
+ color: var(--playpilot-text-color);
47
+ }
48
+
49
+ .track {
50
+ position: relative;
51
+ width: 2rem;
52
+ height: 1.25rem;
53
+ border-radius: 1rem;
54
+ background: var(--playpilot-content);
55
+ transition: background-color 100ms;
56
+ }
57
+
58
+ .active .track {
59
+ background: var(--playpilot-green);
60
+ }
61
+
62
+ .knob {
63
+ position: absolute;
64
+ top: 0.125rem;
65
+ left: 0.125rem;
66
+ height: 1rem;
67
+ width: 1rem;
68
+ border-radius: 50%;
69
+ background: white;
70
+ transition: left 100ms;
71
+ }
72
+
73
+ .active .knob {
74
+ left: 0.875rem;
75
+ }
76
+ </style>
@@ -0,0 +1,29 @@
1
+ <script>
2
+ /** @type {{ value: string, label?: string, name?: string, readonly?: boolean, oninput?: () => void }} */
3
+ let { value = $bindable(), label = '', name = '', readonly = false, oninput = () => null } = $props()
4
+ </script>
5
+
6
+ <input type="text" bind:value {name} aria-label={label} placeholder={label} {readonly} {oninput} />
7
+
8
+ <style>
9
+ input {
10
+ width: 100%;
11
+ padding: 0.5rem 1rem;
12
+ border: 0;
13
+ border-radius: 2rem;
14
+ background: var(--playpilot-light);
15
+ color: var(--playpilot-text-color-alt);
16
+ font-size: 0.75rem;
17
+ font-family: var(--playpilot-font-family);
18
+ }
19
+
20
+ input:focus {
21
+ background: var(--playpilot-lighter);
22
+ outline: 1px solid white;
23
+ }
24
+
25
+ input::placeholder {
26
+ color: var(--playpilot-text-color-alt);
27
+ opacity: 0.75;
28
+ }
29
+ </style>
@@ -0,0 +1,41 @@
1
+ <script>
2
+ import genreData from '$lib/genres.json'
3
+
4
+ /** @type {{ genres: string[] }} */
5
+ const { genres } = $props()
6
+
7
+ let expanded = $state(false)
8
+ let shownGenres = $derived(expanded ? genres : [genres[0]])
9
+ </script>
10
+
11
+ {#each shownGenres as genre}
12
+ <div class="genre">
13
+ {genreData.find(g => g.slug === genre)?.name}
14
+ </div>
15
+ {/each}
16
+
17
+ {#if !expanded && genres.length > 1}
18
+ <button class="genre expand" onclick={() => expanded = true}>+{genres.length - 1}</button>
19
+ {/if}
20
+
21
+ <style>
22
+ .genre {
23
+ display: flex;
24
+ flex-wrap: wrap;
25
+ background: var(--playpilot-genre-background, var(--playpilot-content));
26
+ border: 0;
27
+ border-radius: var(--playpilot-genre-border-radius, 1rem);
28
+ padding: 0.25rem 0.5rem;
29
+ font-family: var(--playpilot-genre-font-family, inherit);
30
+ text-transform: var(--playpilot-genre-text-transform, none);
31
+ color: var(--playpilot-genre-text-color, var(--playpilot-text-color-alt));
32
+ line-height: 1;
33
+ font-size: inherit;
34
+ }
35
+
36
+ button.genre:hover {
37
+ cursor: pointer;
38
+ filter: var(--playpilot-genre-hover-filter, brightness(1.1));
39
+ background: var(--playpilot-genre-hover-background, var(--playpilot-genre-background));
40
+ }
41
+ </style>
@@ -0,0 +1,12 @@
1
+ <script>
2
+ /** @type {{ align?: Alignment }} */
3
+ let { align = 'bottom' } = $props()
4
+ </script>
5
+
6
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
7
+ {#if align === 'bottom'}
8
+ <path d="M13.5833 13.6667L4.41667 13.6667M5.5 1.5H12.5C13.9001 1.5 14.6002 1.5 15.135 1.77248C15.6054 2.01217 15.9878 2.39462 16.2275 2.86502C16.5 3.3998 16.5 4.09987 16.5 5.5V12.5C16.5 13.9001 16.5 14.6002 16.2275 15.135C15.9878 15.6054 15.6054 15.9878 15.135 16.2275C14.6002 16.5 13.9001 16.5 12.5 16.5H5.5C4.09987 16.5 3.3998 16.5 2.86502 16.2275C2.39462 15.9878 2.01217 15.6054 1.77248 15.135C1.5 14.6002 1.5 13.9001 1.5 12.5V5.5C1.5 4.09987 1.5 3.3998 1.77248 2.86502C2.01217 2.39462 2.39462 2.01217 2.86502 1.77248C3.3998 1.5 4.09987 1.5 5.5 1.5Z" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
9
+ {:else if align === 'center'}
10
+ <path d="M13.5833 8.58342H4.41667M5.5 1.50009H12.5C13.9001 1.50009 14.6002 1.50009 15.135 1.77258C15.6054 2.01226 15.9878 2.39471 16.2275 2.86512C16.5 3.39989 16.5 4.09996 16.5 5.50009V12.5001C16.5 13.9002 16.5 14.6003 16.2275 15.1351C15.9878 15.6055 15.6054 15.9879 15.135 16.2276C14.6002 16.5001 13.9001 16.5001 12.5 16.5001H5.5C4.09987 16.5001 3.3998 16.5001 2.86502 16.2276C2.39462 15.9879 2.01217 15.6055 1.77248 15.1351C1.5 14.6003 1.5 13.9002 1.5 12.5001V5.50009C1.5 4.09996 1.5 3.39989 1.77248 2.86512C2.01217 2.39471 2.39462 2.01226 2.86502 1.77258C3.3998 1.50009 4.09987 1.50009 5.5 1.50009Z" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
11
+ {/if}
12
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="4" height="8" viewBox="0 0 4 8" fill="none">
2
+ <path d="M3.5 7L0.5 4L3.5 1" stroke="currentcolor" stroke-width="0.9975" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
2
+ <path d="M3.33334 5.2C3.33334 4.0799 3.33334 3.51984 3.55132 3.09202C3.74307 2.71569 4.04903 2.40973 4.42535 2.21799C4.85318 2 5.41323 2 6.53334 2H9.46667C10.5868 2 11.1468 2 11.5747 2.21799C11.951 2.40973 12.2569 2.71569 12.4487 3.09202C12.6667 3.51984 12.6667 4.0799 12.6667 5.2V14L8 11.3333L3.33334 14V5.2Z" stroke="var(--playpilot-button-text-color, var(--playpilot-text-color-alt))" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,18 @@
1
+ <script>
2
+ /** @type {{ expanded: boolean }} */
3
+ const { expanded = false } = $props()
4
+ </script>
5
+
6
+ <svg width="8" height="4" viewBox="0 0 8 4" fill="none" class:expanded>
7
+ <path d="M1 0.5L4 3.5L7 0.5" stroke="currentColor" stroke-width="0.9975" stroke-linecap="round" stroke-linejoin="round"/>
8
+ </svg>
9
+
10
+ <style>
11
+ svg {
12
+ transition: transform 100ms;
13
+ }
14
+
15
+ .expanded {
16
+ transform: rotate(180deg);
17
+ }
18
+ </style>
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
2
+ <path d="M14 2L2 14M2 2L14 14" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="6" height="12" viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0 11L5 6L0 1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M9.99984 10.8333C10.4601 10.8333 10.8332 10.4602 10.8332 10C10.8332 9.53977 10.4601 9.16668 9.99984 9.16668C9.5396 9.16668 9.1665 9.53977 9.1665 10C9.1665 10.4602 9.5396 10.8333 9.99984 10.8333Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
3
+ <path d="M9.99984 5.00001C10.4601 5.00001 10.8332 4.62691 10.8332 4.16668C10.8332 3.70644 10.4601 3.33334 9.99984 3.33334C9.5396 3.33334 9.1665 3.70644 9.1665 4.16668C9.1665 4.62691 9.5396 5.00001 9.99984 5.00001Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M9.99984 16.6667C10.4601 16.6667 10.8332 16.2936 10.8332 15.8333C10.8332 15.3731 10.4601 15 9.99984 15C9.5396 15 9.1665 15.3731 9.1665 15.8333C9.1665 16.2936 9.5396 16.6667 9.99984 16.6667Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
5
+ </svg>
@@ -0,0 +1,12 @@
1
+ <script>
2
+ /** @type {{ expanded: boolean }} */
3
+ const { expanded = false } = $props()
4
+ </script>
5
+
6
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
7
+ {#if expanded}
8
+ <path d="M2.5 6.66661H2.66667C4.0668 6.66661 4.76686 6.66661 5.30164 6.39412C5.77205 6.15444 6.1545 5.77199 6.39418 5.30158C6.66667 4.7668 6.66667 4.06674 6.66667 2.66661V2.49994M2.5 13.3333H2.66667C4.0668 13.3333 4.76686 13.3333 5.30164 13.6058C5.77205 13.8454 6.1545 14.2279 6.39418 14.6983C6.66667 15.2331 6.66667 15.9331 6.66667 17.3333V17.4999M13.3333 2.49994V2.66661C13.3333 4.06674 13.3333 4.7668 13.6058 5.30158C13.8455 5.77199 14.228 6.15444 14.6984 6.39412C15.2331 6.66661 15.9332 6.66661 17.3333 6.66661H17.5M13.3333 17.4999V17.3333C13.3333 15.9331 13.3333 15.2331 13.6058 14.6983C13.8455 14.2279 14.228 13.8454 14.6984 13.6058C15.2331 13.3333 15.9332 13.3333 17.3333 13.3333H17.5" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
9
+ {:else}
10
+ <path d="M6.66667 2.5H6.5C5.09987 2.5 4.3998 2.5 3.86502 2.77248C3.39462 3.01217 3.01217 3.39462 2.77248 3.86502C2.5 4.3998 2.5 5.09987 2.5 6.5V6.66667M6.66667 17.5H6.5C5.09987 17.5 4.3998 17.5 3.86502 17.2275C3.39462 16.9878 3.01217 16.6054 2.77248 16.135C2.5 15.6002 2.5 14.9001 2.5 13.5V13.3333M17.5 6.66667V6.5C17.5 5.09987 17.5 4.3998 17.2275 3.86502C16.9878 3.39462 16.6054 3.01217 16.135 2.77248C15.6002 2.5 14.9001 2.5 13.5 2.5H13.3333M17.5 13.3333V13.5C17.5 14.9001 17.5 15.6002 17.2275 16.135C16.9878 16.6054 16.6054 16.9878 16.135 17.2275C15.6002 17.5 14.9001 17.5 13.5 17.5H13.3333" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
11
+ {/if}
12
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M7 0.351929L9.163 4.72803L14 5.43408L10.5 8.8385L11.326 13.648L7 11.3761L2.674 13.648L3.5 8.8385L0 5.43408L4.837 4.72803L7 0.351929Z" fill="#F6C045"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 -960 960 960" width="18" height="18">
2
+ <path fill="currentColor" d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"/>
3
+ </svg>
@@ -0,0 +1,106 @@
1
+ <script>
2
+ import { fade, fly, scale } from 'svelte/transition'
3
+ import IconClose from './Icons/IconClose.svelte'
4
+ import RoundButton from './RoundButton.svelte'
5
+ import { onMount, setContext } from 'svelte'
6
+
7
+ /** @type {{ children: import('svelte').Snippet, onclose?: () => void, onscroll?: () => void }} */
8
+ const { children, onclose = () => null, onscroll = () => null } = $props()
9
+
10
+ setContext('scope', 'modal')
11
+
12
+ onMount(() => {
13
+ const baseOverflowStyle = document.body.style.overflowY
14
+ document.body.style.overflowY = 'hidden'
15
+
16
+ return () => document.body.style.overflowY = baseOverflowStyle || ''
17
+ })
18
+
19
+ /**
20
+ * @param {Element} node
21
+ * @returns {import('svelte/transition').TransitionConfig}
22
+ */
23
+ function scaleOrFly(node) {
24
+ const shouldFly = window.innerWidth < 600
25
+
26
+ if (shouldFly) return fly(node, { duration: 250, y: window.innerHeight })
27
+ return scale(node, { duration: 150, start: 0.85 })
28
+ }
29
+ </script>
30
+
31
+ <svelte:window on:keydown={({ key }) => { if (key === 'Escape') onclose() }} />
32
+
33
+ <div class="modal" transition:fade={{ duration: 150 }}>
34
+ <div class="dialog" {onscroll} role="dialog" transition:scaleOrFly>
35
+ <div class="close">
36
+ <RoundButton onclick={() => onclose()}>
37
+ <IconClose />
38
+ </RoundButton>
39
+ </div>
40
+
41
+ {@render children()}
42
+ </div>
43
+
44
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
45
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
46
+ <div class="backdrop" onclick={() => onclose()}></div>
47
+ </div>
48
+
49
+ <style>
50
+ .modal {
51
+ z-index: 9999;
52
+ position: fixed;
53
+ display: flex;
54
+ justify-content: center;
55
+ align-items: flex-start;
56
+ top: 0;
57
+ left: 0;
58
+ width: 100%;
59
+ height: 100%;
60
+ background: var(--playpilot-detail-backdrop, rgba(0, 0, 0, 0.65));
61
+ }
62
+
63
+ @media (min-width: 600px) {
64
+ .modal {
65
+ padding: 2rem;
66
+ }
67
+ }
68
+
69
+ .dialog {
70
+ position: relative;
71
+ width: 100%;
72
+ max-width: 600px;
73
+ max-height: 80vh;
74
+ overflow: auto;
75
+ margin-top: auto;
76
+ border-radius: var(--playpilot-detail-border-radius, 1rem 1rem 0 0);
77
+ background: var(--playpilot-detail-background, var(--playpilot-light));
78
+ }
79
+
80
+ @media (min-width: 600px) {
81
+ .dialog {
82
+ margin-top: 0;
83
+ border-radius: var(--playpilot-detail-border-radius, 1rem);
84
+ max-height: 100%;
85
+ }
86
+ }
87
+
88
+ .backdrop {
89
+ position: absolute;
90
+ top: 0;
91
+ right: 0;
92
+ bottom: 0;
93
+ left: 0;
94
+ }
95
+
96
+ .close {
97
+ z-index: 5;
98
+ position: absolute;
99
+ top: 1rem;
100
+ right: 1rem;
101
+ }
102
+
103
+ .close:hover {
104
+ filter: brightness(1.1);
105
+ }
106
+ </style>
@@ -0,0 +1,44 @@
1
+ <script>
2
+ /** @type {{ participants: Participant[] }} */
3
+ const { participants } = $props()
4
+ </script>
5
+
6
+ <h2>Cast</h2>
7
+
8
+ <div class="participants">
9
+ {#each participants as participant}
10
+ <div class="participant">
11
+ {participant.name}
12
+
13
+ <div class="character">{participant.character?.slice(0, 20) || ''}{#if participant.character?.length || 0 > 20}...{/if}</div>
14
+ </div>
15
+ {/each}
16
+ </div>
17
+
18
+ <style>
19
+ h2 {
20
+ margin: 1.5rem 0 1rem;
21
+ font-size: var(--playpilot-cast-title-font-size, 18px);
22
+ font-weight: inherit;
23
+ }
24
+
25
+ .participants {
26
+ display: flex;
27
+ gap: 1rem;
28
+ width: calc(100% + 2rem);
29
+ padding: 0 1rem;
30
+ margin: 0 -1rem;
31
+ overflow: auto;
32
+ white-space: nowrap;
33
+ scrollbar-width: none;
34
+ }
35
+
36
+ .participants::-webkit-scrollbar {
37
+ display: none;
38
+ }
39
+
40
+ .character {
41
+ color: var(--playpilot-cast-character-text-color, var(--playpilot-text-color-alt));
42
+ font-style: italic;
43
+ }
44
+ </style>