itube-specs 0.0.892 → 0.0.895

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.
@@ -204,7 +204,6 @@ $video-card-mini-title: $color-zinc-300;
204
204
  $video-card-mini-weight: 400;
205
205
  $video-card-user-text: $color-zinc-400;
206
206
  $video-card-user-bg: $color-zinc-700;
207
- $card-preview-loader-bg: linear-gradient(to right, $color-indigo-muted, $color-indigo-deep);
208
207
 
209
208
  // ─── History card ───
210
209
  $history-card-bg: $color-zinc-900-40;
@@ -965,11 +964,29 @@ $library-item-badge-text: $color-white;
965
964
  $library-item-badge-bordered-border: $color-zinc-600;
966
965
  $library-item-badge-bordered-text: $color-zinc-400;
967
966
 
967
+ // ─── Library feed (блок подписки: модель / канал / категория) ───
968
+ $library-feed-bg: $color-zinc-900-40;
969
+ $library-feed-border: $color-zinc-800;
970
+ $library-feed-fade: $color-zinc-950;
971
+ $library-feed-avatar-border: $color-zinc-700;
972
+ $library-feed-avatar-border--hover: $color-zinc-500;
973
+ $library-feed-initials: $color-zinc-400;
974
+ $library-feed-initials-gradient-from: $color-zinc-700;
975
+ $library-feed-initials-gradient-to: $color-zinc-800;
976
+ $library-feed-name: $color-white;
977
+ $library-feed-name--hover: $color-indigo-400;
978
+ $library-feed-badge-bg: $color-indigo-600;
979
+ $library-feed-badge-text: $color-white;
980
+ $library-feed-meta: $color-zinc-500;
981
+ $library-feed-meta-dot: $color-zinc-600;
982
+ $library-feed-title: $color-zinc-500;
983
+
968
984
  // ─── Page: library ───
969
985
  $page-library-heading: $color-white;
970
986
  $page-library-heading-icon: $color-white;
971
987
  $page-library-heading-icon-bg: $mono-white-10;
972
988
  $page-library-toggle-text: $color-zinc-300;
989
+ $page-library-confirm-text: $color-zinc-300;
973
990
 
974
991
  // ─── Page: history ───
975
992
  $page-history-group-title: $color-zinc-300;
@@ -261,7 +261,6 @@ $video-card-mini-title: $color-gray-900;
261
261
  $video-card-mini-weight: 500;
262
262
  $video-card-user-text: $color-gray-700;
263
263
  $video-card-user-bg: $color-gray-300;
264
- $card-preview-loader-bg: linear-gradient(to right, $color-indigo-muted, $color-indigo-deep);
265
264
  $video-card-dropdown-size--hover: 1.25;
266
265
 
267
266
  // ─── History card ───
@@ -995,11 +994,29 @@ $library-item-badge-text: $color-white;
995
994
  $library-item-badge-bordered-border: $color-gray-300;
996
995
  $library-item-badge-bordered-text: $color-gray-600;
997
996
 
997
+ // ─── Library feed (блок подписки: модель / канал / категория) ───
998
+ $library-feed-bg: $color-gray-100;
999
+ $library-feed-border: transparent;
1000
+ $library-feed-fade: $color-gray-100;
1001
+ $library-feed-avatar-border: $color-gray-300;
1002
+ $library-feed-avatar-border--hover: $color-indigo-500-50;
1003
+ $library-feed-initials: $color-gray-600;
1004
+ $library-feed-initials-gradient-from: $color-gray-200;
1005
+ $library-feed-initials-gradient-to: $color-gray-300;
1006
+ $library-feed-name: $color-gray-900;
1007
+ $library-feed-name--hover: $color-indigo-600;
1008
+ $library-feed-badge-bg: $color-indigo-600;
1009
+ $library-feed-badge-text: $color-white;
1010
+ $library-feed-meta: $color-gray-500;
1011
+ $library-feed-meta-dot: $color-gray-400;
1012
+ $library-feed-title: $color-gray-500;
1013
+
998
1014
  // ─── Page: library ───
999
1015
  $page-library-heading: $color-gray-900;
1000
1016
  $page-library-heading-icon: $color-gray-900;
1001
1017
  $page-library-heading-icon-bg: $color-gray-200;
1002
1018
  $page-library-toggle-text: $color-gray-700;
1019
+ $page-library-confirm-text: $color-gray-700;
1003
1020
 
1004
1021
  // ─── Page: history ───
1005
1022
  $page-history-group-title: $color-gray-900;
@@ -1,7 +1,8 @@
1
1
  <!-- Кнопка меню карточки: до первого клика в DOM только она, чанк самого меню грузится по требованию -->
2
2
  <template>
3
- <LazyCardDropdown
4
- v-if="isMounted"
3
+ <component
4
+ v-if="dropdown"
5
+ :is="dropdown"
5
6
  v-bind="{ ...$attrs, ...props }"
6
7
  auto-open
7
8
  />
@@ -21,6 +22,7 @@
21
22
  </template>
22
23
 
23
24
  <script setup lang="ts">
25
+ import type { Component } from 'vue';
24
26
  import type { IVideoCard } from '../../../types';
25
27
 
26
28
  defineOptions({ inheritAttrs: false });
@@ -35,11 +37,13 @@ const props = defineProps<{
35
37
 
36
38
  const { t } = useI18n();
37
39
 
38
- const isMounted = ref(false);
40
+ // Не LazyCardDropdown: async-обёртка на первом рендере отдаёт коммент-заглушку вместо элемента,
41
+ // колонка грида схлопывается и текст карточки успевает дёрнуться. Кладём уже загруженный компонент —
42
+ // подмена кнопки на меню происходит одним патчем.
43
+ const dropdown = shallowRef<Component | null>(null);
39
44
 
40
45
  async function mountDropdown() {
41
- await import('./index.vue');
42
- isMounted.value = true;
46
+ dropdown.value = (await import('./index.vue')).default;
43
47
  }
44
48
  </script>
45
49
 
@@ -20,7 +20,7 @@
20
20
  height="270"
21
21
  @error="posterError = true"
22
22
  />
23
- <CardPreview
23
+ <UiVideoPreview
24
24
  v-if="isPreviewShow && card.previewUrl"
25
25
  :url="card.previewUrl"
26
26
  />
@@ -179,6 +179,7 @@ const videoCard = computed<IVideoCard>(() => ({
179
179
  <style lang="scss">
180
180
  $inset-label: 8px;
181
181
  $progress-height: 4px;
182
+ $dropdown-column: 18px;
182
183
 
183
184
  .card-history {
184
185
  @include video-card;
@@ -249,7 +250,7 @@ $progress-height: 4px;
249
250
  .card-history__wrapper {
250
251
  display: grid;
251
252
  column-gap: 8px;
252
- grid-template-columns: 32px 1fr auto;
253
+ grid-template-columns: 32px 1fr $dropdown-column;
253
254
  align-items: start;
254
255
  padding: 12px;
255
256
  color: $history-card-secondary;
@@ -343,7 +344,7 @@ $progress-height: 4px;
343
344
  }
344
345
 
345
346
  .card-history__wrapper {
346
- grid-template-columns: 1fr auto;
347
+ grid-template-columns: 1fr $dropdown-column;
347
348
  }
348
349
 
349
350
  .card-history__title,
@@ -3,6 +3,7 @@
3
3
  <div
4
4
  :class="[
5
5
  {'--mini': mini},
6
+ {'--compact': compact},
6
7
  {'--avatar': card.channelAvatar},
7
8
  {'--no-channel': isChannelPage},
8
9
  ]"
@@ -27,7 +28,7 @@
27
28
  height="270"
28
29
  @error="onPosterError"
29
30
  />
30
- <CardPreview
31
+ <UiVideoPreview
31
32
  v-if="isPreviewShow && card.previewUrl"
32
33
  :url="card.previewUrl"
33
34
  />
@@ -81,7 +82,7 @@
81
82
  >{{ card.title }}
82
83
  </NuxtLink>
83
84
  <component
84
- v-if="!mini && card.channelName"
85
+ v-if="!mini && !compact && card.channelName"
85
86
  :is="component"
86
87
  :to="channelLink"
87
88
  class="card-video__channel-wrapper"
@@ -96,7 +97,7 @@
96
97
  </span>
97
98
  </component>
98
99
  <div
99
- v-if="!mini && cardTags && cardTags.length > 0"
100
+ v-if="!mini && !compact && cardTags && cardTags.length > 0"
100
101
  class="card-video__tags"
101
102
  >
102
103
  <div class="card-video__tags-wrapper">
@@ -130,6 +131,8 @@ const featureFlags = useRuntimeConfig().public.featureFlags;
130
131
  const props = defineProps<{
131
132
  card: IVideoCard
132
133
  mini?: boolean
134
+ /** Аватар канала и тайтл, но без названия канала и тегов — лента подписок */
135
+ compact?: boolean
133
136
  loading?: 'eager' | 'lazy'
134
137
  topChips?: string[]
135
138
  playlistId?: string
@@ -202,6 +205,10 @@ const cardTags = computed(() => {
202
205
  $inset-label: 8px;
203
206
  $layout-breakpoint: 250px;
204
207
 
208
+ // Колонка под меню карточки фиксирована, а не auto: иначе ширина зависит от того, отрисовано меню
209
+ // или нет, и в момент подмены кнопки заголовок дёргается
210
+ $dropdown-column: 18px;
211
+
205
212
  .card-video {
206
213
  @include video-card;
207
214
 
@@ -276,7 +283,7 @@ $layout-breakpoint: 250px;
276
283
 
277
284
  .card-video__wrapper {
278
285
  display: grid;
279
- grid-template-columns: 1fr auto;
286
+ grid-template-columns: 1fr $dropdown-column;
280
287
  align-items: start;
281
288
  align-content: center;
282
289
  color: $video-card-secondary;
@@ -287,13 +294,13 @@ $layout-breakpoint: 250px;
287
294
 
288
295
  @include from-cq($layout-breakpoint) {
289
296
  column-gap: 8px;
290
- grid-template-columns: 32px 1fr auto;
297
+ grid-template-columns: 32px 1fr $dropdown-column;
291
298
  }
292
299
  }
293
300
 
294
301
  .--avatar .card-video__wrapper {
295
302
  column-gap: 8px;
296
- grid-template-columns: 32px 1fr auto;
303
+ grid-template-columns: 32px 1fr $dropdown-column;
297
304
  }
298
305
 
299
306
  .--avatar .card-video__title {
@@ -321,7 +328,7 @@ $layout-breakpoint: 250px;
321
328
  }
322
329
 
323
330
  .--no-channel .card-video__wrapper {
324
- grid-template-columns: 1fr auto;
331
+ grid-template-columns: 1fr $dropdown-column;
325
332
  }
326
333
 
327
334
  .--no-channel .card-video__title {
@@ -339,7 +346,13 @@ $layout-breakpoint: 250px;
339
346
  }
340
347
 
341
348
  .--mini .card-video__wrapper {
342
- grid-template-columns: 1fr auto;
349
+ grid-template-columns: 1fr $dropdown-column;
350
+ }
351
+
352
+ /* Без строки канала и тегов вторая строка грида не нужна — иначе аватар и «троеточие» растягиваются. */
353
+ .--compact .card-video__logo-wrapper,
354
+ .--compact .card-video__dropdown {
355
+ grid-row: 1;
343
356
  }
344
357
 
345
358
  .--mini .card-video__dropdown {
@@ -0,0 +1,340 @@
1
+ <!-- Блок подписки (модель / канал / категория): шапка с аватаром, тегами и счётчиками + лента роликов -->
2
+ <template>
3
+ <article class="library-feed">
4
+ <header class="library-feed__header">
5
+ <NuxtLink
6
+ :to="item.to"
7
+ :title="item.title"
8
+ class="library-feed__avatar-link"
9
+ >
10
+ <UiImg
11
+ v-if="currentAvatar"
12
+ :src="currentAvatar"
13
+ :alt="item.title"
14
+ :loading="priority ? 'eager' : 'lazy'"
15
+ :priority="priority"
16
+ class="library-feed__avatar"
17
+ width="96"
18
+ height="96"
19
+ sizes="96px"
20
+ @error="avatarError = true"
21
+ />
22
+ <span
23
+ v-else
24
+ class="library-feed__avatar library-feed__initials"
25
+ >{{ initials }}</span>
26
+ </NuxtLink>
27
+
28
+ <div class="library-feed__info">
29
+ <div class="library-feed__heading">
30
+ <NuxtLink
31
+ :to="item.to"
32
+ class="library-feed__name"
33
+ >
34
+ {{ item.title }}
35
+ <img
36
+ v-if="item.country"
37
+ :alt="item.country.title"
38
+ :title="`${t('birthplace_country')}: ${item.country.title}`"
39
+ :src="`/icons/flags/${item.country.name}.svg`"
40
+ class="library-feed__flag"
41
+ width="20"
42
+ height="14"
43
+ >
44
+ </NuxtLink>
45
+
46
+ <span
47
+ v-if="item.newCount"
48
+ class="library-feed__badge"
49
+ >{{ t('library.new_count', { count: item.newCount }) }}</span>
50
+ </div>
51
+
52
+ <LazyUiExpandRow
53
+ v-if="item.tags && item.tags.length"
54
+ :items="item.tags"
55
+ class="library-feed__tags"
56
+ hydrate-on-visible
57
+ micro
58
+ />
59
+
60
+ <p class="library-feed__meta">
61
+ <span
62
+ v-for="(entry, index) in item.meta"
63
+ :key="`meta-${index}`"
64
+ class="library-feed__meta-item"
65
+ >
66
+ <UiIcon
67
+ v-if="entry.icon"
68
+ :name="entry.icon"
69
+ size="14"
70
+ />
71
+ {{ entry.text }}
72
+ </span>
73
+ </p>
74
+ </div>
75
+
76
+ <div class="library-feed__actions _from-sm">
77
+ <UiBtn
78
+ :theme="following ? 'secondary' : 'primary'"
79
+ class="library-feed__follow"
80
+ @click="emit('toggle-follow', item)"
81
+ >
82
+ <UiIcon
83
+ :name="following ? 'check' : 'plus'"
84
+ size="16"
85
+ />
86
+ {{ following ? t('library.following_state') : t('follow') }}
87
+ </UiBtn>
88
+
89
+ <UiLink
90
+ :to="item.to"
91
+ theme="secondary"
92
+ >
93
+ {{ videosText }}
94
+ <UiIcon
95
+ name="arrow-right"
96
+ size="16"
97
+ />
98
+ </UiLink>
99
+ </div>
100
+ </header>
101
+
102
+ <h3
103
+ v-if="item.feedTitle"
104
+ class="library-feed__feed-title"
105
+ >{{ item.feedTitle }}</h3>
106
+
107
+ <div class="library-feed__grid">
108
+ <CardVideo
109
+ v-for="(video, index) in item.videos"
110
+ :key="video.guid"
111
+ :card="video"
112
+ :loading="priority && index < 3 ? 'eager' : 'lazy'"
113
+ :priority="priority && index < 3"
114
+ compact
115
+ />
116
+ </div>
117
+
118
+ <UiLink
119
+ :to="item.to"
120
+ theme="secondary"
121
+ class="library-feed__all _to-sm"
122
+ wide
123
+ >
124
+ {{ videosText }}
125
+ <UiIcon
126
+ name="arrow-right"
127
+ size="16"
128
+ />
129
+ </UiLink>
130
+ </article>
131
+ </template>
132
+
133
+ <script setup lang="ts">
134
+ import type { ILibraryFeedItem } from '../../types';
135
+
136
+ const props = defineProps<{
137
+ item: ILibraryFeedItem
138
+ following?: boolean
139
+ priority?: boolean
140
+ }>();
141
+
142
+ const emit = defineEmits<{
143
+ (eventName: 'toggle-follow', item: ILibraryFeedItem): void,
144
+ }>();
145
+
146
+ const { t } = useI18n();
147
+
148
+ const avatarError = ref(false);
149
+
150
+ const currentAvatar = computed(() => avatarError.value ? undefined : props.item.avatarUrl);
151
+
152
+ /** Заглушка вместо аватара: «Top To Bottom» → TTB, «Bareback» → B. */
153
+ const initials = computed(() => props.item.title
154
+ .split(/\s+/)
155
+ .slice(0, 3)
156
+ .map(word => word.charAt(0))
157
+ .join('')
158
+ .toUpperCase());
159
+
160
+ const videosText = computed(() => t('plural.videos', props.item.videosCount));
161
+ </script>
162
+
163
+ <style lang="scss">
164
+ .library-feed {
165
+ /* Растушёвка «ещё» у тегов уходит в фон карточки, а не страницы */
166
+ --expand-row-fade-end: #{$library-feed-fade};
167
+
168
+ padding: 16px;
169
+ background-color: $library-feed-bg;
170
+ border: 1px solid $library-feed-border;
171
+ border-radius: $radius-xl;
172
+
173
+ @include from-br(sm) {
174
+ padding: 20px;
175
+ }
176
+ }
177
+
178
+ .library-feed__header {
179
+ display: grid;
180
+ grid-template-columns: auto minmax(0, 1fr);
181
+ column-gap: 16px;
182
+ row-gap: 12px;
183
+ align-items: start;
184
+ margin-bottom: 20px;
185
+
186
+ @include from-br(sm) {
187
+ grid-template-columns: auto minmax(0, 1fr) auto;
188
+ }
189
+ }
190
+
191
+ .library-feed__avatar {
192
+ width: 80px;
193
+ height: 80px;
194
+ object-fit: cover;
195
+ object-position: center 30%;
196
+ border-radius: $radius-full;
197
+ border: 2px solid $library-feed-avatar-border;
198
+
199
+ @include from-br(sm) {
200
+ width: 96px;
201
+ height: 96px;
202
+ transition: var(--transition) border-color;
203
+ }
204
+ }
205
+
206
+ @include from-br(sm) {
207
+ .library-feed__avatar-link:is(:hover, :focus-visible) .library-feed__avatar {
208
+ border-color: $library-feed-avatar-border--hover;
209
+ }
210
+ }
211
+
212
+ .library-feed__initials {
213
+ @include font-lg;
214
+
215
+ display: grid;
216
+ place-content: center;
217
+ font-weight: 700;
218
+ text-transform: uppercase;
219
+ color: $library-feed-initials;
220
+ background: linear-gradient(to bottom right, $library-feed-initials-gradient-from, $library-feed-initials-gradient-to);
221
+ }
222
+
223
+ .library-feed__info {
224
+ min-width: 0;
225
+ }
226
+
227
+ .library-feed__heading {
228
+ display: flex;
229
+ flex-wrap: wrap;
230
+ align-items: center;
231
+ column-gap: 10px;
232
+ row-gap: 6px;
233
+ }
234
+
235
+ .library-feed__name {
236
+ @include font-lg;
237
+
238
+ display: flex;
239
+ align-items: center;
240
+ column-gap: 8px;
241
+ font-weight: 700;
242
+ text-transform: uppercase;
243
+ color: $library-feed-name;
244
+
245
+ @include from-br(sm) {
246
+ transition: var(--transition) color;
247
+ }
248
+
249
+ @include hover(color) {
250
+ color: $library-feed-name--hover;
251
+ }
252
+ }
253
+
254
+ .library-feed__flag {
255
+ flex-shrink: 0;
256
+ border-radius: 2px;
257
+ }
258
+
259
+ .library-feed__badge {
260
+ @include font-xs;
261
+
262
+ flex-shrink: 0;
263
+ font-weight: 600;
264
+ white-space: nowrap;
265
+ padding-block: 4px;
266
+ padding-inline: 10px;
267
+ color: $library-feed-badge-text;
268
+ background-color: $library-feed-badge-bg;
269
+ border-radius: $radius-full;
270
+ }
271
+
272
+ .library-feed__tags {
273
+ margin-top: 8px;
274
+ }
275
+
276
+ .library-feed__meta {
277
+ @include font-sm;
278
+
279
+ display: flex;
280
+ flex-wrap: wrap;
281
+ align-items: center;
282
+ column-gap: 8px;
283
+ row-gap: 4px;
284
+ margin-top: 8px;
285
+ color: $library-feed-meta;
286
+ }
287
+
288
+ .library-feed__meta-item {
289
+ display: flex;
290
+ align-items: center;
291
+ column-gap: 6px;
292
+ }
293
+
294
+ .library-feed__meta-item:not(:last-child)::after {
295
+ content: '·';
296
+ margin-inline-start: 2px;
297
+ color: $library-feed-meta-dot;
298
+ }
299
+
300
+ .library-feed__actions {
301
+ display: flex;
302
+ align-items: center;
303
+ column-gap: 8px;
304
+ }
305
+
306
+ .library-feed__follow {
307
+ min-width: 124px;
308
+ }
309
+
310
+ .library-feed__feed-title {
311
+ @include font-sm;
312
+
313
+ margin: 0 0 12px;
314
+ font-weight: 400;
315
+ color: $library-feed-title;
316
+ }
317
+
318
+ .library-feed__grid {
319
+ display: grid;
320
+ gap: 16px;
321
+ grid-template-columns: repeat(2, 1fr);
322
+
323
+ @include from-br(xm) {
324
+ grid-template-columns: repeat(3, 1fr);
325
+ }
326
+
327
+ @include from-br(sm) {
328
+ gap: 24px 16px;
329
+ grid-template-columns: repeat(4, 1fr);
330
+ }
331
+
332
+ @include from-br(md) {
333
+ grid-template-columns: repeat(5, 1fr);
334
+ }
335
+ }
336
+
337
+ .library-feed__all {
338
+ margin-top: 16px;
339
+ }
340
+ </style>