itube-specs 0.0.958 → 0.0.959

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.
@@ -868,6 +868,8 @@ $bg-fade-90: $color-zinc-950-90; // почти непрозрачный
868
868
  // ─── Model card ───
869
869
  $model-card-bg: $color-zinc-900-50;
870
870
  $model-card-bg--hover: $color-zinc-800-50;
871
+ $model-card-thumb-bg: $color-zinc-800;
872
+ $model-card-separator: $color-zinc-600;
871
873
  $model-card-border: $color-zinc-800-50;
872
874
  $model-card-border--hover: $color-zinc-700-50;
873
875
  $model-card-params: $color-zinc-500;
@@ -894,6 +894,8 @@ $bg-fade-90: $color-gray-950-90; // почти непрозрачный
894
894
  // ─── Model card ───
895
895
  $model-card-bg: $color-gray-100;
896
896
  $model-card-bg--hover: $model-card-bg;
897
+ $model-card-thumb-bg: $color-gray-200;
898
+ $model-card-separator: $color-gray-400;
897
899
  $model-card-border: transparent;
898
900
  $model-card-border--hover: $model-card-border;
899
901
  $model-card-params: $color-gray-600;
@@ -1,82 +1,83 @@
1
- <!-- Карточка модели: аватар, параметры, страна, счётчики видео/обновлений -->
1
+ <!-- Карточка модели: портрет 3:4, имя со страной, параметры и счётчики видео/обновлений -->
2
2
  <template>
3
3
  <NuxtLink
4
4
  :to="generateLink(`/models/${convertString().toSlug(card.title)}`)"
5
5
  :title="card.title"
6
6
  class="card-model"
7
7
  >
8
- <div class="card-model__inner">
8
+ <div class="card-model__thumb">
9
9
  <UiImg
10
10
  :src="currentImage"
11
11
  :loading="loading"
12
12
  :alt="card.title"
13
13
  :priority="priority"
14
+ :sizes="`(max-width: ${CSS_BREAKPOINTS.xs}px) 50vw, 240px`"
14
15
  class="card-model__img"
15
- width="64"
16
- height="64"
17
- sizes="xs:64px sm:64px md:64px lg:64px"
16
+ width="240"
17
+ height="320"
18
18
  @error="onImageError"
19
19
  />
20
- <div class="card-model__wrapper">
21
- <p class="card-model__info">
22
- <span class="card-model__info-name">
23
- {{ card.title }}
24
- </span>
25
- <span
26
- v-if="card.country && card.country.name"
27
- :title="`${t('birthplace_country')}: ${card.country.title}`"
28
- class="card-model__country-wrapper"
29
- >
30
- <img
31
- :alt="card.country.title"
32
- :src="`/icons/flags/${card.country.name}.svg`"
33
- class="card-model__info-country"
34
- width="16"
35
- height="12"
36
- >
37
- </span>
38
- </p>
39
- <p class="card-model__parameters">
40
- <span
41
- v-for="(parameter, index) in card.parameters"
42
- :key="`parameter-${index}`"
43
- class="card-model__parameter"
44
- >
45
- <span
46
- v-if="parameter.title"
47
- class="card-model__parameter-title"
48
- >{{ getValue(parameter) }}</span>
49
- </span>
50
- </p>
51
- <p class="card-model__parameters-footer">
52
- <span
53
- v-if="card.videosCount"
54
- class="card-model__count"
20
+ </div>
21
+ <div class="card-model__wrapper">
22
+ <p class="card-model__info">
23
+ <span class="card-model__info-name">
24
+ {{ card.title }}
25
+ </span>
26
+ <span
27
+ v-if="card.country?.name"
28
+ :title="`${t('birthplace_country')}: ${card.country.title}`"
29
+ class="card-model__country-wrapper"
30
+ >
31
+ <img
32
+ :alt="card.country.title"
33
+ :src="`/icons/flags/${card.country.name}.svg`"
34
+ class="card-model__info-country"
35
+ width="16"
36
+ height="12"
55
37
  >
56
- <UiIcon
57
- name="video-camera"
58
- size="14"
59
- />
60
- {{ $n(card.videosCount, 'compact') }}
61
- </span>
38
+ </span>
39
+ </p>
40
+ <p class="card-model__parameters">
41
+ <span
42
+ v-for="(parameter, index) in visibleParameters"
43
+ :key="`parameter-${index}`"
44
+ class="card-model__parameter"
45
+ >
62
46
  <span
63
- v-if="card.lastVideoAddedAt"
64
- class="card-model__update"
65
- >
66
- <UiIcon
67
- name="time-updated"
68
- size="14"
69
- />
70
- {{ formatDate(card.lastVideoAddedAt) }}
71
- </span>
72
- </p>
73
- </div>
47
+ v-if="parameter.title"
48
+ class="card-model__parameter-title"
49
+ >{{ getValue(parameter) }}</span>
50
+ </span>
51
+ <span
52
+ v-if="card.videosCount"
53
+ class="card-model__count"
54
+ >
55
+ <UiIcon
56
+ name="video-camera"
57
+ size="14"
58
+ />
59
+ {{ $n(card.videosCount, 'compact') }}
60
+ </span>
61
+ </p>
62
+ <p
63
+ v-if="card.lastVideoAddedAt"
64
+ class="card-model__update"
65
+ >
66
+ <UiIcon
67
+ name="time-updated"
68
+ size="12"
69
+ />
70
+ <span class="card-model__update-text">
71
+ {{ t('library.updated_at', { date: formatDate(card.lastVideoAddedAt) }) }}
72
+ </span>
73
+ </p>
74
74
  </div>
75
75
  </NuxtLink>
76
76
  </template>
77
77
 
78
78
  <script setup lang="ts">
79
79
  import { convertString, formatDate } from '../../runtime';
80
+ import { CSS_BREAKPOINTS } from '../../config/css-breakpoints';
80
81
  import type { IModelCard } from '../../types';
81
82
 
82
83
  const props = withDefaults(defineProps<{
@@ -102,6 +103,16 @@ function onImageError() {
102
103
  imageError.value = true;
103
104
  }
104
105
 
106
+ /** В строку помещается только первый параметр из списка (тип парня) и возраст — остальные скрыты. */
107
+ const visibleParameters = computed(() => {
108
+ const isAge = (item: IModelCard['parameters'][number]) => item.title === 'age';
109
+
110
+ return [
111
+ props.card.parameters.find(item => !isAge(item)),
112
+ props.card.parameters.find(isAge),
113
+ ].filter(item => item !== undefined);
114
+ });
115
+
105
116
  function getValue(item: { title?: string; value?: string }) {
106
117
  if (item.title === 'age') {
107
118
  return `${item.value} ${t('yo')}`;
@@ -112,11 +123,12 @@ function getValue(item: { title?: string; value?: string }) {
112
123
 
113
124
  <style lang="scss">
114
125
  .card-model {
115
- container-type: inline-size;
126
+ display: flex;
127
+ flex-direction: column;
116
128
  background-color: $model-card-bg;
117
- border-radius: $radius-xl;
129
+ border-radius: $radius-md;
118
130
  border: 1px solid $model-card-border;
119
- display: block;
131
+ overflow: hidden;
120
132
 
121
133
  @include hover(border-color, background-color, box-shadow) {
122
134
  border-color: $model-card-border--hover;
@@ -125,52 +137,48 @@ function getValue(item: { title?: string; value?: string }) {
125
137
  }
126
138
  }
127
139
 
128
- .card-model__inner {
129
- --img-size: 80px;
140
+ .card-model__thumb {
141
+ aspect-ratio: 3/4;
142
+ background-color: $model-card-thumb-bg;
143
+ overflow: hidden;
144
+ }
130
145
 
131
- display: grid;
132
- grid-template-columns: var(--img-size) 1fr;
133
- padding: 8px;
134
- column-gap: 8px;
146
+ .card-model__img {
147
+ width: 100%;
148
+ height: 100%;
149
+ object-fit: cover;
150
+ object-position: center top;
135
151
 
136
152
  @include from-br(sm) {
137
- padding: 12px;
138
- column-gap: 12px;
153
+ transition: var(--transition) transform;
139
154
  }
140
155
  }
141
156
 
142
- .card-model__parameters {
143
- @include font-sm;
144
-
145
- color: $model-card-params;
146
- display: flex;
147
- flex-wrap: nowrap;
148
- align-items: center;
157
+ @media (hover: hover) {
158
+ .card-model:is(:hover, :focus-visible) .card-model__img {
159
+ transform: scale(1.05);
160
+ }
149
161
  }
150
162
 
151
- .card-model__parameters-footer {
152
- @include font-sm;
153
-
154
- display: flex;
155
- flex-wrap: nowrap;
156
- align-items: center;
157
- color: $model-card-params-footer;
158
- margin-top: 2px;
159
- column-gap: 12px;
163
+ .card-model__wrapper {
164
+ padding: 8px;
160
165
  }
161
166
 
162
- .card-model__update {
167
+ .card-model__info {
163
168
  display: grid;
164
- grid-template-columns: 12px 1fr;
165
- column-gap: 4px;
169
+ grid-template-columns: minmax(0, auto) auto;
170
+ column-gap: 6px;
171
+ justify-content: start;
166
172
  align-items: center;
173
+ margin-bottom: 2px;
167
174
  }
168
175
 
169
176
  .card-model__info-name {
177
+ @include font-sm;
178
+
170
179
  font-weight: 500;
171
180
  color: $model-card-name;
172
181
  white-space: nowrap;
173
- max-width: 100%;
174
182
  text-overflow: ellipsis;
175
183
  overflow: hidden;
176
184
  text-transform: capitalize;
@@ -186,50 +194,64 @@ function getValue(item: { title?: string; value?: string }) {
186
194
  }
187
195
  }
188
196
 
189
- .card-model__info {
190
- display: grid;
191
- column-gap: 8px;
192
- margin-bottom: 4px;
193
- grid-template-columns: auto auto auto;
194
- justify-content: start;
195
- align-items: center;
197
+ .card-model__country-wrapper {
198
+ display: flex;
196
199
  }
197
200
 
198
- .card-model__count {
201
+ .card-model__info-country {
202
+ border-radius: 2px;
203
+ }
204
+
205
+ .card-model__parameters {
199
206
  @include font-sm;
200
207
 
201
208
  display: flex;
209
+ flex-wrap: nowrap;
202
210
  align-items: center;
203
- column-gap: 4px;
204
- color: $model-card-count;
205
- }
206
-
207
- .card-model__img {
208
- width: var(--img-size);
209
- height: var(--img-size);
210
- border-radius: $radius-xl;
211
- object-fit: cover;
212
- object-position: center 70%;
211
+ overflow: hidden;
212
+ color: $model-card-params;
213
+ margin-bottom: 2px;
213
214
  }
214
215
 
215
216
  .card-model__parameter:not(:last-child) {
216
- position: relative;
217
217
  display: grid;
218
- grid-template-columns: 1fr auto;
218
+ grid-template-columns: minmax(0, 1fr) auto;
219
219
  column-gap: 6px;
220
220
  padding-right: 6px;
221
221
 
222
222
  &::after {
223
223
  content: '·';
224
+ color: $model-card-separator;
224
225
  }
225
226
  }
226
227
 
227
- .card-model__parameter {
228
- text-align: center;
229
- color: inherit;
228
+ .card-model__parameter-title {
229
+ white-space: nowrap;
230
+ text-overflow: ellipsis;
231
+ overflow: hidden;
230
232
  }
231
233
 
232
- .card-model__country-wrapper {
233
- padding-block: 4px;
234
+ .card-model__count {
235
+ display: flex;
236
+ align-items: center;
237
+ column-gap: 4px;
238
+ flex-shrink: 0;
239
+ color: $model-card-count;
240
+ }
241
+
242
+ .card-model__update {
243
+ @include font-sm;
244
+
245
+ display: grid;
246
+ grid-template-columns: 12px minmax(0, 1fr);
247
+ column-gap: 4px;
248
+ align-items: center;
249
+ color: $model-card-params-footer;
250
+ }
251
+
252
+ .card-model__update-text {
253
+ white-space: nowrap;
254
+ text-overflow: ellipsis;
255
+ overflow: hidden;
234
256
  }
235
257
  </style>
@@ -26,15 +26,27 @@ defineProps<{
26
26
  </script>
27
27
 
28
28
  <style lang="scss">
29
+ /* Лестница колонок как в макете: 2 → 3 (640px) → 4 (768px) → 5 (1024px) → 6 (1280px).
30
+ Пороги — литералы макета, с тирами $brkpnts намеренно не совпадают. */
29
31
  .grid-models {
30
- --min-width: 280px;
31
-
32
32
  display: grid;
33
- grid-template-columns: repeat(auto-fill, minmax(var(--min-width) ,1fr));
33
+ grid-template-columns: repeat(2, minmax(0, 1fr));
34
34
  gap: 12px;
35
35
 
36
- @include from-br(sm) {
37
- --min-width: 320px;
36
+ @include from-br(640px) {
37
+ grid-template-columns: repeat(3, minmax(0, 1fr));
38
+ }
39
+
40
+ @include from-br(768px) {
41
+ grid-template-columns: repeat(4, minmax(0, 1fr));
42
+ }
43
+
44
+ @include from-br(1024px) {
45
+ grid-template-columns: repeat(5, minmax(0, 1fr));
46
+ }
47
+
48
+ @include from-br(1280px) {
49
+ grid-template-columns: repeat(6, minmax(0, 1fr));
38
50
  }
39
51
  }
40
52
  </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.958",
4
+ "version": "0.0.959",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -46,11 +46,14 @@ const testModelImages = [
46
46
  '/img/models/Sebastian _Kane.png',
47
47
  ];
48
48
 
49
- function applyTestImages<T extends { primaryImageUrl?: string; thumbUrl?: string }>(items: T[] | undefined): T[] | undefined {
49
+ /** Портреты 3:4 — только под карточку модели, круглым аватарам топа они не подходят. */
50
+ const testModelPortraits = Array.from({ length: 16 }, (_, index) => `/img/models/portrait-${index + 1}.png`);
51
+
52
+ function applyTestImages<T extends { primaryImageUrl?: string; thumbUrl?: string }>(items: T[] | undefined, images: string[]): T[] | undefined {
50
53
  return items?.map((item, i) => ({
51
54
  ...item,
52
- primaryImageUrl: testModelImages[i % testModelImages.length],
53
- thumbUrl: testModelImages[i % testModelImages.length],
55
+ primaryImageUrl: images[i % images.length],
56
+ thumbUrl: images[i % images.length],
54
57
  }));
55
58
  }
56
59
 
@@ -63,10 +66,10 @@ const [
63
66
  ]);
64
67
 
65
68
  if (topModels.value?.items) {
66
- topModels.value.items = applyTestImages(topModels.value.items)!;
69
+ topModels.value.items = applyTestImages(topModels.value.items, testModelImages)!;
67
70
  }
68
71
  if (models.value?.items) {
69
- models.value.items = applyTestImages(models.value.items)!;
72
+ models.value.items = applyTestImages(models.value.items, testModelPortraits)!;
70
73
  }
71
74
 
72
75
  const { meta, h1 } = useMeta({