itube-specs 0.0.899 → 0.0.902

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 (40) hide show
  1. package/assets/icons/common/clock-filled.svg +1 -0
  2. package/assets/scss/mixins/_breakpoints.scss +3 -2
  3. package/assets/scss/themes/dark/_theme.scss +17 -23
  4. package/assets/scss/themes/light/_theme.scss +17 -23
  5. package/assets/scss/vars/_palette.scss +1 -0
  6. package/components/card/category.vue +4 -8
  7. package/components/card/dropdown/index.vue +47 -9
  8. package/components/card/dropdown/trigger.vue +1 -0
  9. package/components/card/model.vue +1 -1
  10. package/components/card/playlist.vue +15 -6
  11. package/components/card/round.vue +3 -3
  12. package/components/card/video/actions.vue +123 -0
  13. package/components/card/video/index.vue +149 -30
  14. package/components/filter/selects.vue +1 -1
  15. package/components/info/grid.vue +1 -1
  16. package/components/layout/header-user.vue +1 -1
  17. package/components/library/feed.vue +65 -24
  18. package/components/library/follows.vue +8 -44
  19. package/components/library/item.vue +4 -4
  20. package/components/player/like.vue +1 -1
  21. package/components/player/playlist.vue +2 -2
  22. package/components/report/main.vue +20 -5
  23. package/components/ui/checkbox.vue +1 -1
  24. package/components/ui/dropdown.vue +1 -1
  25. package/components/ui/input.vue +2 -2
  26. package/components/ui/radio.vue +1 -1
  27. package/components/ui/toggle.vue +1 -1
  28. package/composables/use-report-popup.ts +26 -3
  29. package/composables/use-video-collections.ts +30 -0
  30. package/lib/scheme/profile-navigation-items.ts +1 -1
  31. package/package.json +1 -1
  32. package/pages/history.vue +12 -36
  33. package/pages/library/categories.vue +2 -1
  34. package/pages/library/channels.vue +1 -1
  35. package/pages/library/index.vue +3 -3
  36. package/pages/library/models.vue +4 -4
  37. package/stubs/library-follows.ts +19 -16
  38. package/types/library-feed.d.ts +6 -0
  39. package/types/video-card.d.ts +4 -0
  40. package/components/card/history.vue +0 -359
@@ -6,6 +6,8 @@
6
6
  {'--compact': compact},
7
7
  {'--avatar': card.channelAvatar},
8
8
  {'--no-channel': isChannelPage},
9
+ {'--progress': hasProgress},
10
+ {'--favorite': isFavoriteCard},
9
11
  ]"
10
12
  class="card-video"
11
13
  >
@@ -38,20 +40,31 @@
38
40
  class="card-video__new"
39
41
  theme="primary-gradient"
40
42
  />
41
- <UiLabel
42
- v-if="card.quality && parseInt(card.quality) >= 1080"
43
- :text="card.quality"
44
- class="card-video__resolution"
45
- theme="yellow"
46
- />
47
43
  <UiLabel
48
44
  v-if="card.duration"
49
45
  :text="duration"
50
46
  class="card-video__duration"
51
47
  theme="black"
52
48
  />
49
+ <span
50
+ v-if="hasProgress"
51
+ :style="{ '--card-video-progress': `${progress}%` }"
52
+ :aria-label="t('watched_percent', { percent: progress })"
53
+ :aria-valuenow="progress"
54
+ class="card-video__progress"
55
+ role="progressbar"
56
+ aria-valuemin="0"
57
+ aria-valuemax="100"
58
+ />
53
59
  </NuxtLink>
54
60
 
61
+ <CardVideoActions
62
+ v-if="showActions"
63
+ :card="card"
64
+ :show-favorites="canFavorite"
65
+ :show-watch-later="canWatchLater"
66
+ />
67
+
55
68
  <div class="card-video__wrapper">
56
69
  <component
57
70
  v-if="!mini"
@@ -82,7 +95,7 @@
82
95
  >{{ card.title }}
83
96
  </NuxtLink>
84
97
  <component
85
- v-if="!mini && !compact && card.channelName"
98
+ v-if="!mini && !compact && !history && card.channelName"
86
99
  :is="component"
87
100
  :to="channelLink"
88
101
  class="card-video__channel-wrapper"
@@ -96,8 +109,28 @@
96
109
  </template>
97
110
  </span>
98
111
  </component>
112
+ <p
113
+ v-if="history"
114
+ class="card-video__meta"
115
+ >
116
+ <NuxtLink
117
+ v-if="card.channelName"
118
+ :to="channelLink"
119
+ class="card-video__channel-title"
120
+ >{{ card.channelName }}</NuxtLink>
121
+ <span
122
+ :class="{'--remaining': remaining}"
123
+ class="card-video__watched"
124
+ >
125
+ <UiIcon
126
+ name="clock"
127
+ size="12"
128
+ />
129
+ {{ remaining ? remainingText : watchedAgo }}
130
+ </span>
131
+ </p>
99
132
  <div
100
- v-if="!mini && !compact && cardTags && cardTags.length > 0"
133
+ v-if="!mini && !compact && !history && cardTags && cardTags.length > 0"
101
134
  class="card-video__tags"
102
135
  >
103
136
  <div class="card-video__tags-wrapper">
@@ -113,9 +146,14 @@
113
146
  </div>
114
147
  <CardDropdownTrigger
115
148
  :card="card"
116
- :show-watch-later="featureFlags.FeatureWatchLaterEnabled"
149
+ :show-watch-later="canWatchLater"
150
+ :show-favorites="canFavorite"
151
+ :show-report="!history"
152
+ :show-remove="history"
153
+ :show-share="history"
117
154
  class="card-video__dropdown"
118
- show-report
155
+ @share="emit('share', card)"
156
+ @remove="emit('remove', card)"
119
157
  />
120
158
  </div>
121
159
  </div>
@@ -138,9 +176,18 @@ const props = defineProps<{
138
176
  playlistId?: string
139
177
  priority?: boolean
140
178
  playlist?: boolean
179
+ /** Тип «история»: мета-строка со временем просмотра вместо канала и тегов, меню с удалением и шарингом */
180
+ history?: boolean
181
+ /** Развилка мета-строки истории: вместо «когда смотрели» — сколько осталось до конца («Продолжить просмотр») */
182
+ remaining?: boolean
141
183
  }>();
142
184
 
143
- const { t } = useI18n();
185
+ const emit = defineEmits<{
186
+ (e: 'remove', card: IVideoCard): void
187
+ (e: 'share', card: IVideoCard): void
188
+ }>();
189
+
190
+ const { t, locale } = useI18n();
144
191
 
145
192
  const NuxtLinkComponent = resolveComponent('NuxtLink');
146
193
 
@@ -184,6 +231,27 @@ const duration = computed(() => {
184
231
  return getDuration(props.card.duration);
185
232
  });
186
233
 
234
+ const { isFavorite } = useVideoCollections();
235
+
236
+ const isFavoriteCard = computed(() => isFavorite(props.card.guid));
237
+
238
+ const canFavorite = computed(() => featureFlags.FeatureFavoritesEnabled);
239
+ const canWatchLater = computed(() => !props.history && featureFlags.FeatureWatchLaterEnabled);
240
+ const showActions = computed(() => !props.mini && (canFavorite.value || canWatchLater.value));
241
+
242
+ const hasProgress = computed(() => props.card.progress !== undefined);
243
+
244
+ const progress = computed(() => Math.min(100, Math.max(0, Math.round(props.card.progress ?? 0))));
245
+
246
+ const watchedAgo = computed(() => props.card.watchedAt === undefined
247
+ ? ''
248
+ : useFormatTimeAgo(props.card.watchedAt, locale.value, true));
249
+
250
+ const remainingText = computed(() => {
251
+ const left = Math.ceil((props.card.duration * (100 - progress.value)) / 100 / 60);
252
+ return t('minutes_left', { count: Math.max(left, 1) });
253
+ });
254
+
187
255
  const rootPath = computed(() => props.playlistId ? `playlists/${props.playlistId}` : 'videos');
188
256
 
189
257
  const link = computed(() => generateLink(`/${rootPath.value}/${props.card[ featureFlags.FetchByUrlEnabled ? 'url' : 'id' ]}`));
@@ -219,6 +287,7 @@ const cardTags = computed(() => {
219
287
  <style lang="scss">
220
288
  $inset-label: 8px;
221
289
  $layout-breakpoint: 250px;
290
+ $progress-height: 4px;
222
291
 
223
292
  // Колонка под меню карточки фиксирована, а не auto: иначе ширина зависит от того, отрисовано меню
224
293
  // или нет, и в момент подмены кнопки заголовок дёргается
@@ -227,6 +296,7 @@ $dropdown-column: 18px;
227
296
  .card-video {
228
297
  @include video-card;
229
298
 
299
+ position: relative;
230
300
  container-type: inline-size;
231
301
 
232
302
  @include to-br(xs) {
@@ -246,7 +316,6 @@ $dropdown-column: 18px;
246
316
  }
247
317
 
248
318
  .card-video__duration,
249
- .card-video__resolution,
250
319
  .card-video__new {
251
320
  position: absolute;
252
321
  }
@@ -256,6 +325,11 @@ $dropdown-column: 18px;
256
325
  left: $inset-label;
257
326
  }
258
327
 
328
+ /* Кнопка избранного занимает левый верхний угол постера — плашка «новое» встаёт правее неё */
329
+ .--favorite .card-video__new {
330
+ left: $inset-label * 2 + 36px;
331
+ }
332
+
259
333
  .card-video__dropdown {
260
334
  grid-column: 2;
261
335
  grid-row: 1;
@@ -273,29 +347,45 @@ $dropdown-column: 18px;
273
347
  }
274
348
 
275
349
  .card-dropdown__trigger > .ui-icon {
276
- @include from-br(sm) {
350
+ @media (hover: hover) {
277
351
  transition: var(--transition) transform;
278
352
  }
279
353
  }
280
354
 
281
355
  .card-dropdown__trigger:is(:hover, :focus-visible) > .ui-icon {
282
- @include from-br(sm) {
356
+ @media (hover: hover) {
283
357
  transform: scale($video-card-dropdown-size--hover);
284
358
  }
285
359
  }
286
360
 
287
- .card-video__resolution {
288
- top: $inset-label;
289
- right: $inset-label;
290
- background: $video-card-resolution-bg;
291
- color: $video-card-resolution-text;
292
- }
293
-
294
361
  .card-video__duration {
295
362
  bottom: $inset-label;
296
363
  left: $inset-label;
297
364
  }
298
365
 
366
+ .--progress .card-video__duration {
367
+ bottom: $inset-label + $progress-height;
368
+ }
369
+
370
+ /* Превью кроет постер целиком ($z-index-plus) — полоса прогресса должна остаться поверх него */
371
+ .card-video__progress {
372
+ position: absolute;
373
+ right: 0;
374
+ bottom: 0;
375
+ left: 0;
376
+ z-index: $z-index-loader;
377
+ height: $progress-height;
378
+ background-color: $video-card-progress-bg;
379
+
380
+ &::after {
381
+ content: '';
382
+ display: block;
383
+ width: var(--card-video-progress, 0%);
384
+ height: 100%;
385
+ background-color: $video-card-progress-fill;
386
+ }
387
+ }
388
+
299
389
  .card-video__wrapper {
300
390
  display: grid;
301
391
  grid-template-columns: 1fr $dropdown-column;
@@ -322,7 +412,8 @@ $dropdown-column: 18px;
322
412
  grid-column: 2;
323
413
  }
324
414
 
325
- .--avatar .card-video__channel-wrapper {
415
+ .--avatar .card-video__channel-wrapper,
416
+ .--avatar .card-video__meta {
326
417
  grid-column: 2;
327
418
  }
328
419
 
@@ -386,13 +477,13 @@ $dropdown-column: 18px;
386
477
  grid-column: 1;
387
478
  }
388
479
 
389
- @include from-br(xs) {
480
+ @media (hover: hover) {
390
481
  transition: var(--transition) color;
391
482
  }
392
483
  }
393
484
 
394
485
  .card-video:is(:hover, :focus-visible) .card-video__title {
395
- @include from-br(xs) {
486
+ @media (hover: hover) {
396
487
  color: $video-card-text--hover;
397
488
  }
398
489
  }
@@ -413,6 +504,38 @@ $dropdown-column: 18px;
413
504
  }
414
505
  }
415
506
 
507
+ /* Мета-строка истории: «канал · когда смотрели / сколько осталось» на месте строки канала */
508
+ .card-video__meta {
509
+ @include font-xs;
510
+
511
+ display: flex;
512
+ align-items: center;
513
+ grid-row: 2;
514
+ margin: 6px 0 0;
515
+ font-weight: 400;
516
+ color: $video-card-secondary;
517
+
518
+ @include to-cq($layout-breakpoint) {
519
+ grid-column: 1;
520
+ }
521
+ }
522
+
523
+ .card-video__meta .card-video__channel-title + .card-video__watched::before {
524
+ content: '·';
525
+ margin-inline: 6px;
526
+ }
527
+
528
+ .card-video__watched {
529
+ display: flex;
530
+ align-items: center;
531
+ column-gap: 4px;
532
+ flex-shrink: 0;
533
+ }
534
+
535
+ .card-video__watched.--remaining {
536
+ color: $video-card-remaining;
537
+ }
538
+
416
539
  .card-video__channel-logo {
417
540
  border-radius: 50%;
418
541
  }
@@ -425,12 +548,8 @@ $dropdown-column: 18px;
425
548
  .card-video__logo-wrapper[href],
426
549
  .card-video__channel-wrapper[href],
427
550
  .card-video__title {
428
- @include from-br(xs) {
429
- transition: var(--transition) opacity;
430
-
431
- &:is(:hover, :focus-visible) {
432
- opacity: 0.9;
433
- }
551
+ @include hover(opacity) {
552
+ opacity: 0.9;
434
553
  }
435
554
  }
436
555
 
@@ -347,7 +347,7 @@ function getSelectLabel(item: IModelFilter) {
347
347
  }
348
348
 
349
349
  .filter-selects__wrapper:is(:hover, :focus-visible) .filter-selects__select-button:not(.--active-select) {
350
- @include from-br(sm) {
350
+ @media (hover: hover) {
351
351
  background: $filters-main-bg--hover;
352
352
  }
353
353
  }
@@ -168,7 +168,7 @@ function isWarning(value: string) {
168
168
  }
169
169
 
170
170
  .info-grid__item.--link:is(:hover, :focus-visible) .info-grid__item-value {
171
- @include from-br(sm) {
171
+ @media (hover: hover) {
172
172
  background-color: $info-grid-item-value-link-bg--hover;
173
173
  }
174
174
  }
@@ -240,7 +240,7 @@ function openChilds() {
240
240
  width: 100%;
241
241
  text-transform: capitalize;
242
242
 
243
- &:is(:hover, :focus-visible) {
243
+ @include hover(background-color) {
244
244
  background-color: $header-user-item-bg--hover;
245
245
  }
246
246
  }
@@ -1,8 +1,12 @@
1
1
  <!-- Блок подписки (модель / канал / категория): шапка с аватаром, тегами и счётчиками + лента роликов -->
2
2
  <template>
3
3
  <article class="library-feed">
4
- <header class="library-feed__header">
4
+ <header
5
+ :class="{ '--without-avatar': withoutAvatar }"
6
+ class="library-feed__header"
7
+ >
5
8
  <NuxtLink
9
+ v-if="!withoutAvatar"
6
10
  :to="item.to"
7
11
  :title="item.title"
8
12
  class="library-feed__avatar-link"
@@ -14,9 +18,9 @@
14
18
  :loading="priority ? 'eager' : 'lazy'"
15
19
  :priority="priority"
16
20
  class="library-feed__avatar"
17
- width="96"
18
- height="96"
19
- sizes="96px"
21
+ width="64"
22
+ height="64"
23
+ sizes="64px"
20
24
  @error="avatarError = true"
21
25
  />
22
26
  <span
@@ -45,7 +49,7 @@
45
49
 
46
50
  <span
47
51
  v-if="item.newCount"
48
- class="library-feed__badge"
52
+ class="library-feed__badge _from-sm"
49
53
  >{{ t('library.new_count', { count: item.newCount }) }}</span>
50
54
  </div>
51
55
 
@@ -57,10 +61,19 @@
57
61
  micro
58
62
  />
59
63
 
64
+ <p
65
+ v-if="parametersText || ageText"
66
+ class="library-feed__params"
67
+ >{{ parametersText }}<span
68
+ v-if="ageText"
69
+ class="library-feed__age"
70
+ >{{ ageText }}</span></p>
71
+
60
72
  <p class="library-feed__meta">
61
73
  <span
62
74
  v-for="(entry, index) in item.meta"
63
75
  :key="`meta-${index}`"
76
+ :class="{ '_from-sm': entry.desktopOnly }"
64
77
  class="library-feed__meta-item"
65
78
  >
66
79
  <UiIcon
@@ -137,6 +150,8 @@ const props = defineProps<{
137
150
  item: ILibraryFeedItem
138
151
  following?: boolean
139
152
  priority?: boolean
153
+ /** У категорий аватарки нет вообще — колонка не рисуется, инициалы не подставляются */
154
+ withoutAvatar?: boolean
140
155
  }>();
141
156
 
142
157
  const emit = defineEmits<{
@@ -158,6 +173,14 @@ const initials = computed(() => props.item.title
158
173
  .toUpperCase());
159
174
 
160
175
  const videosText = computed(() => t('plural.videos', props.item.videosCount));
176
+
177
+ const parametersText = computed(() => (props.item.parameters ?? []).filter(Boolean).join(' · '));
178
+
179
+ /** Возраст идёт хвостом той же строки, поэтому разделитель печатается только когда параметры есть. */
180
+ const ageText = computed(() => {
181
+ if (!props.item.age) return '';
182
+ return parametersText.value ? ` · ${props.item.age}` : props.item.age;
183
+ });
161
184
  </script>
162
185
 
163
186
  <style lang="scss">
@@ -165,13 +188,11 @@ const videosText = computed(() => t('plural.videos', props.item.videosCount));
165
188
  /* Растушёвка «ещё» у тегов уходит в фон карточки, а не страницы */
166
189
  --expand-row-fade-end: #{$library-feed-fade};
167
190
 
168
- padding: 16px;
169
- background-color: $library-feed-bg;
170
- border: 1px solid $library-feed-border;
171
- border-radius: $radius-xl;
172
-
173
191
  @include from-br(sm) {
174
192
  padding: 20px;
193
+ border: 1px solid $library-feed-border;
194
+ background-color: $library-feed-bg;
195
+ border-radius: $radius-xl;
175
196
  }
176
197
  }
177
198
 
@@ -180,7 +201,7 @@ const videosText = computed(() => t('plural.videos', props.item.videosCount));
180
201
  grid-template-columns: auto minmax(0, 1fr);
181
202
  column-gap: 16px;
182
203
  row-gap: 12px;
183
- align-items: start;
204
+ align-items: center;
184
205
  margin-bottom: 20px;
185
206
 
186
207
  @include from-br(sm) {
@@ -188,24 +209,32 @@ const videosText = computed(() => t('plural.videos', props.item.videosCount));
188
209
  }
189
210
  }
190
211
 
212
+ .library-feed__header.--without-avatar {
213
+ grid-template-columns: minmax(0, 1fr);
214
+
215
+ @include from-br(sm) {
216
+ grid-template-columns: minmax(0, 1fr) auto;
217
+ }
218
+ }
219
+
191
220
  .library-feed__avatar {
192
- width: 80px;
193
- height: 80px;
221
+ width: 64px;
222
+ height: 64px;
194
223
  object-fit: cover;
195
224
  object-position: center 30%;
196
225
  border-radius: $radius-full;
197
- border: 2px solid $library-feed-avatar-border;
198
226
 
199
- @include from-br(sm) {
200
- width: 96px;
201
- height: 96px;
202
- transition: var(--transition) border-color;
227
+ /* Кольцо рисуем тенью, а не бордером: при border-box рамка съедала бы 4px картинки */
228
+ box-shadow: 0 0 0 2px $library-feed-avatar-border;
229
+
230
+ @media (hover: hover) {
231
+ transition: var(--transition) box-shadow;
203
232
  }
204
233
  }
205
234
 
206
- @include from-br(sm) {
235
+ @media (hover: hover) {
207
236
  .library-feed__avatar-link:is(:hover, :focus-visible) .library-feed__avatar {
208
- border-color: $library-feed-avatar-border--hover;
237
+ box-shadow: 0 0 0 2px $library-feed-avatar-border--hover;
209
238
  }
210
239
  }
211
240
 
@@ -270,7 +299,18 @@ const videosText = computed(() => t('plural.videos', props.item.videosCount));
270
299
  }
271
300
 
272
301
  .library-feed__tags {
273
- margin-top: 8px;
302
+ margin-top: 4px;
303
+ }
304
+
305
+ .library-feed__params {
306
+ @include font-sm;
307
+
308
+ margin-top: 4px;
309
+ color: $library-feed-params;
310
+ }
311
+
312
+ .library-feed__age {
313
+ color: $library-feed-age;
274
314
  }
275
315
 
276
316
  .library-feed__meta {
@@ -281,7 +321,7 @@ const videosText = computed(() => t('plural.videos', props.item.videosCount));
281
321
  align-items: center;
282
322
  column-gap: 8px;
283
323
  row-gap: 4px;
284
- margin-top: 8px;
324
+ margin-top: 2px;
285
325
  color: $library-feed-meta;
286
326
  }
287
327
 
@@ -291,9 +331,10 @@ const videosText = computed(() => t('plural.videos', props.item.videosCount));
291
331
  column-gap: 6px;
292
332
  }
293
333
 
294
- .library-feed__meta-item:not(:last-child)::after {
334
+ /* Разделитель слева, а не справа: скрытый на мобиле счётчик не оставляет висячей точки */
335
+ .library-feed__meta-item:not(:first-child)::before {
295
336
  content: '·';
296
- margin-inline-start: 2px;
337
+ margin-inline-end: 2px;
297
338
  color: $library-feed-meta-dot;
298
339
  }
299
340
 
@@ -1,4 +1,4 @@
1
- <!-- Список подписок «Библиотеки»: сортировка, блоки лент, подтверждение отписки -->
1
+ <!-- Список подписок «Библиотеки»: сортировка, блоки лент -->
2
2
  <template>
3
3
  <div class="library-follows">
4
4
  <UiSectionTitle
@@ -27,6 +27,7 @@
27
27
  :item="item"
28
28
  :following="isFollowing(item)"
29
29
  :priority="index === 0"
30
+ :without-avatar="withoutAvatar"
30
31
  @toggle-follow="onToggleFollow"
31
32
  />
32
33
  </div>
@@ -35,15 +36,6 @@
35
36
  v-else
36
37
  without-reset
37
38
  />
38
-
39
- <UiConfirm
40
- v-model="isUnfollowOpen"
41
- :title="t('library.unfollow_title', { name: pendingUnfollow?.title })"
42
- :confirm-text="t('library.unfollow_confirm')"
43
- @confirm="onUnfollowConfirm"
44
- >
45
- <p class="library-follows__confirm">{{ t('library.unfollow_text') }}</p>
46
- </UiConfirm>
47
39
  </div>
48
40
  </template>
49
41
 
@@ -61,6 +53,8 @@ const props = defineProps<{
61
53
  items: ILibraryFeedItem[]
62
54
  title: string
63
55
  icon: string
56
+ /** Список без аватарок — у категорий их нет (и инициалы вместо них тоже не рисуем) */
57
+ withoutAvatar?: boolean
64
58
  }>();
65
59
 
66
60
  const { t } = useI18n();
@@ -90,31 +84,10 @@ function isFollowing(item: ILibraryFeedItem) {
90
84
  return !unfollowed.value.includes(item.guid);
91
85
  }
92
86
 
93
- /** Сущность, для которой открыт попап отписки. */
94
- const pendingUnfollow = ref<ILibraryFeedItem | null>(null);
95
-
96
- const isUnfollowOpen = computed({
97
- get: () => pendingUnfollow.value !== null,
98
- set: value => {
99
- if (!value) pendingUnfollow.value = null;
100
- },
101
- });
102
-
103
- /** Подписка — сразу, отписка — только через подтверждение: она сносит ленту обновлений. */
104
87
  function onToggleFollow(item: ILibraryFeedItem) {
105
- if (isFollowing(item)) {
106
- pendingUnfollow.value = item;
107
- return;
108
- }
109
-
110
- unfollowed.value = unfollowed.value.filter(guid => guid !== item.guid);
111
- }
112
-
113
- function onUnfollowConfirm() {
114
- if (!pendingUnfollow.value) return;
115
-
116
- unfollowed.value = [...unfollowed.value, pendingUnfollow.value.guid];
117
- pendingUnfollow.value = null;
88
+ unfollowed.value = isFollowing(item)
89
+ ? [...unfollowed.value, item.guid]
90
+ : unfollowed.value.filter(guid => guid !== item.guid);
118
91
  }
119
92
  </script>
120
93
 
@@ -128,20 +101,11 @@ function onUnfollowConfirm() {
128
101
  flex-wrap: wrap;
129
102
  align-items: center;
130
103
  gap: 8px;
131
- margin-bottom: 24px;
104
+ margin-bottom: 32px;
132
105
  }
133
106
 
134
107
  .library-follows__list {
135
108
  display: grid;
136
109
  row-gap: 24px;
137
110
  }
138
-
139
- .library-follows__confirm {
140
- @include font-sm;
141
-
142
- max-width: 400px;
143
- margin: 0 auto;
144
- text-align: center;
145
- color: $page-library-confirm-text;
146
- }
147
111
  </style>
@@ -86,12 +86,12 @@ withDefaults(defineProps<{
86
86
  border: 1px solid $library-item-icon-border;
87
87
  border-radius: $radius-xl;
88
88
 
89
- @include from-br(sm) {
89
+ @media (hover: hover) {
90
90
  transition: var(--transition) background-color, var(--transition) border-color;
91
91
  }
92
92
  }
93
93
 
94
- @include from-br(sm) {
94
+ @media (hover: hover) {
95
95
  .library-item:is(:hover, :focus-visible) .library-item__icon {
96
96
  background-color: $library-item-icon-bg--hover;
97
97
  border-color: $library-item-icon-border--hover;
@@ -117,12 +117,12 @@ withDefaults(defineProps<{
117
117
  font-weight: 600;
118
118
  color: $library-item-title;
119
119
 
120
- @include from-br(sm) {
120
+ @media (hover: hover) {
121
121
  transition: var(--transition) color;
122
122
  }
123
123
  }
124
124
 
125
- @include from-br(sm) {
125
+ @media (hover: hover) {
126
126
  .library-item:is(:hover, :focus-visible) .library-item__title {
127
127
  color: $library-item-title--hover;
128
128
  }
@@ -176,7 +176,7 @@ const dislikePercent = computed(() => percents.value.dislikePercent);
176
176
  }
177
177
 
178
178
  .player-like__control:is(:hover, :focus-visible) {
179
- @include from-br(sm) {
179
+ @media (hover: hover) {
180
180
  color: $like-text--hover;
181
181
  }
182
182
  }
@@ -414,7 +414,7 @@ function onFollowClick() {
414
414
  align-self: center;
415
415
  margin-left: auto;
416
416
 
417
- @include from-br(sm) {
417
+ @media (hover: hover) {
418
418
  opacity: 0;
419
419
  transition: var(--transition) opacity;
420
420
  color: $video-playlist-card-dropdown-text;
@@ -428,7 +428,7 @@ function onFollowClick() {
428
428
  .player-playlist__card:is(:hover, :focus-visible) .player-playlist__card-dropdown,
429
429
  .player-playlist__card-dropdown.--open,
430
430
  .player-playlist__card-dropdown:focus-within {
431
- @include from-br(sm) {
431
+ @media (hover: hover) {
432
432
  opacity: 1;
433
433
  }
434
434
  }