itube-specs 0.0.984 → 0.0.987

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.
@@ -52,16 +52,18 @@ const props = defineProps<{
52
52
  card: IModelCard
53
53
  }>();
54
54
 
55
- const fallbackImage = '/img/model-placeholder.webp';
55
+ const fallbackImage = '/img/model-placeholder-square.webp';
56
+ const fallbackAvifImage = '/img/model-placeholder-square.avif';
56
57
  const imageError = ref(false);
57
58
 
58
59
  /** Аватар 48px круглый — нужен квадратный кроп, вертикальный портрет сюда не годится. */
59
- const currentImage = computed(() => {
60
- if (imageError.value) return fallbackImage;
61
- return props.card.primaryImageSmallUrl || fallbackImage;
62
- });
60
+ const isFallback = computed(() => imageError.value || !props.card.primaryImageSmallUrl);
63
61
 
64
- const currentAvifSrc = computed(() => (imageError.value ? undefined : props.card.primaryImageSmallAvifUrl));
62
+ const currentImage = computed(() => (isFallback.value ? fallbackImage : props.card.primaryImageSmallUrl));
63
+
64
+ const currentAvifSrc = computed(() => (
65
+ isFallback.value ? fallbackAvifImage : props.card.primaryImageSmallAvifUrl
66
+ ));
65
67
  </script>
66
68
 
67
69
  <style lang="scss">
@@ -98,16 +98,16 @@ const { t } = useI18n();
98
98
  */
99
99
  const SIZES = 'auto, (max-width: 449px) 50vw, (max-width: 1385px) 215px, 250px';
100
100
 
101
- const fallbackImage = '/img/model-placeholder.webp';
101
+ const fallbackImage = '/img/model-placeholder-portrait.webp';
102
+ const fallbackAvifImage = '/img/model-placeholder-portrait.avif';
102
103
  const imageError = ref(false);
103
104
 
104
- const currentImage = computed(() => {
105
- if (imageError.value) return fallbackImage;
106
- return props.card.primaryImageUrl || fallbackImage;
107
- });
105
+ const isFallback = computed(() => imageError.value || !props.card.primaryImageUrl);
106
+
107
+ const currentImage = computed(() => (isFallback.value ? fallbackImage : props.card.primaryImageUrl));
108
108
 
109
- /** На заглушке ладдер сбрасываем — иначе браузер продолжит тянуть битые кандидаты. */
110
- const currentAvifSrc = computed(() => (imageError.value ? undefined : props.card.primaryImageAvifUrl));
109
+ /** На заглушке подменяем и avif-ветку — иначе браузер продолжит тянуть битый кандидат. */
110
+ const currentAvifSrc = computed(() => (isFallback.value ? fallbackAvifImage : props.card.primaryImageAvifUrl));
111
111
 
112
112
  function onImageError() {
113
113
  imageError.value = true;
@@ -49,6 +49,8 @@ const props = withDefaults(defineProps<{
49
49
  loading?: 'eager' | 'lazy'
50
50
  priority?: boolean
51
51
  placeholderAvatar?: string
52
+ /** Та же заглушка в avif. У канальной (svg) пары нет — тогда проп не передают. */
53
+ placeholderAvatarAvif?: string
52
54
  /** Только аватар и название — без счётчика роликов (страница подписок) */
53
55
  withoutCount?: boolean
54
56
  }>(), {
@@ -64,12 +66,15 @@ const SIZES = `auto, (max-width: ${CSS_BREAKPOINTS.sm - 1}px) 80px, 96px`;
64
66
  const fallbackImage = props.placeholderAvatar;
65
67
  const imageError = ref(false);
66
68
 
67
- const currentImage = computed(() => {
68
- if (imageError.value) return fallbackImage || '';
69
- return props.card.primaryImageUrl || fallbackImage || '';
70
- });
69
+ const isFallback = computed(() => imageError.value || !props.card.primaryImageUrl);
70
+
71
+ const currentImage = computed(() => (
72
+ (isFallback.value ? fallbackImage : props.card.primaryImageUrl) || ''
73
+ ));
71
74
 
72
- const currentAvifSrc = computed(() => (imageError.value ? undefined : props.card.primaryImageAvifUrl));
75
+ const currentAvifSrc = computed(() => (
76
+ isFallback.value ? props.placeholderAvatarAvif : props.card.primaryImageAvifUrl
77
+ ));
73
78
 
74
79
  function onImageError() {
75
80
  imageError.value = true;
@@ -5,7 +5,6 @@
5
5
  {'--mini': mini},
6
6
  {'--compact': compact},
7
7
  {'--avatar': card.channelAvatar},
8
- {'--no-channel': isChannelPage},
9
8
  {'--progress': hasProgress},
10
9
  {'--favorite': isFavoriteCard},
11
10
  {'--tags-open': isTagsOpen},
@@ -95,30 +94,10 @@
95
94
  class="card-video__title _truncate"
96
95
  >{{ card.title }}
97
96
  </NuxtLink>
98
- <component
99
- v-if="!mini && !compact && !history && card.channelName"
100
- :is="component"
101
- :to="channelLink"
102
- class="card-video__channel-wrapper"
103
- >
104
- <span
105
- :data-nosnippet="!card.channelName || null"
106
- class="card-video__channel-title"
107
- >
108
- <template v-if="card.channelName">
109
- {{ card.channelName }}
110
- </template>
111
- </span>
112
- </component>
113
97
  <p
114
98
  v-if="history"
115
99
  class="card-video__meta"
116
100
  >
117
- <NuxtLink
118
- v-if="card.channelName"
119
- :to="channelLink"
120
- class="card-video__channel-title"
121
- >{{ card.channelName }}</NuxtLink>
122
101
  <span
123
102
  :class="{'--remaining': remaining}"
124
103
  class="card-video__watched"
@@ -189,9 +168,6 @@ const component = computed(() => props.card.channelName ? NuxtLinkComponent : 'd
189
168
 
190
169
  const { generateLink } = useGenerateLink();
191
170
 
192
- const pageName = useGetPureRouteName();
193
- const isChannelPage = computed(() => pageName === 'channels-channel');
194
-
195
171
  const previewId = useId();
196
172
  const { activePreviewId, startPreviewByHover, stopPreviewByHover, startPreviewByTouch } = useVideoPreview();
197
173
 
@@ -383,7 +359,6 @@ $dropdown-column: 18px;
383
359
  grid-column: 2;
384
360
  }
385
361
 
386
- .--avatar .card-video__channel-wrapper,
387
362
  .--avatar .card-video__meta {
388
363
  grid-column: 2;
389
364
  }
@@ -398,25 +373,26 @@ $dropdown-column: 18px;
398
373
  grid-column: 2 / -1;
399
374
  }
400
375
 
376
+ /* Кружок канала — только на десктопе: на мобиле карточка отдаёт всю ширину заголовку */
401
377
  @include to-br(sm) {
402
- .--no-channel .card-video__logo-wrapper,
403
- .--no-channel .card-video__channel-wrapper {
378
+ .card-video .card-video__logo-wrapper {
404
379
  display: none;
405
380
  }
406
381
 
407
- .--no-channel .card-video__wrapper {
382
+ .card-video .card-video__wrapper {
408
383
  grid-template-columns: minmax(0, 1fr) $dropdown-column;
409
384
  }
410
385
 
411
- .--no-channel .card-video__title {
386
+ .card-video .card-video__title,
387
+ .card-video .card-video__meta {
412
388
  grid-column: 1;
413
389
  }
414
390
 
415
- .--no-channel .card-video__tags {
391
+ .card-video .card-video__tags {
416
392
  grid-column: 1 / -1;
417
393
  }
418
394
 
419
- .--no-channel .card-video__dropdown {
395
+ .card-video .card-video__dropdown {
420
396
  grid-column: 2;
421
397
  grid-row: 1;
422
398
  }
@@ -426,7 +402,7 @@ $dropdown-column: 18px;
426
402
  grid-template-columns: minmax(0, 1fr) $dropdown-column;
427
403
  }
428
404
 
429
- /* Без строки канала и тегов вторая строка грида не нужна — иначе аватар и «троеточие» растягиваются. */
405
+ /* Без тегов вторая строка грида не нужна — иначе кружок и «троеточие» растягиваются. */
430
406
  .--compact .card-video__logo-wrapper,
431
407
  .--compact .card-video__dropdown {
432
408
  grid-row: 1;
@@ -455,23 +431,7 @@ $dropdown-column: 18px;
455
431
  }
456
432
  }
457
433
 
458
- .card-video__channel-wrapper {
459
- @include font-xs;
460
-
461
- font-weight: 400;
462
- display: grid;
463
- grid-row: 2;
464
- grid-template-columns: auto 1fr;
465
- align-items: center;
466
- column-gap: 4px;
467
- margin-top: 6px;
468
-
469
- @include to-cq($layout-breakpoint) {
470
- grid-column: 1;
471
- }
472
- }
473
-
474
- /* Мета-строка истории: «канал · когда смотрели / сколько осталось» на месте строки канала */
434
+ /* Мета-строка истории: когда смотрели / сколько осталось */
475
435
  .card-video__meta {
476
436
  @include font-xs;
477
437
 
@@ -488,19 +448,6 @@ $dropdown-column: 18px;
488
448
  }
489
449
  }
490
450
 
491
- /* На узкой карточке (две колонки на телефоне) «канал · время» в строку не встают, а нешрикуемое
492
- время задаёт колонке min-content и разносит грид — оставляем только время просмотра */
493
- @include to-cq($layout-breakpoint) {
494
- .card-video__meta .card-video__channel-title {
495
- display: none;
496
- }
497
- }
498
-
499
- .card-video__meta .card-video__channel-title + .card-video__watched::before {
500
- content: '·';
501
- margin-inline: 6px;
502
- }
503
-
504
451
  .card-video__watched {
505
452
  display: flex;
506
453
  align-items: center;
@@ -519,29 +466,24 @@ $dropdown-column: 18px;
519
466
  border-radius: 50%;
520
467
  }
521
468
 
469
+ /* Кружок раскидан на обе строки: его 36px набираются заголовком плюс плашками, иначе под
470
+ однострочным заголовком остаётся пустой хвост высотой в остаток кружка */
522
471
  .card-video__logo-wrapper {
523
472
  grid-row: 1/3;
524
473
  grid-column: 1;
525
474
  }
526
475
 
527
- .card-video__logo-wrapper[href],
528
- .card-video__channel-wrapper[href] {
476
+ /* У видео без моделей плашек нет вовсе — второй строки тоже, кружку спан не по чему набирать */
477
+ .card-video__wrapper:not(:has(.card-video__tags, .card-video__meta)) .card-video__logo-wrapper {
478
+ grid-row: 1;
479
+ }
480
+
481
+ .card-video__logo-wrapper[href] {
529
482
  @include hover(opacity) {
530
483
  opacity: 0.9;
531
484
  }
532
485
  }
533
486
 
534
- .card-video__channel-title {
535
- @include font-xs;
536
-
537
- font-weight: 400;
538
- color: $video-card-secondary;
539
- overflow: hidden;
540
- white-space: nowrap;
541
- text-overflow: ellipsis;
542
- max-width: 100%;
543
- }
544
-
545
487
  .card-video.--mini {
546
488
  row-gap: 8px;
547
489
  }
@@ -561,13 +503,15 @@ $dropdown-column: 18px;
561
503
  z-index: $z-index-dropdown;
562
504
  }
563
505
 
564
- /* Ряд плашек лежит ниже «троеточия» — колонка под меню в этом ряду свободна, отдаём её плашкам */
506
+ /* Ряд плашек лежит ниже «троеточия» — колонка под меню в этом ряду свободна, отдаём её плашкам.
507
+ Строка задана явно: на авторазмещении плашки уезжали в третью строку, оставляя пустую вторую */
565
508
  .card-video__tags {
509
+ grid-row: 2;
566
510
  grid-column: 1 / -1;
567
511
 
568
512
  @include from-cq($layout-breakpoint) {
569
513
  grid-column: 2 / -1;
570
- margin-top: 10px;
514
+ margin-top: 6px;
571
515
  }
572
516
  }
573
517
 
@@ -204,20 +204,25 @@ function onFollowClick() {
204
204
  }
205
205
 
206
206
  const placeholders: Record<string, string> = {
207
- model: '/img/model-placeholder.webp',
207
+ model: '/img/model-placeholder-square.webp',
208
208
  channel: '/img/logo-placeholder.svg',
209
209
  };
210
- const placeholder = computed(() => placeholders[ props.type || 'channel' ]);
210
+ /** У канала заглушка — svg, avif-пары у неё нет. */
211
+ const avifPlaceholders: Record<string, string | undefined> = {
212
+ model: '/img/model-placeholder-square.avif',
213
+ };
214
+ const placeholderType = computed(() => props.type || 'channel');
215
+ const placeholder = computed(() => placeholders[ placeholderType.value ]);
211
216
  const imageError = ref(false);
212
- const currentImage = computed(() => {
213
- if (imageError.value) return placeholder.value;
214
- return props.card.img || placeholder.value;
215
- });
217
+ const isFallback = computed(() => imageError.value || !props.card.img);
218
+ const currentImage = computed(() => (isFallback.value ? placeholder.value : props.card.img));
216
219
 
217
220
  /** `.info-main__img` — 80px до sm, 96px дальше. `auto` впереди — см. `UiImg`. */
218
221
  const IMG_SIZES = `auto, (max-width: ${CSS_BREAKPOINTS.sm - 1}px) 80px, 96px`;
219
222
 
220
- const currentAvifSrc = computed(() => (imageError.value ? undefined : props.card.imgAvifUrl));
223
+ const currentAvifSrc = computed(() => (
224
+ isFallback.value ? avifPlaceholders[ placeholderType.value ] : props.card.imgAvifUrl
225
+ ));
221
226
 
222
227
  function getParameter(name: string) {
223
228
  return props.card.parameters?.find(p => p.name === name)?.values?.[ 0 ]?.name;
@@ -296,6 +301,7 @@ const socialItems = computed<ILinkItem[]>(() => {
296
301
 
297
302
  .info-main.--model .info-main__content {
298
303
  padding: 16px;
304
+ grid-template-columns: auto 1fr;
299
305
 
300
306
  @include from-br(sm) {
301
307
  padding: 20px;
@@ -46,8 +46,8 @@ const icon = computed(() => {
46
46
  const square = pickPortraitVariant(props.item?.portraits)?.square;
47
47
 
48
48
  return {
49
- webp: square?.webp || '/img/model-placeholder.webp',
50
- avif: square?.avif,
49
+ webp: square?.webp || '/img/model-placeholder-square.webp',
50
+ avif: square?.webp ? square.avif : '/img/model-placeholder-square.avif',
51
51
  };
52
52
  });
53
53
 
@@ -38,6 +38,7 @@
38
38
  <CardRound
39
39
  :card="item"
40
40
  :placeholder-avatar="placeholderAvatar"
41
+ :placeholder-avatar-avif="placeholderAvatarAvif"
41
42
  :without-count="withoutCount"
42
43
  class="section-swiper-cards__card"
43
44
  />
@@ -79,6 +80,8 @@ withDefaults(defineProps<{
79
80
  /** Вариант заголовка — см. `UiSectionTitle` */
80
81
  variant?: 'page' | 'section'
81
82
  placeholderAvatar?: string
83
+ /** Та же заглушка в avif — см. `CardRound` */
84
+ placeholderAvatarAvif?: string
82
85
  /** Карточки только с аватаром и названием, без счётчика роликов */
83
86
  withoutCount?: boolean
84
87
  }>(), {
@@ -92,7 +92,8 @@
92
92
  v-if="similarModels && similarModels.length > 0"
93
93
  :items="modelsRoundCards"
94
94
  :title="$t('similar_models')"
95
- placeholder-avatar="/img/model-placeholder.webp"
95
+ placeholder-avatar="/img/model-placeholder-square.webp"
96
+ placeholder-avatar-avif="/img/model-placeholder-square.avif"
96
97
  class="view-model-videos__other-models"
97
98
  hydrate-on-visible
98
99
  />
@@ -6,7 +6,8 @@
6
6
  v-if="topCards.length >= MINIMUM_TOP_THUMBS"
7
7
  :items="topCards"
8
8
  class="view-models__popular"
9
- placeholder-avatar="/img/model-placeholder.webp"
9
+ placeholder-avatar="/img/model-placeholder-square.webp"
10
+ placeholder-avatar-avif="/img/model-placeholder-square.avif"
10
11
  only-grid
11
12
  />
12
13
 
@@ -91,7 +91,8 @@
91
91
  :items="modelsRoundCards"
92
92
  :title="$t('related_models')"
93
93
  class="view-video__popular-models"
94
- placeholder-avatar="/img/model-placeholder.webp"
94
+ placeholder-avatar="/img/model-placeholder-square.webp"
95
+ placeholder-avatar-avif="/img/model-placeholder-square.avif"
95
96
  hydrate-on-visible
96
97
  />
97
98
 
@@ -49,7 +49,8 @@
49
49
  :all-link-text="$t('show_all_models')"
50
50
  :header-link-text-short="$t('all_models')"
51
51
  class="view-videos__popular-models"
52
- placeholder-avatar="/img/model-placeholder.webp"
52
+ placeholder-avatar="/img/model-placeholder-square.webp"
53
+ placeholder-avatar-avif="/img/model-placeholder-square.avif"
53
54
  hydrate-on-visible
54
55
  />
55
56
  </GridVideos>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.984",
4
+ "version": "0.0.987",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -55,7 +55,8 @@
55
55
  :items="modelsRoundCards"
56
56
  :title="$t('category_top_models_title', {name: title})"
57
57
  class="category__models"
58
- placeholder-avatar="/img/model-placeholder.webp"
58
+ placeholder-avatar="/img/model-placeholder-square.webp"
59
+ placeholder-avatar-avif="/img/model-placeholder-square.avif"
59
60
  />
60
61
  </template>
61
62
  </GridVideos>
@@ -52,7 +52,8 @@
52
52
  :items="modelsRoundCards"
53
53
  :title="t('channels_popular_models')"
54
54
  class="channel__videos-models"
55
- placeholder-avatar="/img/model-placeholder.webp"
55
+ placeholder-avatar="/img/model-placeholder-square.webp"
56
+ placeholder-avatar-avif="/img/model-placeholder-square.avif"
56
57
  hydrate-on-visible
57
58
  />
58
59
  </template>
@@ -18,7 +18,8 @@
18
18
  all-link="/library/models"
19
19
  icon="user"
20
20
  variant="section"
21
- placeholder-avatar="/img/model-placeholder.webp"
21
+ placeholder-avatar="/img/model-placeholder-square.webp"
22
+ placeholder-avatar-avif="/img/model-placeholder-square.avif"
22
23
  class="following__section"
23
24
  without-count
24
25
  />
@@ -49,7 +49,8 @@
49
49
  <template #default="item">
50
50
  <CardRound
51
51
  :card="item"
52
- placeholder-avatar="/img/model-placeholder.webp"
52
+ placeholder-avatar="/img/model-placeholder-square.webp"
53
+ placeholder-avatar-avif="/img/model-placeholder-square.avif"
53
54
  />
54
55
  </template>
55
56
  <template