itube-specs 0.0.1049 → 0.0.1051
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/components/info/model.vue +55 -21
- package/nuxt.config.ts +22 -2
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<!-- Инфо-блок модели: портрет 3:4, псевдонимы, соцсети, шапка с действиями, рождение, описание, сводка-чипсы; «About» и группы параметров — по «Show more», на десктопе свёрнутые доливают колонку до низа портрета -->
|
|
2
2
|
<template>
|
|
3
3
|
<section class="info-model">
|
|
4
|
-
<div
|
|
4
|
+
<div
|
|
5
|
+
ref="asideRef"
|
|
6
|
+
class="info-model__aside"
|
|
7
|
+
>
|
|
5
8
|
<UiImg
|
|
6
9
|
:src="currentImage"
|
|
7
10
|
:avif-src="currentAvifSrc"
|
|
@@ -251,21 +254,31 @@ const hasMore = computed(() => groupedParameters.value.length > 0 || Boolean(pro
|
|
|
251
254
|
/** Единицы, которые прячутся только целиком: заголовки, абзацы, плашки. Порядок — документный. */
|
|
252
255
|
const FIT_SELECTOR = 'h3, .info-model__about-text, .info-grid__plain-text, .info-grid__item';
|
|
253
256
|
|
|
254
|
-
/**
|
|
255
|
-
const
|
|
257
|
+
/** Меньше строк «About» не показываем: одна строка с многоточием читается как поломка. */
|
|
258
|
+
const ABOUT_MIN_LINES = 2;
|
|
256
259
|
|
|
260
|
+
const asideRef = ref<HTMLElement>();
|
|
257
261
|
const detailsRef = ref<HTMLElement>();
|
|
258
262
|
const moreRef = ref<HTMLElement>();
|
|
259
263
|
const hasCut = ref(true);
|
|
260
264
|
const isMeasured = ref(false);
|
|
261
265
|
|
|
262
|
-
/** Строка грида под кнопкой (с её отступами)
|
|
263
|
-
|
|
266
|
+
/** Строка грида под кнопкой (с её отступами). Запоминается, пока кнопка видна, — спрятанную не измерить. */
|
|
267
|
+
let moreRowHeight = 0;
|
|
268
|
+
|
|
269
|
+
function getMoreRowHeight() {
|
|
264
270
|
const more = moreRef.value;
|
|
265
|
-
if (!more || !more.offsetParent) return 0;
|
|
266
271
|
|
|
267
|
-
|
|
268
|
-
|
|
272
|
+
if (more?.offsetParent) {
|
|
273
|
+
const { marginTop, marginBottom } = getComputedStyle(more);
|
|
274
|
+
moreRowHeight = more.offsetHeight + Number.parseFloat(marginTop) + Number.parseFloat(marginBottom);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return moreRowHeight;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function getBottoms(units: HTMLElement[], top: number) {
|
|
281
|
+
return units.map(el => el.getBoundingClientRect().bottom - top);
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
/** До первого замера детали и кнопка на десктопе прозрачны (`--pending`): иначе после гидрации видно, как они перестраиваются. */
|
|
@@ -275,37 +288,54 @@ function measureDetails() {
|
|
|
275
288
|
}
|
|
276
289
|
|
|
277
290
|
/**
|
|
278
|
-
* На десктопе
|
|
279
|
-
*
|
|
280
|
-
*
|
|
291
|
+
* На десктопе свёрнутые детали доливают колонку до низа портрета. Место — от верха блока до низа
|
|
292
|
+
* `aside` за вычетом строки кнопки; «About» обрезается по числу влезающих строк, из остального
|
|
293
|
+
* прячем всё, что не влезло целиком, и заголовок без единой видимой строки под ним.
|
|
294
|
+
* Высота блока = видимый контент, остаток уходит в пустую строку грида под кнопкой — она идёт сразу
|
|
295
|
+
* за контентом. Ниже портрета блок не вытягивается никогда: секция не растёт после гидрации.
|
|
281
296
|
*/
|
|
282
297
|
function fitDetails() {
|
|
283
298
|
const box = detailsRef.value;
|
|
284
299
|
if (!box) return;
|
|
285
300
|
|
|
286
301
|
const units = Array.from(box.querySelectorAll<HTMLElement>(FIT_SELECTOR));
|
|
287
|
-
|
|
302
|
+
const about = box.querySelector<HTMLElement>('.info-model__about-text');
|
|
303
|
+
box.style.height = '';
|
|
304
|
+
about?.style.removeProperty('--line-clamp');
|
|
288
305
|
|
|
289
|
-
if (isExpanded.value || !isChipsOverflowWidth()) {
|
|
306
|
+
if (isExpanded.value || !isChipsOverflowWidth() || !asideRef.value) {
|
|
290
307
|
units.forEach(el => el.removeAttribute('data-cut'));
|
|
291
308
|
hasCut.value = true;
|
|
292
309
|
return;
|
|
293
310
|
}
|
|
294
311
|
|
|
312
|
+
about?.style.setProperty('--line-clamp', '999');
|
|
313
|
+
|
|
295
314
|
const { top } = box.getBoundingClientRect();
|
|
296
|
-
const
|
|
297
|
-
const
|
|
315
|
+
const space = asideRef.value.getBoundingClientRect().bottom - top;
|
|
316
|
+
const moreRow = getMoreRowHeight();
|
|
317
|
+
let bottoms = getBottoms(units, top);
|
|
298
318
|
const contentHeight = Math.max(0, ...bottoms);
|
|
299
319
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (contentHeight <= fullLimit + FIT_TOLERANCE) {
|
|
303
|
-
if (contentHeight > fullLimit) box.style.minHeight = `${Math.ceil(contentHeight)}px`;
|
|
320
|
+
if (contentHeight <= space + 0.5) {
|
|
321
|
+
box.style.height = `${Math.ceil(contentHeight)}px`;
|
|
304
322
|
units.forEach(el => el.removeAttribute('data-cut'));
|
|
305
323
|
hasCut.value = false;
|
|
306
324
|
return;
|
|
307
325
|
}
|
|
308
326
|
|
|
327
|
+
const limit = space - moreRow + 0.5;
|
|
328
|
+
|
|
329
|
+
if (about) {
|
|
330
|
+
const lineHeight = Number.parseFloat(getComputedStyle(about).lineHeight);
|
|
331
|
+
const rect = about.getBoundingClientRect();
|
|
332
|
+
const fullLines = Math.round(rect.height / lineHeight);
|
|
333
|
+
const lines = Math.min(fullLines, Math.floor((limit - (rect.top - top)) / lineHeight));
|
|
334
|
+
|
|
335
|
+
about.style.setProperty('--line-clamp', String(Math.max(lines, ABOUT_MIN_LINES)));
|
|
336
|
+
bottoms = getBottoms(units, top);
|
|
337
|
+
}
|
|
338
|
+
|
|
309
339
|
const visible: boolean[] = [];
|
|
310
340
|
let isOver = false;
|
|
311
341
|
|
|
@@ -322,6 +352,9 @@ function fitDetails() {
|
|
|
322
352
|
|
|
323
353
|
units.forEach((el, index) => el.toggleAttribute('data-cut', !visible[index]));
|
|
324
354
|
hasCut.value = visible.includes(false);
|
|
355
|
+
|
|
356
|
+
const visibleHeight = Math.max(0, ...bottoms.filter((_, index) => visible[index]));
|
|
357
|
+
box.style.height = `${Math.ceil(visibleHeight)}px`;
|
|
325
358
|
}
|
|
326
359
|
|
|
327
360
|
useChipsOverflow(detailsRef, measureDetails);
|
|
@@ -391,14 +424,15 @@ const shareModelCard = computed<IModelCard>(() => ({
|
|
|
391
424
|
|
|
392
425
|
@include from-br(sm) {
|
|
393
426
|
grid-template-columns: 240px minmax(0, 1fr);
|
|
394
|
-
grid-template-rows: auto auto auto auto
|
|
427
|
+
grid-template-rows: auto auto auto auto auto auto 1fr;
|
|
395
428
|
grid-template-areas:
|
|
396
429
|
'aside header'
|
|
397
430
|
'aside born'
|
|
398
431
|
'aside description'
|
|
399
432
|
'aside summary'
|
|
400
433
|
'aside details'
|
|
401
|
-
'aside more'
|
|
434
|
+
'aside more'
|
|
435
|
+
'aside .';
|
|
402
436
|
}
|
|
403
437
|
}
|
|
404
438
|
|
package/nuxt.config.ts
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
import { readdirSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { defineNuxtConfig } from 'nuxt/config';
|
|
4
|
+
import { useNuxt } from 'nuxt/kit';
|
|
4
5
|
|
|
5
6
|
// Общие для всех сайтов переводы (навигация, сортировки, UI-действия). `@nuxtjs/i18n`
|
|
6
7
|
// мержит локали слоёв: файл слоя встаёт в `files` ПЕРЕД проектным, поэтому проект
|
|
7
8
|
// перекрывает любой ключ, а `deepCopy` мержит вложенные группы, не заменяя их целиком.
|
|
8
|
-
// Список кодов выводится из самих файлов, поэтому набор здесь —
|
|
9
|
-
//
|
|
9
|
+
// Список кодов выводится из самих файлов, поэтому набор здесь — надмножество локалей всех
|
|
10
|
+
// сайтов. Код, которого нет в locales приложения, i18n добавил бы приложению лишней локалью
|
|
11
|
+
// (без `language` → «ISO code is required to generate alternate link», лишние /<code>/ роуты),
|
|
12
|
+
// поэтому до установки модулей локали слоя урезаются до объявленных приложением.
|
|
10
13
|
const sharedLocalesDir = fileURLToPath(new URL('./i18n/locales', import.meta.url));
|
|
11
14
|
const sharedLocales = readdirSync(sharedLocalesDir)
|
|
12
15
|
.filter(file => file.endsWith('.json'))
|
|
13
16
|
.sort()
|
|
14
17
|
.map(file => ({ code: file.split('-')[0], file }));
|
|
15
18
|
|
|
19
|
+
const localeCode = (locale: string | { code: string }) => typeof locale === 'string' ? locale : locale.code;
|
|
20
|
+
|
|
16
21
|
export default defineNuxtConfig({
|
|
22
|
+
hooks: {
|
|
23
|
+
'modules:before'() {
|
|
24
|
+
const [project, ...layers] = useNuxt().options._layers;
|
|
25
|
+
const appCodes = new Set((project.config.i18n?.locales ?? []).map(localeCode));
|
|
26
|
+
if (!appCodes.size) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const layer of layers) {
|
|
30
|
+
const i18n = layer.config.i18n;
|
|
31
|
+
if (i18n?.locales) {
|
|
32
|
+
i18n.locales = i18n.locales.filter(locale => appCodes.has(localeCode(locale)));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
},
|
|
17
37
|
modules: ['@nuxt/eslint', '@nuxt/icon', '@nuxtjs/i18n'],
|
|
18
38
|
imports: {
|
|
19
39
|
dirs: ['composables/**']
|