@playpilot/tpi 8.29.11 → 8.31.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 (32) hide show
  1. package/dist/editorial.mount.js +9 -9
  2. package/dist/link-injections.js +1 -1
  3. package/dist/mount.js +7 -7
  4. package/package.json +1 -1
  5. package/src/lib/api/externalPages.ts +1 -0
  6. package/src/lib/api/participants.ts +1 -1
  7. package/src/lib/api/providers.ts +1 -1
  8. package/src/lib/api/search.ts +9 -0
  9. package/src/lib/api/titles.ts +1 -1
  10. package/src/lib/injection.ts +479 -479
  11. package/src/lib/types/global.d.ts +1 -0
  12. package/src/routes/components/Editorial/Editor.svelte +0 -2
  13. package/src/routes/components/Editorial/EditorItem.svelte +2 -2
  14. package/src/routes/components/Editorial/ManualInjection.svelte +26 -19
  15. package/src/routes/components/Editorial/Search/ParticipantSearchItem.svelte +86 -0
  16. package/src/routes/components/Editorial/Search/TypeSearch.svelte +120 -0
  17. package/src/routes/components/Explore/Routes/ExploreModal.svelte +107 -107
  18. package/src/routes/components/Modals/TitlesReelModal.svelte +3 -3
  19. package/src/routes/components/Rails/TitlesRail.svelte +1 -1
  20. package/src/routes/components/Tabs.svelte +12 -23
  21. package/src/routes/components/YouTubeEmbed.svelte +61 -20
  22. package/src/routes/components/YouTubeEmbedBackground.svelte +1 -0
  23. package/src/routes/components/YouTubeEmbedOverlay.svelte +1 -1
  24. package/src/routes/elements/+page.svelte +12 -0
  25. package/src/tests/lib/api/externalPages.test.js +134 -1
  26. package/src/tests/routes/components/Editorial/EditorItem.test.js +1 -2
  27. package/src/tests/routes/components/Editorial/ManualInjection.test.js +43 -6
  28. package/src/tests/routes/components/Editorial/Search/{TitleSearch.test.js → TypeSearch.test.js} +21 -9
  29. package/src/tests/routes/components/YouTubeEmbed.test.js +109 -33
  30. package/src/tests/routes/components/YouTubeEmbedBackground.test.js +2 -3
  31. package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +2 -3
  32. package/src/routes/components/Editorial/Search/TitleSearch.svelte +0 -96
@@ -12,6 +12,7 @@ declare global {
12
12
  mount(): void
13
13
  destroy(): void
14
14
  }
15
+ Plyr: any
15
16
  }
16
17
  }
17
18
 
@@ -45,8 +45,6 @@
45
45
  const position: Position = JSON.parse(localStorage.getItem(editorPositionKey) || '{ "x": 0, "y": 0 }')
46
46
  const height: number = parseInt(localStorage.getItem(editorHeightKey) || '0')
47
47
 
48
-
49
-
50
48
  let editorElement: HTMLElement | null = $state(null)
51
49
  let manualInjectionActive = $state(false)
52
50
  let saving = $state(false)
@@ -113,6 +113,8 @@
113
113
  <div class="header">
114
114
  {#if type === 'title'}
115
115
  <img class="poster" src={removeImageUrlPrefix(title_details!.standing_poster)} alt="" width="32" height="48" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
116
+ {:else if type === 'participant'}
117
+ <img class="poster participant" src={participant_details!.image} alt="" width="32" height="32" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
116
118
  {:else}
117
119
  <div class="placeholder-image"></div>
118
120
  {/if}
@@ -207,7 +209,6 @@
207
209
  .poster {
208
210
  display: block;
209
211
  width: margin(2);
210
- height: margin(3);
211
212
  border-radius: margin(0.25);
212
213
  background: theme(content);
213
214
 
@@ -355,4 +356,3 @@
355
356
  }
356
357
  }
357
358
  </style>
358
-
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { LinkInjection } from '$lib/types/injection'
2
+ import type { LinkInjection, LinkInjectionDataType } from '$lib/types/injection'
3
3
  import type { TitleData } from '$lib/types/title'
4
4
  import { onMount } from 'svelte'
5
5
  import { playPilotBaseUrl } from '$lib/constants'
@@ -12,7 +12,9 @@
12
12
  import RoundButton from '../RoundButton.svelte'
13
13
  import Alert from './Alert.svelte'
14
14
  import TextInput from './TextInput.svelte'
15
- import TitleSearch from './Search/TitleSearch.svelte'
15
+ import TypeSearch from './Search/TypeSearch.svelte'
16
+ import type { ParticipantData } from '$lib/types/participant'
17
+ import { participantUrl, titleUrl } from '$lib/routes'
16
18
 
17
19
  interface Props {
18
20
  onsave: (linkInjection: LinkInjection) => void
@@ -25,7 +27,8 @@
25
27
 
26
28
  let currentSelection = $state('')
27
29
  let selectionSentence = $state('')
28
- let selectedTitle: TitleData | null = $state(null)
30
+ let selectedItem: TitleData | ParticipantData | null = $state(null)
31
+ let selectedType: LinkInjectionDataType = $state('title')
29
32
  let phraseAfter = $state('')
30
33
  let phraseBefore = $state('')
31
34
  let error = $state('')
@@ -145,8 +148,9 @@
145
148
  return total
146
149
  }
147
150
 
148
- function onselect(title: TitleData): void {
149
- selectedTitle = title
151
+ function onselect(title: TitleData | ParticipantData, type: LinkInjectionDataType): void {
152
+ selectedItem = title
153
+ selectedType = type
150
154
 
151
155
  // Remove selection that was made anywhere on the page. This is to avoid the input from being overwritten
152
156
  // by the selection right after selecting a result.
@@ -155,25 +159,28 @@
155
159
 
156
160
  function save(): void {
157
161
  if (!currentSelection) return
158
- if (!selectedTitle) return
162
+ if (!selectedItem) return
159
163
 
160
- const typePath = selectedTitle.type === 'movie' ? 'movie' : 'show'
161
- const url = playPilotBaseUrl + `/${typePath}/${selectedTitle.slug}`
162
-
163
- const linkInjection: LinkInjection = {
164
- sid: selectedTitle.sid,
164
+ const linkInjection: Partial<LinkInjection> = {
165
+ sid: selectedItem.sid,
165
166
  title: currentSelection,
166
167
  sentence: selectionSentence,
167
168
  phrase_before: phraseBefore,
168
169
  phrase_after: phraseAfter,
169
- playpilot_url: url,
170
- key: generateInjectionKey(selectedTitle.sid),
171
- title_details: selectedTitle,
170
+ key: generateInjectionKey(selectedItem.sid),
172
171
  manual: true,
173
- type: 'title',
172
+ type: selectedType,
173
+ }
174
+
175
+ if (selectedType === 'title') {
176
+ linkInjection.title_details = selectedItem as TitleData
177
+ linkInjection.playpilot_url = titleUrl(selectedItem as TitleData)
178
+ } else if (selectedType === 'participant') {
179
+ linkInjection.participant_details = selectedItem as ParticipantData
180
+ linkInjection.playpilot_url = participantUrl(selectedItem as ParticipantData)
174
181
  }
175
182
 
176
- onsave(linkInjection)
183
+ onsave(linkInjection as LinkInjection)
177
184
  onclose()
178
185
  }
179
186
  </script>
@@ -198,13 +205,13 @@
198
205
  {/if}
199
206
 
200
207
  {#if !error}
201
- <label for="text">Paired title</label>
208
+ <label for="text">Paired link</label>
202
209
  {#key selectionSentence + currentSelection}
203
- <TitleSearch {onselect} bind:query />
210
+ <TypeSearch {onselect} bind:query />
204
211
  {/key}
205
212
  {/if}
206
213
 
207
- <button class="save" onclick={save} disabled={!currentSelection || !selectedTitle}>
214
+ <button class="save" onclick={save} disabled={!currentSelection || !selectedItem}>
208
215
  Add playlink
209
216
  </button>
210
217
  </section>
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ // In many parts this is a copy of ./TitleSearchItem.svelte
3
+ // Perhaps worth unifying the two in some sense, but for now I decided to keep it simple.
4
+
5
+ import { imagePlaceholderDataUrl } from '$lib/constants'
6
+ import { removeImageUrlPrefix } from '$lib/image'
7
+ import { participantUrl } from '$lib/routes'
8
+ import type { ParticipantData } from '$lib/types/participant'
9
+ import IconNewTab from '../../Icons/IconNewTab.svelte'
10
+
11
+ interface Props {
12
+ participant: ParticipantData
13
+ hoverable?: boolean
14
+ onclick?: () => void
15
+ }
16
+
17
+ const { participant, hoverable = true, onclick = () => null }: Props = $props()
18
+ </script>
19
+
20
+ <button class="item" class:hoverable {onclick}>
21
+ <img class="image" src={participant.image || imagePlaceholderDataUrl} alt="" width="42" height="42" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
22
+
23
+ <div class="content">
24
+ <div class="name">{participant.name}</div>
25
+ </div>
26
+
27
+ <a
28
+ href={participantUrl(participant)}
29
+ target="_blank"
30
+ class="open-in-new-tab"
31
+ onclick={event => event.stopImmediatePropagation()}>
32
+ <IconNewTab />
33
+ </a>
34
+ </button>
35
+
36
+ <style lang="scss">
37
+ .item {
38
+ appearance: none;
39
+ display: flex;
40
+ align-items: center;
41
+ gap: margin(1);
42
+ width: 100%;
43
+ background: transparent;
44
+ border: 0;
45
+ padding: margin(0.5);
46
+ border-bottom: 1px solid theme(content);
47
+ color: theme(text-color-alt);
48
+ font-family: inherit;
49
+ text-align: left;
50
+ font-size: margin(0.85);
51
+
52
+ &.hoverable {
53
+ cursor: pointer;
54
+
55
+ &:hover {
56
+ background: theme(lighter);
57
+ }
58
+ }
59
+
60
+ &:last-child {
61
+ border-bottom: 0;
62
+ }
63
+ }
64
+
65
+ .image {
66
+ flex: 0 0 auto;
67
+ height: auto;
68
+ width: margin(1.75);
69
+ aspect-ratio: 1/1;
70
+ border-radius: margin(0.25);
71
+ background: theme(content);
72
+ }
73
+
74
+ .name {
75
+ color: theme(text-color);
76
+ }
77
+
78
+ .open-in-new-tab {
79
+ margin-left: auto;
80
+ color: theme(text-color-alt);
81
+
82
+ &:hover {
83
+ color: theme(text-color);
84
+ }
85
+ }
86
+ </style>
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ import { searchTitles, searchParticipants } from '$lib/api/search'
3
+ import type { LinkInjectionDataType } from '$lib/types/injection'
4
+ import type { ParticipantData } from '$lib/types/participant'
5
+ import type { TitleData } from '$lib/types/title'
6
+ import Tabs from '../../Tabs.svelte'
7
+ import TextInput from '../TextInput.svelte'
8
+ import ParticipantSearchItem from './ParticipantSearchItem.svelte'
9
+ import TitleSearchItem from './TitleSearchItem.svelte'
10
+
11
+ interface Props {
12
+ onselect?: (item: TitleData | ParticipantData, type: LinkInjectionDataType) => void
13
+ query: string
14
+ }
15
+
16
+ let { onselect = () => null, query = $bindable() }: Props = $props()
17
+
18
+ const tabs: { label: string, value: LinkInjectionDataType }[] = [{ label: 'Movies & Shows', value: 'title' }, { label: 'Cast & Crew', value: 'participant' }]
19
+
20
+ let results: TitleData[] | ParticipantData[] = $state([])
21
+ let selectedResult: TitleData | ParticipantData | null = $state(null)
22
+ let currentTab: LinkInjectionDataType = $state(tabs[0].value)
23
+ let loading = $state(false)
24
+
25
+ $effect(() => {
26
+ if (query) search(query)
27
+ })
28
+
29
+ async function search(query: string): Promise<void> {
30
+ loading = true
31
+
32
+ try {
33
+ results = currentTab === 'title' ? await searchTitles(query) : await searchParticipants(query)
34
+ } finally {
35
+ loading = false
36
+ }
37
+ }
38
+
39
+ function select(item: TitleData | ParticipantData): void {
40
+ query = currentTab === 'title' ? (item as TitleData).title : (item as ParticipantData).name
41
+ selectedResult = item
42
+
43
+ onselect(item, currentTab)
44
+ }
45
+
46
+ function setTab(value: string): void {
47
+ currentTab = value as LinkInjectionDataType
48
+ query = ''
49
+ selectedResult = null
50
+ }
51
+ </script>
52
+
53
+ <Tabs options={tabs} onclick={(value) => setTab(value)} />
54
+
55
+ <div class="search">
56
+ <TextInput
57
+ bind:value={query}
58
+ oninput={() => selectedResult = null}
59
+ name="search"
60
+ label="Search..." />
61
+
62
+ {#if query && !selectedResult}
63
+ <div class="results" data-testid="search-results">
64
+ {#each results as item (item.sid)}
65
+ {#if currentTab === 'title'}
66
+ <TitleSearchItem title={item as TitleData} onclick={() => select(item as TitleData)} />
67
+ {:else if currentTab === 'participant'}
68
+ <ParticipantSearchItem participant={item as ParticipantData} onclick={() => select(item as ParticipantData)} />
69
+ {/if}
70
+ {:else}
71
+ {#if !loading}
72
+ <em class="empty">No results found</em>
73
+ {/if}
74
+ {/each}
75
+ </div>
76
+ {/if}
77
+
78
+ {#if selectedResult}
79
+ <div class="selected">
80
+ {#if currentTab === 'title'}
81
+ <TitleSearchItem title={selectedResult as TitleData} hoverable={false} />
82
+ {:else if currentTab === 'participant'}
83
+ <ParticipantSearchItem participant={selectedResult as ParticipantData} hoverable={false} />
84
+ {/if}
85
+ </div>
86
+ {/if}
87
+ </div>
88
+
89
+ <style lang="scss">
90
+ .search {
91
+ position: relative;
92
+ margin-top: margin(0.5);
93
+ }
94
+
95
+ .results {
96
+ z-index: 1;
97
+ position: absolute;
98
+ top: calc(100% + margin(0.5));
99
+ left: 0;
100
+ right: 0;
101
+ height: 30vh;
102
+ max-height: margin(10);
103
+ border-radius: margin(0.5);
104
+ background: theme(lighter);
105
+ scrollbar-width: thin;
106
+ overflow: auto;
107
+ }
108
+
109
+ .empty {
110
+ padding: margin(0.5);
111
+ font-size: margin(0.75);
112
+ color: theme(text-color-alt);
113
+ }
114
+
115
+ .selected {
116
+ margin: margin(0.5) 0;
117
+ border-radius: 0.5rem;
118
+ border: 2px solid theme(green);
119
+ }
120
+ </style>
@@ -1,107 +1,107 @@
1
- <script lang="ts">
2
- import { fetchParticipantBySid } from '$lib/api/participants'
3
- import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
4
- import { mobileBreakpoint } from '$lib/constants'
5
- import { SplitTest } from '$lib/enums/SplitTest'
6
- import { t } from '$lib/localization'
7
- import { openModal, type ModalType } from '$lib/modal'
8
- import { getSplitTestVariantName, trackSplitTestView } from '$lib/splitTest'
9
- import type { TitleData } from '$lib/types/title'
10
- import Modal from '../../Modals/Modal.svelte'
11
-
12
- interface Props {
13
- navigate?: (key: string, pushState?: boolean) => void
14
- }
15
-
16
- const { navigate = () => null }: Props = $props()
17
-
18
- async function openModalViaRoute(): Promise<void> {
19
- const currentUrl = new URL(document.location.toString())
20
- const sid = currentUrl.searchParams.get('sid')
21
-
22
- if (!sid) return
23
-
24
- let type: ModalType = sid.startsWith('pr') ? 'participant' : 'title' // Sids with with `pr` for participants, `ti` for titles
25
- let data: any = null
26
-
27
- if (type === 'title') {
28
- const isMobile = window.innerWidth < mobileBreakpoint
29
- const isInReelSplitTest = isMobile && getSplitTestVariantName(SplitTest.Reels) === 'Reel'
30
-
31
- if (isMobile) trackSplitTestView(SplitTest.Reels)
32
-
33
- type = isMobile && isInReelSplitTest ? 'titles-reel' : 'titles-rail'
34
-
35
- const [title, railTitles] = (await Promise.allSettled([fetchTitleBySid(sid), fetchSimilarTitles({ sid } as unknown as TitleData)])).map((promise) =>
36
- promise.status === 'fulfilled' ? promise.value : null,
37
- )
38
- data = [title as TitleData, ...(railTitles as TitleData[])]
39
- } else if (type === 'participant') {
40
- data = await fetchParticipantBySid(sid)
41
- }
42
-
43
- openModal({
44
- type,
45
- data,
46
- props: {
47
- onclose: () => navigate('home'),
48
- pushState: false,
49
- },
50
- })
51
- }
52
- </script>
53
-
54
- {#await openModalViaRoute()}
55
- <Modal blur pushState={false}>
56
- {#snippet dialog()}
57
- <div class="loading">
58
- <div class="bar"></div>
59
- <div class="bar"></div>
60
- <div class="bar"></div>
61
- </div>
62
- {/snippet}
63
- </Modal>
64
- {:catch}
65
- <Modal blur onclose={() => navigate('home')} pushState={false}>
66
- <div class="error">
67
- {t('An Error Occurred')}
68
- </div>
69
- </Modal>
70
- {/await}
71
-
72
- <style lang="scss">
73
- .loading {
74
- display: flex;
75
- align-items: center;
76
- justify-content: center;
77
- height: 100%;
78
- gap: margin(0.5);
79
- }
80
-
81
- @keyframes animate-bar {
82
- to {
83
- height: margin(3);
84
- opacity: 0.5;
85
- }
86
- }
87
-
88
- .bar {
89
- height: margin(4);
90
- width: margin(1);
91
- border-radius: margin(0.25);
92
- background: white;
93
- animation: animate-bar 750ms infinite;
94
-
95
- @for $i from 0 through 2 {
96
- &:nth-child(#{$i}) {
97
- animation-delay: $i * 250ms;
98
- }
99
- }
100
- }
101
-
102
- .error {
103
- padding: margin(2);
104
- font-size: 1.25em;
105
- color: theme(text-color);
106
- }
107
- </style>
1
+ <script lang="ts">
2
+ import { fetchParticipantBySid } from '$lib/api/participants'
3
+ import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
4
+ import { mobileBreakpoint } from '$lib/constants'
5
+ import { SplitTest } from '$lib/enums/SplitTest'
6
+ import { t } from '$lib/localization'
7
+ import { openModal, type ModalType } from '$lib/modal'
8
+ import { getSplitTestVariantName, trackSplitTestView } from '$lib/splitTest'
9
+ import type { TitleData } from '$lib/types/title'
10
+ import Modal from '../../Modals/Modal.svelte'
11
+
12
+ interface Props {
13
+ navigate?: (key: string, pushState?: boolean) => void
14
+ }
15
+
16
+ const { navigate = () => null }: Props = $props()
17
+
18
+ async function openModalViaRoute(): Promise<void> {
19
+ const currentUrl = new URL(document.location.toString())
20
+ const sid = currentUrl.searchParams.get('sid')
21
+
22
+ if (!sid) return
23
+
24
+ let type: ModalType = sid.startsWith('pr') ? 'participant' : 'title' // Sids with with `pr` for participants, `ti` for titles
25
+ let data: any = null
26
+
27
+ if (type === 'title') {
28
+ const isMobile = window.innerWidth < mobileBreakpoint
29
+ const isInReelSplitTest = isMobile && getSplitTestVariantName(SplitTest.Reels) === 'Reel'
30
+
31
+ if (isMobile) trackSplitTestView(SplitTest.Reels)
32
+
33
+ type = isMobile && isInReelSplitTest ? 'titles-reel' : 'titles-rail'
34
+
35
+ const [title, railTitles] = (await Promise.allSettled([fetchTitleBySid(sid), fetchSimilarTitles({ sid } as unknown as TitleData)])).map((promise) =>
36
+ promise.status === 'fulfilled' ? promise.value : null,
37
+ )
38
+ data = [title as TitleData, ...(railTitles as TitleData[])]
39
+ } else if (type === 'participant') {
40
+ data = await fetchParticipantBySid(sid)
41
+ }
42
+
43
+ openModal({
44
+ type,
45
+ data,
46
+ props: {
47
+ onclose: () => navigate('home'),
48
+ pushState: false,
49
+ },
50
+ })
51
+ }
52
+ </script>
53
+
54
+ {#await openModalViaRoute()}
55
+ <Modal blur pushState={false}>
56
+ {#snippet dialog()}
57
+ <div class="loading">
58
+ <div class="bar"></div>
59
+ <div class="bar"></div>
60
+ <div class="bar"></div>
61
+ </div>
62
+ {/snippet}
63
+ </Modal>
64
+ {:catch}
65
+ <Modal blur onclose={() => navigate('home')} pushState={false}>
66
+ <div class="error">
67
+ {t('An Error Occurred')}
68
+ </div>
69
+ </Modal>
70
+ {/await}
71
+
72
+ <style lang="scss">
73
+ .loading {
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ height: 100%;
78
+ gap: margin(0.5);
79
+ }
80
+
81
+ @keyframes animate-bar {
82
+ to {
83
+ height: margin(3);
84
+ opacity: 0.5;
85
+ }
86
+ }
87
+
88
+ .bar {
89
+ height: margin(4);
90
+ width: margin(1);
91
+ border-radius: margin(0.25);
92
+ background: white;
93
+ animation: animate-bar 750ms infinite;
94
+
95
+ @for $i from 0 through 2 {
96
+ &:nth-child(#{$i}) {
97
+ animation-delay: $i * 250ms;
98
+ }
99
+ }
100
+ }
101
+
102
+ .error {
103
+ padding: margin(2);
104
+ font-size: 1.25em;
105
+ color: theme(text-color);
106
+ }
107
+ </style>
@@ -22,7 +22,7 @@
22
22
  onclose?: () => void
23
23
  }
24
24
 
25
- const { titles, onclose = () => null }: Props = $props()
25
+ const { titles, onclose = () => null, ...restProps }: Props = $props()
26
26
 
27
27
 
28
28
  let interval: ReturnType<typeof setInterval> | null = null
@@ -74,7 +74,7 @@
74
74
  {/snippet}
75
75
 
76
76
  <div class="reel" style:--available-height="{availableHeight}px">
77
- <Modal {prepend}>
77
+ <Modal {prepend} {onclose} {...restProps}>
78
78
  {#snippet dialog()}
79
79
  <div class="slider" bind:this={element}>
80
80
  <TinySlider vertical threshold={50} moveThreshold={10} {onchange}>
@@ -142,7 +142,7 @@
142
142
  overscroll-behavior: contain;
143
143
  }
144
144
 
145
- :global(iframe) {
145
+ :global(.plyr) {
146
146
  pointer-events: none;
147
147
  z-index: 0;
148
148
  }
@@ -309,7 +309,7 @@
309
309
  background: black;
310
310
  z-index: 1;
311
311
 
312
- :global(iframe) {
312
+ :global(.plyr) {
313
313
  opacity: 0;
314
314
  animation: fade-iframe 500ms 500ms forwards;
315
315
 
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import Button from './Button.svelte'
3
+
2
4
  interface Option {
3
5
  label: string
4
6
  value: string
@@ -6,16 +8,22 @@
6
8
 
7
9
  interface Props {
8
10
  options: Option[]
11
+ onclick?: (value: string) => void
9
12
  }
10
13
 
11
- const { options }: Props = $props()
14
+ const { options, onclick = () => null }: Props = $props()
15
+
16
+ let active = $derived(options[0]?.value)
12
17
 
13
- let active = $state(options[0]?.value)
18
+ function onchange(value: string): void {
19
+ active = value
20
+ onclick(value)
21
+ }
14
22
  </script>
15
23
 
16
24
  <div class="tabs">
17
25
  {#each options as { label, value }}
18
- <button class="tab" class:active={value === active} onclick={() => active = value}>{label}</button>
26
+ <Button variant={value === active ? 'filled' : 'border'} onclick={() => onchange(value)}>{label}</Button>
19
27
  {/each}
20
28
  </div>
21
29
 
@@ -23,25 +31,6 @@
23
31
  .tabs {
24
32
  display: flex;
25
33
  gap: margin(0.5);
26
- }
27
-
28
- .tab {
29
- cursor: pointer;
30
- appearance: none;
31
- background: transparent;
32
- padding: margin(0.25) margin(0.5);
33
- border: 1px solid currentColor;
34
- border-radius: theme(tabs-border-radius, border-radius-small);
35
- transition: opacity 100ms;
36
- font-family: inherit;
37
- font-weight: theme(tabs-font-weight, 500);
38
- color: theme(tabs-text-color, text-color-alt);
39
- font-size: inherit;
40
-
41
- &.active,
42
- &:hover {
43
- background: theme(tabs-background-active, content);
44
- color: theme(tabs-text-color-active, text-color);
45
- }
34
+ font-size: theme(font-size-base);
46
35
  }
47
36
  </style>