itube-specs 0.0.910 → 0.0.911

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.
@@ -1001,10 +1001,11 @@ $library-item-icon-border--hover: $color-indigo-500-40;
1001
1001
  $library-item-title: $color-white;
1002
1002
  $library-item-title--hover: $color-indigo-400;
1003
1003
  $library-item-subtitle: $color-zinc-500;
1004
- $library-item-badge-bg: $color-indigo-600-90;
1005
- $library-item-badge-text: $color-white;
1006
- $library-item-badge-bordered-border: $color-zinc-600;
1007
- $library-item-badge-bordered-text: $color-zinc-400;
1004
+ $library-item-avatar-bg: $color-zinc-700;
1005
+ $library-item-avatar-text: $color-zinc-200;
1006
+ $library-item-avatar-border: $color-zinc-900;
1007
+ $library-item-rest-bg: $color-zinc-800;
1008
+ $library-item-rest-text: $color-zinc-400;
1008
1009
 
1009
1010
  // ─── Library feed (блок подписки: модель / канал / категория) ───
1010
1011
  $library-feed-bg: $color-zinc-900-40;
@@ -1023,6 +1024,8 @@ $library-feed-age: $color-zinc-500;
1023
1024
  $library-feed-meta: $color-zinc-500;
1024
1025
  $library-feed-meta-dot: $color-zinc-600;
1025
1026
  $library-feed-title: $color-zinc-500;
1027
+ $library-feed-open: $color-zinc-400;
1028
+ $library-feed-open--hover: $color-white;
1026
1029
 
1027
1030
  // ─── Page: library ───
1028
1031
  $page-library-toggle-text: $color-zinc-300;
@@ -1029,10 +1029,11 @@ $library-item-icon-border--hover: transparent;
1029
1029
  $library-item-title: $color-gray-900;
1030
1030
  $library-item-title--hover: $color-indigo-600;
1031
1031
  $library-item-subtitle: $color-gray-500;
1032
- $library-item-badge-bg: $color-indigo-600;
1033
- $library-item-badge-text: $color-white;
1034
- $library-item-badge-bordered-border: $color-gray-300;
1035
- $library-item-badge-bordered-text: $color-gray-600;
1032
+ $library-item-avatar-bg: $color-gray-300;
1033
+ $library-item-avatar-text: $color-gray-700;
1034
+ $library-item-avatar-border: $color-gray-100;
1035
+ $library-item-rest-bg: $color-gray-200;
1036
+ $library-item-rest-text: $color-gray-600;
1036
1037
 
1037
1038
  // ─── Library feed (блок подписки: модель / канал / категория) ───
1038
1039
  $library-feed-bg: $color-gray-100;
@@ -1051,6 +1052,8 @@ $library-feed-age: $color-gray-500;
1051
1052
  $library-feed-meta: $color-gray-500;
1052
1053
  $library-feed-meta-dot: $color-gray-400;
1053
1054
  $library-feed-title: $color-gray-500;
1055
+ $library-feed-open: $color-gray-600;
1056
+ $library-feed-open--hover: $color-gray-900;
1054
1057
 
1055
1058
  // ─── Page: library ───
1056
1059
  $page-library-toggle-text: $color-gray-700;
@@ -49,7 +49,7 @@
49
49
 
50
50
  <span
51
51
  v-if="item.newCount"
52
- class="library-feed__badge _from-sm"
52
+ class="library-feed__badge"
53
53
  >{{ t('library.new_count', { count: item.newCount }) }}</span>
54
54
  </div>
55
55
 
@@ -108,10 +108,28 @@
108
108
  </div>
109
109
  </header>
110
110
 
111
- <!-- <h3
112
- v-if="item.feedTitle"
113
- class="library-feed__feed-title"
114
- >{{ item.feedTitle }}</h3> -->
111
+ <!-- На мобиле ссылки на сущность в шапке нет — она уезжает сюда, в строку над лентой -->
112
+ <div
113
+ v-if="item.feedTitle || item.openText"
114
+ class="library-feed__feed-head _to-sm"
115
+ >
116
+ <p
117
+ v-if="item.feedTitle"
118
+ class="library-feed__feed-title"
119
+ >{{ item.feedTitle }}</p>
120
+
121
+ <NuxtLink
122
+ v-if="item.openText"
123
+ :to="item.to"
124
+ class="library-feed__open"
125
+ >
126
+ {{ item.openText }}
127
+ <UiIcon
128
+ name="arrow-right"
129
+ size="16"
130
+ />
131
+ </NuxtLink>
132
+ </div>
115
133
 
116
134
  <div class="library-feed__grid">
117
135
  <CardVideo
@@ -329,13 +347,38 @@ const ageText = computed(() => {
329
347
  column-gap: 8px;
330
348
  }
331
349
 
332
- // .library-feed__feed-title {
333
- // @include font-sm;
350
+ .library-feed__feed-head {
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: space-between;
354
+ column-gap: 12px;
355
+ margin-bottom: 8px;
356
+ }
357
+
358
+ .library-feed__feed-title {
359
+ @include font-sm;
334
360
 
335
- // margin: 0 0 12px;
336
- // font-weight: 400;
337
- // color: $library-feed-title;
338
- // }
361
+ margin: 0;
362
+ font-weight: 400;
363
+ color: $library-feed-title;
364
+ }
365
+
366
+ /* Заголовка может не быть (лента без подписи) — ссылка всё равно держится правого края */
367
+ .library-feed__open {
368
+ @include font-sm;
369
+
370
+ display: flex;
371
+ align-items: center;
372
+ column-gap: 4px;
373
+ flex-shrink: 0;
374
+ margin-inline-start: auto;
375
+ white-space: nowrap;
376
+ color: $library-feed-open;
377
+
378
+ @include hover(color) {
379
+ color: $library-feed-open--hover;
380
+ }
381
+ }
339
382
 
340
383
  .library-feed__grid {
341
384
  display: grid;
@@ -1,4 +1,4 @@
1
- <!-- Карточка-ссылка библиотеки: иконка-плитка, счётчик, заголовок и опциональный бейдж -->
1
+ <!-- Карточка-ссылка библиотеки: иконка-плитка, ряд аватарок «что внутри» (десктоп), заголовок и счётчик -->
2
2
  <template>
3
3
  <NuxtLink
4
4
  :to="to"
@@ -8,55 +8,76 @@
8
8
  <span class="library-item__icon">
9
9
  <UiIcon
10
10
  :name="icon"
11
- size="20"
11
+ size="22"
12
12
  />
13
13
  </span>
14
- <span
15
- v-if="subtitle"
16
- class="library-item__subtitle _truncate"
17
- >{{ subtitle }}</span>
18
- </span>
19
14
 
20
- <span class="library-item__footer">
21
- <h3 class="library-item__title">{{ title }}</h3>
22
15
  <span
23
- v-if="badge"
24
- :class="`--${badgeTheme}`"
25
- class="library-item__badge"
16
+ v-if="avatars && avatars.length"
17
+ class="library-item__preview"
26
18
  >
27
- {{ badge }}
28
19
  <span
29
- v-if="badgeNote"
30
- class="library-item__badge-note _from-xs"
31
- >{{ badgeNote }}</span>
20
+ v-for="(avatar, index) in avatars"
21
+ :key="`${avatar.title}-${index}`"
22
+ :style="{ zIndex: avatars.length - index }"
23
+ :title="avatar.title"
24
+ class="library-item__avatar"
25
+ >
26
+ <UiImg
27
+ v-if="avatar.avatarUrl && !failedAvatars.includes(index)"
28
+ :src="avatar.avatarUrl"
29
+ :alt="avatar.title"
30
+ class="library-item__photo"
31
+ width="40"
32
+ height="40"
33
+ @error="failedAvatars.push(index)"
34
+ />
35
+ <span
36
+ v-else
37
+ class="library-item__initials"
38
+ >{{ getNameInitials(avatar.title) }}</span>
39
+ </span>
40
+
41
+ <span
42
+ v-if="restCount"
43
+ class="library-item__rest"
44
+ >+{{ restCount }}</span>
32
45
  </span>
33
46
  </span>
47
+
48
+ <h3 class="library-item__title">{{ title }}</h3>
49
+
50
+ <span
51
+ v-if="subtitle"
52
+ class="library-item__subtitle"
53
+ >{{ subtitle }}</span>
34
54
  </NuxtLink>
35
55
  </template>
36
56
 
37
57
  <script setup lang="ts">
38
- withDefaults(defineProps<{
58
+ import { getNameInitials } from '../../utils/player-channel/get-name-initials';
59
+ import type { ILibraryItemAvatar } from '../../types';
60
+
61
+ defineProps<{
39
62
  to: string
40
63
  icon: string
41
64
  title: string
42
- /** Счётчик справа от иконки: «128 videos», «3 private · 1 public» */
65
+ /** Счётчик под заголовком: «128 videos», «3 private · 1 public» */
43
66
  subtitle?: string
44
- /** Бейдж у заголовка: «3 new», «+214» */
45
- badge?: string
46
- /** Хвост бейджа, скрытый на мобиле: «this week» */
47
- badgeNote?: string
48
- badgeTheme?: 'accent' | 'bordered'
49
- }>(), {
50
- badgeTheme: 'accent'
51
- });
67
+ /** Первые подписки внутри раздела — ряд аватарок, виден только на широкой карточке */
68
+ avatars?: ILibraryItemAvatar[]
69
+ /** Сколько подписок не влезло в ряд: плашка «+8» в его хвосте */
70
+ restCount?: number
71
+ }>();
72
+
73
+ const failedAvatars = ref<number[]>([]);
52
74
  </script>
53
75
 
54
76
  <style lang="scss">
55
77
  .library-item {
78
+ container-type: inline-size;
56
79
  display: flex;
57
80
  flex-direction: column;
58
- row-gap: 32px;
59
- justify-content: space-between;
60
81
  padding: 20px;
61
82
  background-color: $library-item-bg;
62
83
  border: 1px solid $library-item-border;
@@ -71,16 +92,17 @@ withDefaults(defineProps<{
71
92
  .library-item__head {
72
93
  display: flex;
73
94
  align-items: center;
74
- justify-content: space-between;
75
- column-gap: 8px;
95
+ column-gap: 12px;
96
+ min-height: 44px;
97
+ margin-bottom: 32px;
76
98
  }
77
99
 
78
100
  .library-item__icon {
79
101
  display: grid;
80
102
  place-content: center;
81
103
  flex-shrink: 0;
82
- width: 44px;
83
- height: 44px;
104
+ width: 48px;
105
+ height: 48px;
84
106
  color: $library-item-icon;
85
107
  background-color: $library-item-icon-bg;
86
108
  border: 1px solid $library-item-icon-border;
@@ -98,18 +120,70 @@ withDefaults(defineProps<{
98
120
  }
99
121
  }
100
122
 
123
+ /* Ряд аватарок занимает больше половины карточки: показываем, только когда она реально широкая */
124
+ .library-item__preview {
125
+ display: none;
126
+ }
127
+
128
+ @include from-br(sm) {
129
+ @include from-cq(220px) {
130
+ .library-item__preview {
131
+ display: flex;
132
+ align-items: center;
133
+ }
134
+ }
135
+ }
136
+
137
+ .library-item__avatar,
138
+ .library-item__rest {
139
+ display: grid;
140
+ place-content: center;
141
+ overflow: hidden;
142
+ flex-shrink: 0;
143
+ width: 40px;
144
+ height: 40px;
145
+ border: 2px solid $library-item-avatar-border;
146
+ border-radius: $radius-full;
147
+ }
148
+
149
+ .library-item__avatar {
150
+ background-color: $library-item-avatar-bg;
151
+ }
152
+
153
+ .library-item__avatar:not(:first-child),
154
+ .library-item__rest {
155
+ margin-inline-start: -11px;
156
+ }
157
+
158
+ .library-item__photo {
159
+ width: 100%;
160
+ height: 100%;
161
+ object-fit: cover;
162
+ object-position: center 30%;
163
+ }
164
+
165
+ .library-item__initials {
166
+ @include font-3xs;
167
+
168
+ font-weight: 700;
169
+ color: $library-item-avatar-text;
170
+ }
171
+
172
+ .library-item__rest {
173
+ @include font-3xs;
174
+
175
+ font-weight: 600;
176
+ color: $library-item-rest-text;
177
+ background-color: $library-item-rest-bg;
178
+ }
179
+
101
180
  .library-item__subtitle {
102
181
  @include font-sm;
103
182
 
183
+ margin-top: 4px;
104
184
  color: $library-item-subtitle;
105
185
  }
106
186
 
107
- .library-item__footer {
108
- display: flex;
109
- align-items: center;
110
- column-gap: 8px;
111
- }
112
-
113
187
  .library-item__title {
114
188
  @include font-base;
115
189
 
@@ -127,26 +201,4 @@ withDefaults(defineProps<{
127
201
  color: $library-item-title--hover;
128
202
  }
129
203
  }
130
-
131
- .library-item__badge {
132
- @include font-3xs;
133
-
134
- flex-shrink: 0;
135
- font-weight: 600;
136
- white-space: nowrap;
137
- padding-block: 2px;
138
- padding-inline: 8px;
139
- border: 1px solid transparent;
140
- border-radius: $radius-full;
141
- }
142
-
143
- .library-item__badge.--accent {
144
- color: $library-item-badge-text;
145
- background-color: $library-item-badge-bg;
146
- }
147
-
148
- .library-item__badge.--bordered {
149
- color: $library-item-badge-bordered-text;
150
- border-color: $library-item-badge-bordered-border;
151
- }
152
204
  </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.910",
4
+ "version": "0.0.911",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -35,6 +35,7 @@ const items = computed<ILibraryFeedItem[]>(() => getLibraryCategoriesStub().map(
35
35
  videosCount: category.videosCount,
36
36
  // Нечего показать в новинках — лента откатывается на лучшее за неделю
37
37
  feedTitle: category.newCount ? t('library.latest') : t('library.top_this_week'),
38
+ openText: t('library.open_category'),
38
39
  videos: category.videos,
39
40
  })));
40
41
 
@@ -35,6 +35,8 @@ const items = computed<ILibraryFeedItem[]>(() => getLibraryChannelsStub().map(ch
35
35
  { icon: 'date', text: t('library.updated_at', { date: formatDate(channel.lastVideoAddedAt) }), desktopOnly: true },
36
36
  ],
37
37
  videosCount: channel.videosCount,
38
+ feedTitle: t('library.latest'),
39
+ openText: t('library.open_channel'),
38
40
  videos: channel.videos,
39
41
  })));
40
42
 
@@ -111,9 +111,8 @@
111
111
  :icon="item.icon"
112
112
  :title="item.title"
113
113
  :subtitle="item.subtitle"
114
- :badge="item.badge"
115
- :badge-note="item.badgeNote"
116
- :badge-theme="item.badgeTheme"
114
+ :avatars="item.avatars"
115
+ :rest-count="item.restCount"
117
116
  />
118
117
  </li>
119
118
  </ul>
@@ -135,8 +134,9 @@
135
134
 
136
135
  <script setup lang="ts">
137
136
  import { getHistoryStub } from '../../stubs/history';
137
+ import { getLibraryChannelsStub, getLibraryModelsStub } from '../../stubs/library-follows';
138
138
  import { libraryStub } from '../../stubs/library';
139
- import type { IBreadcrumbItem, IVideoCard } from '../../types';
139
+ import type { IBreadcrumbItem, ILibraryItemAvatar, IVideoCard } from '../../types';
140
140
 
141
141
  /** Карточка-ссылка библиотеки в разметке страницы. */
142
142
  type LibraryLink = {
@@ -144,13 +144,15 @@ type LibraryLink = {
144
144
  icon: string
145
145
  title: string
146
146
  subtitle?: string
147
- badge?: string
148
- badgeNote?: string
149
- badgeTheme?: 'accent' | 'bordered'
147
+ avatars?: ILibraryItemAvatar[]
148
+ restCount?: number
150
149
  };
151
150
 
152
151
  const CONTINUE_LIMIT = 4;
153
152
 
153
+ /** Сколько аватарок помещается в ряд «что внутри» — остальные уходят в плашку «+N». */
154
+ const AVATARS_LIMIT = 4;
155
+
154
156
  const { t } = useI18n();
155
157
 
156
158
  const { generateLink } = useGenerateLink();
@@ -197,36 +199,43 @@ const collections = computed<LibraryLink[]>(() => [
197
199
  },
198
200
  ]);
199
201
 
202
+ /** Первые подписки раздела: ряд аватарок на карточке показывает, что внутри. */
203
+ const modelAvatars: ILibraryItemAvatar[] = getLibraryModelsStub()
204
+ .slice(0, AVATARS_LIMIT)
205
+ .map(model => ({ title: model.title, avatarUrl: model.avatarUrl }));
206
+
207
+ const channelAvatars: ILibraryItemAvatar[] = getLibraryChannelsStub()
208
+ .slice(0, AVATARS_LIMIT)
209
+ .map(channel => ({ title: channel.title, avatarUrl: channel.avatarUrl }));
210
+
200
211
  const following = computed<LibraryLink[]>(() => [
201
212
  {
202
213
  to: generateLink('/library/models'),
203
214
  icon: 'user',
204
215
  title: t('models'),
205
216
  subtitle: t('plural.models', libraryStub.models),
206
- badge: t('library.new_count', { count: libraryStub.newModels }),
217
+ avatars: modelAvatars,
218
+ restCount: Math.max(libraryStub.models - AVATARS_LIMIT, 0),
207
219
  },
208
220
  {
209
221
  to: generateLink('/library/channels'),
210
222
  icon: 'play-circle',
211
223
  title: t('channels'),
212
224
  subtitle: t('plural.channels', libraryStub.channels),
213
- badge: t('library.new_count', { count: libraryStub.newChannels }),
225
+ avatars: channelAvatars,
226
+ restCount: Math.max(libraryStub.channels - AVATARS_LIMIT, 0),
214
227
  },
215
228
  {
216
229
  to: generateLink('/library/categories'),
217
230
  icon: 'grid',
218
231
  title: t('categories'),
219
232
  subtitle: t('plural.categories', libraryStub.categories),
220
- badge: `+${libraryStub.categoryVideosPerWeek}`,
221
- badgeNote: t('library.this_week'),
222
- badgeTheme: 'bordered',
223
233
  },
224
234
  {
225
235
  to: generateLink('/playlists'),
226
236
  icon: 'playlist',
227
237
  title: t('playlists'),
228
238
  subtitle: t('plural.playlists', libraryStub.playlists),
229
- badge: t('library.new_count', { count: libraryStub.newPlaylists }),
230
239
  },
231
240
  ]);
232
241
 
@@ -36,6 +36,8 @@ const items = computed<ILibraryFeedItem[]>(() => getLibraryModelsStub().map(mode
36
36
  { icon: 'date', text: t('library.updated_at', { date: formatDate(model.lastVideoAddedAt) }), desktopOnly: true },
37
37
  ],
38
38
  videosCount: model.videosCount,
39
+ feedTitle: t('library.latest'),
40
+ openText: t('library.open_profile'),
39
41
  videos: model.videos,
40
42
  })));
41
43
 
package/stubs/library.ts CHANGED
@@ -10,7 +10,6 @@ export const libraryStub = {
10
10
  channels: 5,
11
11
  newChannels: 1,
12
12
  categories: 4,
13
- categoryVideosPerWeek: 214,
14
13
  playlists: 7,
15
14
  newPlaylists: 2,
16
15
  };
package/types/index.d.ts CHANGED
@@ -50,6 +50,7 @@ export * from './link-item.d.ts';
50
50
  export * from './fluid-player.d.ts';
51
51
  export * from './chips-item.d.ts';
52
52
  export * from './library-feed.d.ts';
53
+ export * from './library-item.d.ts';
53
54
  export * from './nav-link-item.d.ts';
54
55
  export * from './paginated-response.d.ts';
55
56
  export * from './page-seo.d.ts';
@@ -36,5 +36,7 @@ export interface ILibraryFeedItem {
36
36
  videosCount: number
37
37
  /** Подпись над лентой: «Latest» / «Top this week». Нет — лента без подписи */
38
38
  feedTitle?: string
39
+ /** Текст ссылки на сущность в строке над лентой: «Open profile» / «Open channel» */
40
+ openText?: string
39
41
  videos: IVideoCard[]
40
42
  }
@@ -0,0 +1,6 @@
1
+ /** Аватарка в ряду «что внутри» на карточке библиотеки: фото подписки или инициалы вместо него. */
2
+ export interface ILibraryItemAvatar {
3
+ title: string
4
+ /** Нет — вместо картинки рисуются инициалы */
5
+ avatarUrl?: string
6
+ }