itube-specs 0.0.892 → 0.0.893

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,25 @@ $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 model (блок подписки на модель) ───
968
+ $library-model-bg: $color-zinc-900-40;
969
+ $library-model-border: $color-zinc-800;
970
+ $library-model-avatar-border: $color-zinc-700;
971
+ $library-model-avatar-border--hover: $color-zinc-500;
972
+ $library-model-name: $color-white;
973
+ $library-model-name--hover: $color-indigo-400;
974
+ $library-model-badge-bg: $color-indigo-600;
975
+ $library-model-badge-text: $color-white;
976
+ $library-model-tags: $color-zinc-400;
977
+ $library-model-meta: $color-zinc-500;
978
+ $library-model-meta-dot: $color-zinc-600;
979
+
968
980
  // ─── Page: library ───
969
981
  $page-library-heading: $color-white;
970
982
  $page-library-heading-icon: $color-white;
971
983
  $page-library-heading-icon-bg: $mono-white-10;
972
984
  $page-library-toggle-text: $color-zinc-300;
985
+ $page-library-confirm-text: $color-zinc-300;
973
986
 
974
987
  // ─── Page: history ───
975
988
  $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,25 @@ $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 model (блок подписки на модель) ───
998
+ $library-model-bg: $color-gray-100;
999
+ $library-model-border: transparent;
1000
+ $library-model-avatar-border: $color-gray-300;
1001
+ $library-model-avatar-border--hover: $color-indigo-500-50;
1002
+ $library-model-name: $color-gray-900;
1003
+ $library-model-name--hover: $color-indigo-600;
1004
+ $library-model-badge-bg: $color-indigo-600;
1005
+ $library-model-badge-text: $color-white;
1006
+ $library-model-tags: $color-gray-600;
1007
+ $library-model-meta: $color-gray-500;
1008
+ $library-model-meta-dot: $color-gray-400;
1009
+
998
1010
  // ─── Page: library ───
999
1011
  $page-library-heading: $color-gray-900;
1000
1012
  $page-library-heading-icon: $color-gray-900;
1001
1013
  $page-library-heading-icon-bg: $color-gray-200;
1002
1014
  $page-library-toggle-text: $color-gray-700;
1015
+ $page-library-confirm-text: $color-gray-700;
1003
1016
 
1004
1017
  // ─── Page: history ───
1005
1018
  $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,307 @@
1
+ <!-- Блок подписки на модель: шапка с аватаром, тегами и счётчиками + лента последних роликов -->
2
+ <template>
3
+ <article class="library-model">
4
+ <header class="library-model__header">
5
+ <NuxtLink
6
+ :to="link"
7
+ :title="model.title"
8
+ class="library-model__avatar-link"
9
+ >
10
+ <UiImg
11
+ :src="currentImage"
12
+ :alt="model.title"
13
+ :loading="priority ? 'eager' : 'lazy'"
14
+ :priority="priority"
15
+ class="library-model__avatar"
16
+ width="96"
17
+ height="96"
18
+ sizes="96px"
19
+ @error="imageError = true"
20
+ />
21
+ </NuxtLink>
22
+
23
+ <div class="library-model__info">
24
+ <div class="library-model__heading">
25
+ <NuxtLink
26
+ :to="link"
27
+ class="library-model__name"
28
+ >
29
+ {{ model.title }}
30
+ <img
31
+ v-if="model.country"
32
+ :alt="model.country.title"
33
+ :title="`${t('birthplace_country')}: ${model.country.title}`"
34
+ :src="`/icons/flags/${model.country.name}.svg`"
35
+ class="library-model__flag"
36
+ width="20"
37
+ height="14"
38
+ >
39
+ </NuxtLink>
40
+
41
+ <span
42
+ v-if="model.newCount"
43
+ class="library-model__badge"
44
+ >{{ t('library.new_count', { count: model.newCount }) }}</span>
45
+ </div>
46
+
47
+ <p
48
+ v-if="model.tags.length"
49
+ class="library-model__tags"
50
+ >{{ model.tags.join(' · ') }}</p>
51
+
52
+ <p class="library-model__meta">
53
+ <span class="library-model__meta-item">
54
+ <UiIcon
55
+ name="video-camera"
56
+ size="14"
57
+ />
58
+ <span class="library-model__meta-videos">{{ $n(model.videosCount, 'compact') }}</span>
59
+ {{ model.age }} {{ t('yo') }}
60
+ </span>
61
+
62
+ <span class="library-model__meta-item">
63
+ <UiIcon
64
+ name="date"
65
+ size="14"
66
+ />
67
+ {{ t('library.updated_at', { date: formatDate(model.lastVideoAddedAt) }) }}
68
+ </span>
69
+ </p>
70
+ </div>
71
+
72
+ <div class="library-model__actions _from-sm">
73
+ <UiBtn
74
+ :theme="following ? 'secondary' : 'primary'"
75
+ class="library-model__follow"
76
+ @click="emit('toggle-follow', model)"
77
+ >
78
+ <UiIcon
79
+ :name="following ? 'check' : 'plus'"
80
+ size="16"
81
+ />
82
+ {{ following ? t('library.following_state') : t('follow') }}
83
+ </UiBtn>
84
+
85
+ <UiLink
86
+ :to="link"
87
+ theme="secondary"
88
+ >
89
+ {{ t('plural.videos', model.videosCount) }}
90
+ <UiIcon
91
+ name="arrow-right"
92
+ size="16"
93
+ />
94
+ </UiLink>
95
+ </div>
96
+ </header>
97
+
98
+ <div class="library-model__grid">
99
+ <CardVideo
100
+ v-for="(video, index) in model.videos"
101
+ :key="video.guid"
102
+ :card="video"
103
+ :loading="priority && index < 3 ? 'eager' : 'lazy'"
104
+ :priority="priority && index < 3"
105
+ compact
106
+ />
107
+ </div>
108
+
109
+ <UiLink
110
+ :to="link"
111
+ theme="secondary"
112
+ class="library-model__all _to-sm"
113
+ wide
114
+ >
115
+ {{ t('plural.videos', model.videosCount) }}
116
+ <UiIcon
117
+ name="arrow-right"
118
+ size="16"
119
+ />
120
+ </UiLink>
121
+ </article>
122
+ </template>
123
+
124
+ <script setup lang="ts">
125
+ import { convertString, formatDate } from '../../runtime';
126
+ import type { LibraryModelStub } from '../../stubs/library-models';
127
+
128
+ const props = defineProps<{
129
+ model: LibraryModelStub
130
+ following?: boolean
131
+ priority?: boolean
132
+ }>();
133
+
134
+ const emit = defineEmits<{
135
+ (eventName: 'toggle-follow', model: LibraryModelStub): void,
136
+ }>();
137
+
138
+ const { t } = useI18n();
139
+ const { generateLink } = useGenerateLink();
140
+
141
+ const link = computed(() => generateLink(`/models/${convertString().toSlug(props.model.title)}`));
142
+
143
+ const fallbackImage = '/img/model-placeholder.webp';
144
+ const imageError = ref(false);
145
+
146
+ const currentImage = computed(() => imageError.value ? fallbackImage : props.model.primaryImageUrl);
147
+ </script>
148
+
149
+ <style lang="scss">
150
+ .library-model {
151
+ padding: 16px;
152
+ background-color: $library-model-bg;
153
+ border: 1px solid $library-model-border;
154
+ border-radius: $radius-xl;
155
+
156
+ @include from-br(sm) {
157
+ padding: 20px;
158
+ }
159
+ }
160
+
161
+ .library-model__header {
162
+ display: grid;
163
+ grid-template-columns: auto 1fr;
164
+ column-gap: 16px;
165
+ row-gap: 12px;
166
+ align-items: start;
167
+ margin-bottom: 20px;
168
+
169
+ @include from-br(sm) {
170
+ grid-template-columns: auto 1fr auto;
171
+ }
172
+ }
173
+
174
+ .library-model__avatar {
175
+ width: 80px;
176
+ height: 80px;
177
+ object-fit: cover;
178
+ object-position: center 30%;
179
+ border-radius: $radius-full;
180
+ border: 2px solid $library-model-avatar-border;
181
+
182
+ @include from-br(sm) {
183
+ width: 96px;
184
+ height: 96px;
185
+ transition: var(--transition) border-color;
186
+ }
187
+ }
188
+
189
+ @include from-br(sm) {
190
+ .library-model__avatar-link:is(:hover, :focus-visible) .library-model__avatar {
191
+ border-color: $library-model-avatar-border--hover;
192
+ }
193
+ }
194
+
195
+ .library-model__info {
196
+ min-width: 0;
197
+ }
198
+
199
+ .library-model__heading {
200
+ display: flex;
201
+ flex-wrap: wrap;
202
+ align-items: center;
203
+ column-gap: 10px;
204
+ row-gap: 6px;
205
+ }
206
+
207
+ .library-model__name {
208
+ @include font-lg;
209
+
210
+ display: flex;
211
+ align-items: center;
212
+ column-gap: 8px;
213
+ font-weight: 700;
214
+ text-transform: capitalize;
215
+ color: $library-model-name;
216
+
217
+ @include from-br(sm) {
218
+ transition: var(--transition) color;
219
+ }
220
+
221
+ @include hover(color) {
222
+ color: $library-model-name--hover;
223
+ }
224
+ }
225
+
226
+ .library-model__flag {
227
+ flex-shrink: 0;
228
+ border-radius: 2px;
229
+ }
230
+
231
+ .library-model__badge {
232
+ @include font-3xs;
233
+
234
+ flex-shrink: 0;
235
+ font-weight: 600;
236
+ white-space: nowrap;
237
+ padding-block: 4px;
238
+ padding-inline: 10px;
239
+ color: $library-model-badge-text;
240
+ background-color: $library-model-badge-bg;
241
+ border-radius: $radius-full;
242
+ }
243
+
244
+ .library-model__tags {
245
+ @include font-sm;
246
+
247
+ margin-top: 4px;
248
+ color: $library-model-tags;
249
+ }
250
+
251
+ .library-model__meta {
252
+ @include font-sm;
253
+
254
+ display: flex;
255
+ flex-wrap: wrap;
256
+ align-items: center;
257
+ column-gap: 16px;
258
+ row-gap: 4px;
259
+ margin-top: 2px;
260
+ color: $library-model-meta;
261
+ }
262
+
263
+ .library-model__meta-item {
264
+ display: flex;
265
+ align-items: center;
266
+ column-gap: 6px;
267
+ }
268
+
269
+ .library-model__meta-videos::after {
270
+ content: '·';
271
+ margin-inline-start: 6px;
272
+ color: $library-model-meta-dot;
273
+ }
274
+
275
+ .library-model__actions {
276
+ display: flex;
277
+ align-items: center;
278
+ column-gap: 8px;
279
+ }
280
+
281
+ .library-model__follow {
282
+ min-width: 124px;
283
+ }
284
+
285
+ .library-model__grid {
286
+ display: grid;
287
+ gap: 16px;
288
+ grid-template-columns: repeat(2, 1fr);
289
+
290
+ @include from-br(xm) {
291
+ grid-template-columns: repeat(3, 1fr);
292
+ }
293
+
294
+ @include from-br(sm) {
295
+ gap: 24px 16px;
296
+ grid-template-columns: repeat(4, 1fr);
297
+ }
298
+
299
+ @include from-br(md) {
300
+ grid-template-columns: repeat(5, 1fr);
301
+ }
302
+ }
303
+
304
+ .library-model__all {
305
+ margin-top: 16px;
306
+ }
307
+ </style>
@@ -0,0 +1,62 @@
1
+ <!-- Видео-предпросмотр (автоплей) поверх постера + индикатор загрузки -->
2
+ <template>
3
+ <span
4
+ :class="{'_loading': !isPlaying}"
5
+ class="ui-video-preview"
6
+ >
7
+ <video
8
+ ref="videoRef"
9
+ :src="url"
10
+ :class="{'--playing': isPlaying}"
11
+ class="ui-video-preview__video"
12
+ loop
13
+ muted
14
+ autoplay
15
+ playsinline
16
+ @playing="isPlaying = true"
17
+ @waiting="isPlaying = false"
18
+ @pause="isPlaying = false"
19
+ />
20
+ </span>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ defineProps<{
25
+ url: string
26
+ }>();
27
+
28
+ const videoRef = ref<HTMLVideoElement | null>(null);
29
+ const isPlaying = ref(false);
30
+
31
+ onMounted(() => {
32
+ const video = videoRef.value;
33
+ if (!video) return;
34
+
35
+ video.muted = true;
36
+ video.play().catch(() => {
37
+ isPlaying.value = false;
38
+ });
39
+ });
40
+ </script>
41
+
42
+ <style lang="scss">
43
+ .ui-video-preview {
44
+ --size: 40px;
45
+
46
+ position: absolute;
47
+ inset: 0;
48
+ z-index: $z-index-plus;
49
+ }
50
+
51
+ .ui-video-preview__video {
52
+ width: 100%;
53
+ height: 100%;
54
+ object-fit: cover;
55
+ opacity: 0;
56
+ }
57
+
58
+ /* Пока автоплей не стартовал, браузер рисует поверх видео нативную кнопку плей — держим элемент невидимым */
59
+ .ui-video-preview__video.--playing {
60
+ opacity: 1;
61
+ }
62
+ </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.892",
4
+ "version": "0.0.893",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {