itube-specs 0.0.891 → 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.
- package/assets/scss/themes/dark/_theme.scss +14 -1
- package/assets/scss/themes/light/_theme.scss +14 -1
- package/components/card/dropdown/trigger.vue +9 -5
- package/components/card/history.vue +6 -3
- package/components/card/video/index.vue +21 -8
- package/components/library/model.vue +307 -0
- package/components/ui/video-preview.vue +62 -0
- package/package.json +1 -1
- package/pages/history.vue +1 -0
- package/pages/library/index.vue +366 -0
- package/pages/library/models.vue +168 -0
- package/pages/library.vue +1 -361
- package/stubs/history.ts +17 -14
- package/stubs/library-models.ts +75 -0
- package/stubs/videos.ts +29 -0
- package/components/card/preview.vue +0 -139
|
@@ -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
|
-
<
|
|
4
|
-
v-if="
|
|
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
|
-
|
|
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
|
-
<
|
|
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;
|
|
@@ -227,11 +228,13 @@ $progress-height: 4px;
|
|
|
227
228
|
left: $inset-label;
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
/* Превью кроет постер целиком ($z-index-plus) — полоса прогресса должна остаться поверх него */
|
|
230
232
|
.card-history__progress {
|
|
231
233
|
position: absolute;
|
|
232
234
|
right: 0;
|
|
233
235
|
bottom: 0;
|
|
234
236
|
left: 0;
|
|
237
|
+
z-index: $z-index-loader;
|
|
235
238
|
height: $progress-height;
|
|
236
239
|
background-color: $history-card-progress-bg;
|
|
237
240
|
|
|
@@ -247,7 +250,7 @@ $progress-height: 4px;
|
|
|
247
250
|
.card-history__wrapper {
|
|
248
251
|
display: grid;
|
|
249
252
|
column-gap: 8px;
|
|
250
|
-
grid-template-columns: 32px 1fr
|
|
253
|
+
grid-template-columns: 32px 1fr $dropdown-column;
|
|
251
254
|
align-items: start;
|
|
252
255
|
padding: 12px;
|
|
253
256
|
color: $history-card-secondary;
|
|
@@ -341,7 +344,7 @@ $progress-height: 4px;
|
|
|
341
344
|
}
|
|
342
345
|
|
|
343
346
|
.card-history__wrapper {
|
|
344
|
-
grid-template-columns: 1fr
|
|
347
|
+
grid-template-columns: 1fr $dropdown-column;
|
|
345
348
|
}
|
|
346
349
|
|
|
347
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
|
-
<
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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