itube-specs 0.0.865 → 0.0.868

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/README.md CHANGED
@@ -63,13 +63,15 @@ npm run lint:css # Stylelint по **/*.{scss,vue}
63
63
  npm run lint:css:fix # Stylelint --fix
64
64
  npm run check:themes # сверяет наборы имён токенов во всех темах
65
65
  npm run check:styles # компилирует стили каждого SFC каждой темой (~1.5s)
66
+ npm run check:breakpoints # сверяет JS-зеркало брейкпоинтов с $brkpnts в SCSS
66
67
  npm run test # vitest однократно
67
68
  npm run patch # npm version patch — бамп версии перед публикацией
68
69
  ```
69
70
 
70
71
  Pre-commit (husky + lint-staged): на закоммиченных файлах прогоняются `eslint --fix` и `stylelint --fix`.
71
- Если в коммите есть `assets/scss/themes/**`, гоняется `check:themes`; если любой `.vue` или `assets/scss/**` —
72
- `check:styles`. Оба есть и в CI, в шаге Lint.
72
+ Если в коммите есть `assets/scss/themes/**`, гоняется `check:themes`; если любой `.vue` или
73
+ `assets/scss/**` — `check:styles`; если пара файлов брейкпоинтов — `check:breakpoints`. Все три есть и в
74
+ CI, в шаге Lint.
73
75
  ⚠️ **Шаблоны `.vue` не типизируются вообще.** Обычный `tsc` их не читает, а `vue-tsc` не подключён (накопился
74
76
  бэклог ошибок в шаблонах, подключение — отдельная задача). Значит чистый `tsc` ничего не говорит про выражения
75
77
  в шаблоне: арифметика над строковым union, неверный тип пропа, переименованное поле пройдут молча.
@@ -87,7 +89,8 @@ runtime/ # runtime-значения и хелперы (barrel index.ts)
87
89
  constants/ # доменные `as const` (Niche, PlaylistType, AdSpotType, …)
88
90
  utils/ # чистые хелперы, cleaners/, converters/
89
91
  types/ # .d.ts типы (barrel index.d.ts — с расширением .d.ts в реэкспортах)
90
- lib/ # статические scheme/config-данные (barrel index.ts): scheme/*, css-breakpoints
92
+ lib/ # продуктовые статические данные (barrel index.ts): scheme/*, *-items, *-scheme
93
+ config/ # build-time конфиг для приложения (barrel index.ts): themes, css-breakpoints
91
94
  middleware/ # глобальные: auth.global, normalize.global, age.global
92
95
  plugins/ # SSR/клиент: country/countries/mobile (SSR), services.client, sentry.client, adv
93
96
  server/ # BFF: api/** (тонкие прокси к внешнему API), utils/metrics, plugins/*, tasks
@@ -173,7 +176,7 @@ ad-конфиг), теперь читается из `runtimeConfig.public` (з
173
176
  ## Что живёт в слое, а что в приложении
174
177
 
175
178
  - **Слой:** компоненты, composables, API-сервисы, доменные утилиты, runtime-константы/хелперы, типы,
176
- `lib/` (scheme, css-breakpoints), middleware (`auth`/`normalize`/`age`), SSR-плагины
179
+ `lib/` (продуктовые данные), `config/` (темы, брейкпоинты), middleware (`auth`/`normalize`/`age`), SSR-плагины
177
180
  (`country`/`mobile`/`services`/`sentry`/`adv`), BFF-хендлеры `server/api/**` — всё site-agnostic.
178
181
  - **Приложение:** только per-site — **значения** config (флаги/реклама/локали) через `runtimeConfig.public`
179
182
  (слой держит пустые контейнеры-заглушки), i18n-локали, `public/` и **выбор темы** (`theme:` в
@@ -338,6 +338,18 @@ $language-value-text: $color-white;
338
338
  $language-description: $color-zinc-500;
339
339
  $language-empty-text: $color-zinc-300;
340
340
 
341
+ // ─── Niche ───
342
+ $niche-trigger-text: $color-zinc-400;
343
+ $niche-angle--hover: $color-white;
344
+ $niche-title: $color-zinc-500;
345
+ $niche-value-text: $color-white;
346
+ $niche-menu-bg: $color-night;
347
+ $niche-menu-border: $color-zinc-800;
348
+ $niche-item-text: $color-zinc-300;
349
+ $niche-item-bg--hover: $color-zinc-800;
350
+ $niche-item-text--current: $color-white;
351
+ $niche-item-bg--current: $color-zinc-700;
352
+
341
353
  // ─── Footer language ───
342
354
  $footer-language-bg: $color-zinc-800-50;
343
355
  $footer-language-bg--hover: $color-zinc-700;
@@ -395,6 +395,18 @@ $language-value-text: $color-white;
395
395
  $language-description: $color-gray-500;
396
396
  $language-empty-text: $color-gray-400;
397
397
 
398
+ // ─── Niche ───
399
+ $niche-trigger-text: $color-gray-500;
400
+ $niche-angle--hover: $color-gray-300;
401
+ $niche-title: $color-gray-400;
402
+ $niche-value-text: $color-white;
403
+ $niche-menu-bg: $color-slate-850;
404
+ $niche-menu-border: $color-white-10;
405
+ $niche-item-text: $color-gray-300;
406
+ $niche-item-bg--hover: $color-slate-750;
407
+ $niche-item-text--current: $color-white;
408
+ $niche-item-bg--current: $color-indigo-soft;
409
+
398
410
  // ─── Footer language ───
399
411
  $footer-language-bg: $color-slate-750;
400
412
  $footer-language-bg--hover: $color-slate-700;
@@ -68,6 +68,11 @@
68
68
  </div>
69
69
  </nav>
70
70
 
71
+ <LayoutNiche
72
+ class="layout-burger__niche"
73
+ icon-size="20"
74
+ />
75
+
71
76
  <LayoutLanguage
72
77
  class="layout-burger__language"
73
78
  icon-size="20"
@@ -257,6 +262,10 @@ router.afterEach(() => {
257
262
  padding-bottom: 12px;
258
263
  }
259
264
 
265
+ .layout-burger__niche {
266
+ margin-bottom: 12px;
267
+ }
268
+
260
269
  .layout-burger__close {
261
270
  position: fixed;
262
271
  top: 16px;
@@ -10,6 +10,11 @@
10
10
  glow
11
11
  class="layout-header__logo"
12
12
  />
13
+ <LayoutNiche
14
+ class="layout-header__niche _from-sm"
15
+ icon-size="20"
16
+ />
17
+
13
18
  <div
14
19
  ref="languageRef"
15
20
  class="layout-header__language _from-sm"
@@ -135,7 +140,7 @@ $breakpoint: sm;
135
140
  width: 100%;
136
141
 
137
142
  @include from-br(sm) {
138
- grid-template-columns: auto 1fr auto auto;
143
+ grid-template-columns: auto 1fr auto auto auto;
139
144
  }
140
145
  }
141
146
 
@@ -180,7 +185,14 @@ $breakpoint: sm;
180
185
  .layout-header__user,
181
186
  .layout-header__sign-up {
182
187
  @include from-br(sm) {
183
- grid-column: 4;
188
+ grid-column: 5;
189
+ grid-row: 1;
190
+ }
191
+ }
192
+
193
+ .layout-header__niche {
194
+ @include from-br(sm) {
195
+ grid-column: 3;
184
196
  grid-row: 1;
185
197
  }
186
198
  }
@@ -194,7 +206,7 @@ $breakpoint: sm;
194
206
  .layout-header__language {
195
207
  @include from-br(sm) {
196
208
  position: relative;
197
- grid-column: 3;
209
+ grid-column: 4;
198
210
  grid-row: 1;
199
211
  }
200
212
  }
@@ -0,0 +1,179 @@
1
+ <template>
2
+ <div
3
+ v-if="niches.length > 1"
4
+ ref="nicheRef"
5
+ class="layout-niche"
6
+ @keydown.esc="isOpen = false"
7
+ >
8
+ <button
9
+ :class="{'--open': isOpen}"
10
+ :aria-expanded="isOpen"
11
+ :aria-label="$t('niche')"
12
+ class="layout-niche__trigger"
13
+ type="button"
14
+ @click="isOpen = !isOpen"
15
+ >
16
+ <UiIcon
17
+ :name="currentNiche.icon"
18
+ :size="iconSize"
19
+ class="layout-niche__icon"
20
+ />
21
+ <span class="layout-niche__current _to-sm">
22
+ <span>{{ $t('niche') }}:&nbsp;</span>
23
+ <span class="layout-niche__text">{{ $t(currentNiche.label) }}</span>
24
+ </span>
25
+ <UiIcon
26
+ class="layout-niche__angle"
27
+ name="angle-down"
28
+ size="12"
29
+ />
30
+ </button>
31
+ <ul
32
+ v-if="isOpen"
33
+ class="layout-niche__list"
34
+ >
35
+ <li
36
+ v-for="item in niches"
37
+ :key="`niche-${item.name}`"
38
+ >
39
+ <button
40
+ :class="{'--current': item.name === currentNiche.name}"
41
+ :aria-current="item.name === currentNiche.name ? 'true' : undefined"
42
+ class="layout-niche__item"
43
+ type="button"
44
+ @click="select(item.name)"
45
+ >
46
+ <UiIcon
47
+ :name="item.icon"
48
+ class="layout-niche__item-icon"
49
+ size="20"
50
+ />
51
+ {{ $t(item.label) }}
52
+ </button>
53
+ </li>
54
+ </ul>
55
+ </div>
56
+ </template>
57
+
58
+ <script setup lang="ts">
59
+ import { onClickOutside } from '@vueuse/core';
60
+ import type { Niche } from '../../runtime';
61
+
62
+ withDefaults(defineProps<{
63
+ iconSize?: string
64
+ }>(), {
65
+ iconSize: '20'
66
+ }
67
+ );
68
+
69
+ const { niches, currentNiche, changeNiche } = useNiches();
70
+
71
+ const nicheRef = ref<HTMLElement>();
72
+ const isOpen = ref(false);
73
+
74
+ onClickOutside(nicheRef, () => {
75
+ isOpen.value = false;
76
+ });
77
+
78
+ function select(name: Niche) {
79
+ isOpen.value = false;
80
+ changeNiche(name);
81
+ }
82
+ </script>
83
+
84
+ <style lang="scss">
85
+ .layout-niche {
86
+ display: grid;
87
+
88
+ @include from-br(sm) {
89
+ position: relative;
90
+ }
91
+ }
92
+
93
+ .layout-niche__trigger {
94
+ display: grid;
95
+ grid-auto-flow: column;
96
+ align-items: center;
97
+ justify-content: start;
98
+ column-gap: 6px;
99
+ padding: 6px 8px;
100
+ color: $niche-trigger-text;
101
+
102
+ @include to-br(sm) {
103
+ grid-template-columns: auto 1fr auto;
104
+ width: 100%;
105
+ padding: 10px 16px;
106
+ column-gap: 16px;
107
+ }
108
+ }
109
+
110
+ .layout-niche__current {
111
+ @include font-sm;
112
+
113
+ display: grid;
114
+ grid-template-columns: auto 1fr;
115
+ color: $niche-title;
116
+ }
117
+
118
+ .layout-niche__text {
119
+ color: $niche-value-text;
120
+ font-weight: 500;
121
+ }
122
+
123
+ .layout-niche__angle {
124
+ pointer-events: none;
125
+ transition: transform var(--transition), color var(--transition);
126
+ }
127
+
128
+ .layout-niche__trigger:hover .layout-niche__angle {
129
+ color: $niche-angle--hover;
130
+ }
131
+
132
+ .layout-niche__trigger.--open .layout-niche__angle {
133
+ transform: rotate(180deg);
134
+ }
135
+
136
+ .layout-niche__list {
137
+ display: grid;
138
+
139
+ @include from-br(sm) {
140
+ position: absolute;
141
+ right: 0;
142
+ bottom: -8px;
143
+ z-index: $z-index-plus;
144
+ min-width: 180px;
145
+ padding: 8px;
146
+ background-color: $niche-menu-bg;
147
+ border: 1px solid $niche-menu-border;
148
+ border-radius: $radius-xl;
149
+ transform: translateY(100%);
150
+ }
151
+ }
152
+
153
+ .layout-niche__item {
154
+ @include font-sm;
155
+
156
+ display: grid;
157
+ grid-template-columns: 20px 1fr;
158
+ align-items: center;
159
+ column-gap: 12px;
160
+ width: 100%;
161
+ padding: 10px 16px;
162
+ color: $niche-item-text;
163
+ text-align: start;
164
+ border-radius: $radius-lg;
165
+
166
+ @include hover(background-color) {
167
+ background-color: $niche-item-bg--hover;
168
+ }
169
+
170
+ @include from-br(sm) {
171
+ padding: 8px 10px;
172
+ }
173
+ }
174
+
175
+ .layout-niche__item.--current {
176
+ color: $niche-item-text--current;
177
+ background-color: $niche-item-bg--current;
178
+ }
179
+ </style>
@@ -0,0 +1,47 @@
1
+ import type { Niche } from '../runtime';
2
+ import { NicheHelper } from '../utils/niche-helper';
3
+
4
+ /** Иконка из assets/icons/common под каждую нишу. */
5
+ const NICHE_ICONS: Record<Niche, string> = {
6
+ straight: 'hetero',
7
+ gay: 'gay',
8
+ shemale: 'shemale',
9
+ };
10
+
11
+ export interface INicheItem {
12
+ name: Niche,
13
+ icon: string,
14
+ /** Ключ i18n с названием ниши */
15
+ label: string,
16
+ }
17
+
18
+ /**
19
+ * Ниши сайта для переключателя: список из per-site конфига проекта (appConfig.niches),
20
+ * текущая — из useState('niche') с фолбэком на дефолтную нишу окружения.
21
+ */
22
+ export const useNiches = () => {
23
+ const defaultNiche = NicheHelper.getSiteDefaultNiche();
24
+ const current = useState<Niche>('niche', () => defaultNiche);
25
+ const localePath = useLocalePath();
26
+
27
+ const niches = computed<INicheItem[]>(() =>
28
+ ((useAppConfig().niches as Niche[]) || []).map(name => ({
29
+ name,
30
+ icon: NICHE_ICONS[name] || 'earth',
31
+ label: `niches.${name}`,
32
+ })));
33
+
34
+ const currentNiche = computed(() => niches.value.find(item => item.name === current.value) || niches.value[0]);
35
+
36
+ /**
37
+ * Ниша влияет и на роутинг (префикс пути), и на заголовок X-Niche в SSR-запросах,
38
+ * поэтому переключение — полной перезагрузкой на корень выбранной ниши.
39
+ */
40
+ function changeNiche(name: Niche) {
41
+ if (name === current.value) return;
42
+ current.value = name;
43
+ window.location.href = localePath(name === defaultNiche ? '/' : `/${name}`);
44
+ }
45
+
46
+ return { niches, currentNiche, changeNiche };
47
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.865",
4
+ "version": "0.0.868",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {