itube-specs 0.0.1018 → 0.0.1020

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/SITE_UPDATES.md CHANGED
@@ -11,6 +11,21 @@
11
11
  коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
12
12
  кому именно (всем / сайтам с N), что будет, если не сделать.
13
13
 
14
+ ## 0.0.1019 — 2026-10-08
15
+
16
+ - **Сайтам со своей темой** (`assets/scss/vars/_theme.scss` в сайте, как у hdsex-remaster): завести токены
17
+ строки над плеером и описания — как в `themes/dark/_theme.scss` слоя:
18
+ `$player-tags-chip-bg`, `$player-tags-tag-bg`, `$player-tags-more-text`, `$player-tags-more-text--hover`,
19
+ `$video-info-handler-text--hover`. Плюс по макету изменились значения: `$video-channel-bg` → `$color-zinc-800`,
20
+ `$video-info-handler-text` → `$color-zinc-500`. Не завести — сборка падает на undefined variable. Образец —
21
+ коммит hdsex-remaster с переходом на эту версию.
22
+ - **Сайтам с форком `components/view/video.vue`:** строку над плеером `<UiExpandRow …>` заменить на
23
+ `<PlayerTags …>` (те же `items` и слот `prepend`) — новая раскладка по макету: «More +N» после влезших чипсов,
24
+ теги светлее категорий, на мобиле строка листается. Не заменить — у сайта останется старая строка.
25
+ - **Сайтам с хуком попандера на карточке видео** (`pu-trigger` на `<CardVideo>`): класс перенести в проп
26
+ `link-class="pu-trigger"` — он ляжет только на ссылки на ролик (превью и тайтл), меню и кнопки карточки
27
+ перестанут открывать попандер. Остальным — ничего.
28
+
14
29
  ## 0.0.1013 — 2026-10-07
15
30
 
16
31
  - **Всем сайтам — чистка своих переводов (`i18n/locales/*.json`, во всех локалях):** образец —
@@ -238,7 +238,8 @@ $video-menu-preview-border: $color-zinc-800;
238
238
 
239
239
  // ─── Video info ───
240
240
  $video-info-item-text: $color-zinc-500-70;
241
- $video-info-handler-text: $color-zinc-400;
241
+ $video-info-handler-text: $color-zinc-500;
242
+ $video-info-handler-text--hover: $color-white;
242
243
  $video-info-description-text: $color-zinc-400;
243
244
 
244
245
  // ─── Video interface ───
@@ -253,7 +254,7 @@ $video-interface-mobile-label: $color-zinc-300;
253
254
  $video-interface-mobile-border: $color-zinc-800;
254
255
 
255
256
  // ─── Video channel ───
256
- $video-channel-bg: $color-zinc-800-60;
257
+ $video-channel-bg: $color-zinc-800;
257
258
  $video-channel-initials-gradient-from: $color-indigo-500;
258
259
  $video-channel-initials-gradient-to: $color-purple-600;
259
260
  $video-channel-initials-text: $color-white;
@@ -1171,3 +1172,9 @@ $switcher-subtitle--active: $color-white-60;
1171
1172
  $card-webcam-languages-border: $color-white-20;
1172
1173
  $card-webcam-languages-fallback-bg: $color-zinc-800;
1173
1174
  $card-webcam-languages-fallback-icon: $color-zinc-300;
1175
+
1176
+ // ─── Player tags (строка над плеером) ───
1177
+ $player-tags-chip-bg: $color-zinc-800;
1178
+ $player-tags-tag-bg: $color-zinc-800-50;
1179
+ $player-tags-more-text: $color-zinc-400;
1180
+ $player-tags-more-text--hover: $color-white;
@@ -295,6 +295,7 @@ $video-menu-preview-border: transparent;
295
295
  // ─── Video info ───
296
296
  $video-info-item-text: $color-gray-400;
297
297
  $video-info-handler-text: $color-gray-400;
298
+ $video-info-handler-text--hover: $color-gray-900;
298
299
  $video-info-description-text: $color-gray-700;
299
300
 
300
301
  // ─── Video interface ───
@@ -1198,3 +1199,9 @@ $switcher-subtitle--active: $color-white-60;
1198
1199
  $card-webcam-languages-border: $color-gray-300;
1199
1200
  $card-webcam-languages-fallback-bg: $color-gray-200;
1200
1201
  $card-webcam-languages-fallback-icon: $color-gray-600;
1202
+
1203
+ // ─── Player tags (строка над плеером) ───
1204
+ $player-tags-chip-bg: $color-gray-200;
1205
+ $player-tags-tag-bg: $color-gray-100;
1206
+ $player-tags-more-text: $color-gray-700;
1207
+ $player-tags-more-text--hover: $color-gray-900;
@@ -13,6 +13,7 @@
13
13
  >
14
14
  <NuxtLink
15
15
  :to="link"
16
+ :class="linkClass"
16
17
  class="card-video__poster-wrapper"
17
18
  @mouseenter.passive="startPreviewByHover(previewId)"
18
19
  @mouseleave.passive="stopPreviewByHover(previewId)"
@@ -91,6 +92,7 @@
91
92
  <NuxtLink
92
93
  :to="link"
93
94
  :title="card.title"
95
+ :class="linkClass"
94
96
  class="card-video__title _truncate"
95
97
  >{{ card.title }}
96
98
  </NuxtLink>
@@ -153,6 +155,8 @@ const props = defineProps<{
153
155
  history?: boolean
154
156
  /** Развилка мета-строки истории: вместо «когда смотрели» — сколько осталось до конца («Продолжить просмотр») */
155
157
  remaining?: boolean
158
+ /** Класс на ссылки на ролик (превью и тайтл) — например хук попандера сайта, чтобы меню и кнопки карточки его не задевали */
159
+ linkClass?: string
156
160
  }>();
157
161
 
158
162
  const emit = defineEmits<{
@@ -73,9 +73,9 @@ function onSubscribeClick() {
73
73
  .player-channel {
74
74
  display: flex;
75
75
  align-items: center;
76
- column-gap: 8px;
76
+ column-gap: 14px;
77
77
  height: var(--chips-height);
78
- padding-right: 8px;
78
+ padding-right: 14px;
79
79
  background-color: $video-channel-bg;
80
80
  border-radius: $radius;
81
81
  overflow: hidden;
@@ -95,7 +95,7 @@ function onSubscribeClick() {
95
95
  }
96
96
 
97
97
  .player-channel__initials {
98
- @include font-3xs;
98
+ @include font-4xs;
99
99
 
100
100
  display: flex;
101
101
  align-items: center;
@@ -109,19 +109,22 @@ function onSubscribeClick() {
109
109
  }
110
110
 
111
111
  .player-channel__name {
112
- @include font-sm;
112
+ @include font-2xs;
113
113
 
114
114
  color: $video-channel-name;
115
- transition: color var(--transition);
116
115
  white-space: nowrap;
117
116
 
118
- &:hover {
117
+ @include from-br(sm) {
118
+ @include font-sm;
119
+ }
120
+
121
+ @include hover(color) {
119
122
  color: $video-channel-name--hover;
120
123
  }
121
124
  }
122
125
 
123
126
  .player-channel__subscribe {
124
- @include font-xs;
127
+ @include font-2xs;
125
128
 
126
129
  flex-shrink: 0;
127
130
  color: $video-channel-subscribe-text;
@@ -132,15 +135,23 @@ function onSubscribeClick() {
132
135
  background-color: $video-channel-subscribe-bg;
133
136
  border-radius: $radius-md;
134
137
 
138
+ @include from-br(sm) {
139
+ @include font-sm;
140
+ }
141
+
135
142
  @include hover(background-color) {
136
143
  background-color: $video-channel-subscribe-bg--hover;
137
144
  }
138
145
  }
139
146
 
140
147
  .player-channel__count {
141
- @include font-xs;
148
+ @include font-2xs;
142
149
 
143
150
  color: $video-channel-count;
144
151
  white-space: nowrap;
152
+
153
+ @include from-br(sm) {
154
+ @include font-sm;
155
+ }
145
156
  }
146
157
  </style>
@@ -188,6 +188,7 @@ function onAddPlaylistClick() {
188
188
 
189
189
  @include from-br(sm) {
190
190
  display: flex;
191
+ margin-left: -6px;
191
192
  }
192
193
  }
193
194
 
@@ -224,8 +225,8 @@ function onAddPlaylistClick() {
224
225
  align-items: center;
225
226
  row-gap: 0;
226
227
  column-gap: 6px;
227
- padding: 6px 12px;
228
- font-weight: 500;
228
+ padding: 8px 12px;
229
+ border-radius: $radius-md;
229
230
  white-space: nowrap;
230
231
  background-color: var(--button-bg, #{$video-interface-button-bg});
231
232
  color: var(--button-text, #{$video-interface-button-text});
@@ -272,26 +273,29 @@ function onAddPlaylistClick() {
272
273
 
273
274
  .player-interface__description {
274
275
  display: grid;
275
- grid-template-columns: 1fr 20px;
276
- align-items: center;
277
- column-gap: 8px;
276
+ grid-template-columns: 1fr auto;
277
+ align-items: end;
278
+ column-gap: 12px;
278
279
  }
279
280
 
280
281
  .player-interface__description-handler {
281
- height: 1lh;
282
+ @include font-sm;
283
+
284
+ display: flex;
285
+ align-items: center;
286
+ height: 1.625em;
287
+ padding-left: 6px;
282
288
  color: $video-info-handler-text;
283
289
  cursor: pointer;
290
+
291
+ @include hover(color) {
292
+ color: $video-info-handler-text--hover;
293
+ }
284
294
  }
285
295
 
286
296
  .player-interface__description-icon {
297
+ margin-right: -4px;
287
298
  transition: var(--transition) transform;
288
-
289
- @include from-br(sm) {
290
- $size: 16px;
291
-
292
- width: $size;
293
- height: $size;
294
- }
295
299
  }
296
300
 
297
301
  .player-interface__description-text {
@@ -149,8 +149,8 @@ const likePercent = computed(() => getReactionPercents(resultLikes.value, result
149
149
  @include from-br(sm) {
150
150
  flex-direction: row;
151
151
  row-gap: 0;
152
- column-gap: 6px;
153
- padding: 6px;
152
+ column-gap: 12px;
153
+ padding: 6px 0 6px 6px;
154
154
  border-radius: $radius-small;
155
155
  color: $like-icon;
156
156
  }
@@ -0,0 +1,224 @@
1
+ <!-- Строка над плеером: канал и модели (слот), категории и теги ролика. Видно столько чипсов, сколько
2
+ влезло в строку вместе с кнопкой «More +N»; остальные остаются в разметке (их читает краулер), но скрыты
3
+ до раскрытия. На мобиле строка листается вбок, видны первые MOBILE_VISIBLE чипсов. -->
4
+ <template>
5
+ <div
6
+ ref="rowRef"
7
+ :class="{'--open': isOpen}"
8
+ class="player-tags"
9
+ >
10
+ <slot name="prepend" />
11
+ <UiChips
12
+ v-for="(item, index) in items"
13
+ :key="`${index}-${item.title}`"
14
+ :item="item"
15
+ :class="{
16
+ '--tag': item.prefix === 'tag',
17
+ '--hidden': !isOpen && index >= visibleCount,
18
+ }"
19
+ class="player-tags__chip"
20
+ is-link
21
+ />
22
+ <button
23
+ v-show="hiddenCount > 0 || isOpen"
24
+ ref="moreRef"
25
+ :aria-expanded="isOpen"
26
+ class="player-tags__more"
27
+ type="button"
28
+ @click="isOpen = !isOpen"
29
+ >
30
+ {{ moreLabel }}
31
+ <UiIcon
32
+ class="player-tags__more-icon"
33
+ name="chevron-down"
34
+ size="12"
35
+ />
36
+ </button>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import type { IChipsItem } from '../../types';
42
+ import { CSS_BREAKPOINTS } from '../../config/css-breakpoints';
43
+
44
+ const props = defineProps<{
45
+ items: IChipsItem[]
46
+ }>();
47
+
48
+ /** Сколько чипсов видно на мобиле до раскрытия — строка там листается, «влезло» не считается. */
49
+ const MOBILE_VISIBLE = 4;
50
+
51
+ const { t } = useI18n();
52
+
53
+ const rowRef = ref<HTMLElement>();
54
+ const moreRef = ref<HTMLElement>();
55
+ const isOpen = ref(false);
56
+ // До замера на клиенте видны все — разметка сервера и первого рендера клиента совпадает
57
+ const visibleCount = ref(props.items.length);
58
+
59
+ const hiddenCount = computed(() => Math.max(props.items.length - visibleCount.value, 0));
60
+
61
+ const upperFirst = (text: string) => text.charAt(0).toUpperCase() + text.slice(1);
62
+ const moreLabel = computed(() => upperFirst(isOpen.value ? t('show_less') : `${t('more')} +${hiddenCount.value}`));
63
+
64
+ /** Ширины чипсов меряются один раз, пока все видны: скрытые `display: none` ширины не имеют. */
65
+ let chipWidths: number[] = [];
66
+
67
+ function measureChips() {
68
+ const row = rowRef.value;
69
+ if (!row) return;
70
+
71
+ chipWidths = Array.from(row.querySelectorAll<HTMLElement>(':scope > .player-tags__chip')).map(chip => chip.offsetWidth);
72
+ }
73
+
74
+ function layout() {
75
+ const row = rowRef.value;
76
+ if (!row || isOpen.value) return;
77
+
78
+ if (window.innerWidth < CSS_BREAKPOINTS.sm) {
79
+ visibleCount.value = Math.min(MOBILE_VISIBLE, props.items.length);
80
+ return;
81
+ }
82
+
83
+ const gap = parseFloat(getComputedStyle(row).columnGap) || 0;
84
+ const prependWidth = (Array.from(row.children) as HTMLElement[])
85
+ .filter(child => !child.classList.contains('player-tags__chip') && child !== moreRef.value && child.offsetWidth > 0)
86
+ .reduce((sum, child) => sum + child.offsetWidth + gap, 0);
87
+ const available = row.clientWidth - prependWidth;
88
+ const total = chipWidths.reduce((sum, width) => sum + width + gap, 0) - gap;
89
+
90
+ if (total <= available) {
91
+ visibleCount.value = props.items.length;
92
+ return;
93
+ }
94
+
95
+ // ponytail: ширина кнопки — по тексту «More +88»; при другом числе цифр расхождение в пару px
96
+ const moreWidth = moreRef.value?.offsetWidth || 0;
97
+ let used = 0;
98
+ let count = 0;
99
+
100
+ for (const width of chipWidths) {
101
+ if (used + width + gap + moreWidth > available) break;
102
+ used += width + gap;
103
+ count++;
104
+ }
105
+
106
+ visibleCount.value = count;
107
+ }
108
+
109
+ async function remeasure() {
110
+ isOpen.value = false;
111
+ visibleCount.value = props.items.length;
112
+ await nextTick();
113
+ measureChips();
114
+ layout();
115
+ // кнопка показалась только сейчас — с её реальной шириной раскладка может сдвинуться на чипсу
116
+ await nextTick();
117
+ layout();
118
+ }
119
+
120
+ watch(() => props.items, remeasure);
121
+
122
+ let resizeObserver: ResizeObserver | null = null;
123
+
124
+ onMounted(() => {
125
+ remeasure();
126
+ resizeObserver = new ResizeObserver(() => layout());
127
+ if (rowRef.value) resizeObserver.observe(rowRef.value);
128
+ });
129
+
130
+ onBeforeUnmount(() => {
131
+ resizeObserver?.disconnect();
132
+ });
133
+ </script>
134
+
135
+ <style lang="scss">
136
+ .player-tags {
137
+ --chips-height: 36px;
138
+
139
+ /* строка не растягивает предков своей шириной: на мобиле она листается, а не раздвигает страницу */
140
+ contain: inline-size;
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 8px;
144
+ margin-inline: calc(-1 * var(--container-padding-inline));
145
+ padding-inline: var(--container-padding-inline);
146
+ overflow-x: auto;
147
+ scrollbar-width: none;
148
+
149
+ @include from-br(sm) {
150
+ flex-wrap: wrap;
151
+ margin-inline: 0;
152
+ padding-inline: 0;
153
+ overflow: visible;
154
+ }
155
+ }
156
+
157
+ .player-tags::-webkit-scrollbar {
158
+ display: none;
159
+ }
160
+
161
+ /* До замера на клиенте строка — одна линия, без мигания второго ряда */
162
+ .player-tags:not(.--open) {
163
+ @include from-br(sm) {
164
+ max-height: var(--chips-height);
165
+ overflow: hidden;
166
+ }
167
+ }
168
+
169
+ .player-tags .ui-chips {
170
+ @include font-2xs;
171
+
172
+ flex-shrink: 0;
173
+ border: 1px solid transparent;
174
+ border-radius: $radius;
175
+ background-color: $player-tags-chip-bg;
176
+
177
+ @include from-br(sm) {
178
+ @include font-sm;
179
+ }
180
+ }
181
+
182
+ .player-tags .ui-chips.--tag {
183
+ background-color: $player-tags-tag-bg;
184
+ }
185
+
186
+ .player-tags__chip.--hidden {
187
+ display: none;
188
+ }
189
+
190
+ .player-tags__more {
191
+ @include font-2xs;
192
+
193
+ display: flex;
194
+ flex-shrink: 0;
195
+ align-items: center;
196
+ column-gap: 4px;
197
+ padding: 6px 4px;
198
+ color: $player-tags-more-text;
199
+ white-space: nowrap;
200
+
201
+ @include from-br(sm) {
202
+ @include font-sm;
203
+ }
204
+
205
+ @include hover(color) {
206
+ color: $player-tags-more-text--hover;
207
+ }
208
+ }
209
+
210
+ .player-tags__more-icon {
211
+ width: 14px;
212
+ height: 14px;
213
+ transition: var(--transition) transform;
214
+
215
+ @include from-br(sm) {
216
+ width: 12px;
217
+ height: 12px;
218
+ }
219
+ }
220
+
221
+ .player-tags.--open .player-tags__more-icon {
222
+ transform: rotate(180deg);
223
+ }
224
+ </style>
@@ -6,7 +6,7 @@
6
6
  <span class="_truncate">{{ data.title }}</span>
7
7
  </h1>
8
8
  <div class="view-video__header-wrapper">
9
- <UiExpandRow
9
+ <PlayerTags
10
10
  v-if="topChips && topChips.length > 0"
11
11
  :items="topChips"
12
12
  class="view-video__header-categories"
@@ -27,7 +27,7 @@
27
27
  />
28
28
  </template>
29
29
  </template>
30
- </UiExpandRow>
30
+ </PlayerTags>
31
31
  </div>
32
32
  </header>
33
33
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.1018",
4
+ "version": "0.0.1020",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {