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.
@@ -1,7 +1,10 @@
1
1
  <!-- Инфо-блок модели: портрет 3:4, псевдонимы, соцсети, шапка с действиями, рождение, описание, сводка-чипсы; «About» и группы параметров — по «Show more», на десктопе свёрнутые доливают колонку до низа портрета -->
2
2
  <template>
3
3
  <section class="info-model">
4
- <div class="info-model__aside">
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
- /** Если всё целиком выпирает ниже портрета не больше чем на столько — показываем всё без «Show more». */
255
- const FIT_TOLERANCE = 64;
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
- /** Строка грида под кнопкой (с её отступами): без «Show more» она достаётся деталям. */
263
- function getMoreRowHeight(box: HTMLElement) {
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
- return more.getBoundingClientRect().bottom + Number.parseFloat(getComputedStyle(more).marginBottom)
268
- - box.getBoundingClientRect().bottom;
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
- * Небольшой хвост в пределах допуска не прячем, а вытягиваем блок под него через `min-height`.
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
- box.style.minHeight = '';
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 limit = box.clientHeight + 0.5;
297
- const bottoms = units.map(el => el.getBoundingClientRect().bottom - top);
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
- const fullLimit = limit + getMoreRowHeight(box);
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 1fr 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
- // код, которого нет в locales приложения, добавит приложению лишнюю локаль.
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/**']
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.1049",
4
+ "version": "0.0.1051",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {