itube-specs 0.0.922 → 0.0.927

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.
Files changed (40) hide show
  1. package/DOCS/CONVENTIONS.md +44 -0
  2. package/DOCS/STYLES.md +136 -0
  3. package/README.md +6 -0
  4. package/assets/scss/base/_utils.scss +6 -0
  5. package/assets/scss/layout/_layout.scss +1 -1
  6. package/assets/scss/mixins/_badge.scss +18 -0
  7. package/assets/scss/mixins/_breakpoints.scss +24 -3
  8. package/assets/scss/mixins/_cards-grid.scss +32 -0
  9. package/assets/scss/mixins/_index.scss +2 -0
  10. package/assets/scss/mixins/_page.scss +5 -1
  11. package/assets/scss/mixins/_video-card.scss +3 -13
  12. package/assets/scss/themes/dark/_theme.scss +16 -6
  13. package/assets/scss/themes/light/_theme.scss +16 -6
  14. package/assets/scss/vars/_tokens.scss +5 -1
  15. package/components/grid/categories.vue +3 -6
  16. package/components/grid/channels.vue +2 -12
  17. package/components/grid/playlists.vue +2 -8
  18. package/components/layout/burger.vue +4 -3
  19. package/components/layout/footer-language.vue +37 -61
  20. package/components/layout/header-user.vue +1 -1
  21. package/components/layout/header.vue +42 -41
  22. package/components/layout/language.vue +89 -58
  23. package/components/layout/nav.vue +1 -1
  24. package/components/layout/niche.vue +5 -5
  25. package/components/layout/notifications.vue +1 -1
  26. package/components/search/main.vue +18 -10
  27. package/components/section/swiper-cards.vue +17 -4
  28. package/components/view/model-videos.vue +2 -1
  29. package/components/view/video.vue +2 -3
  30. package/components/view/videos.vue +1 -0
  31. package/config/css-breakpoints.ts +2 -1
  32. package/layouts/default.vue +1 -1
  33. package/lib/scheme/sort-items-default.ts +2 -0
  34. package/lib/scheme/sort-items-search.ts +2 -0
  35. package/package.json +3 -2
  36. package/runtime/utils/check-device-width.ts +7 -3
  37. package/runtime/utils/converters/__tests__/group-categories-by-first-letter.test.ts +2 -3
  38. package/runtime/utils/converters/group-categories-by-first-letter.ts +3 -3
  39. package/utils/category-letter-groups/__tests__/group-by-first-letter.test.ts +2 -2
  40. package/utils/category-letter-groups/group-by-first-letter.ts +3 -3
@@ -0,0 +1,44 @@
1
+ # Conventions
2
+
3
+ Layer + every site. From an app: `node_modules/itube-specs/DOCS/CONVENTIONS.md`. Styles → `DOCS/STYLES.md`.
4
+ Human-facing rationale → the layer's `README.md` (Russian).
5
+
6
+ ## Components
7
+
8
+ `pathPrefix: true` → name = folder path + filename, repeated segments deduped by Nuxt. Folder is the
9
+ namespace; filename must not repeat it.
10
+
11
+ - `ui/icon.vue` → `<UiIcon>` · `player/core.vue` → `<PlayerCore>` · `info/grid.vue` → `<InfoGrid>` (not
12
+ `<InfoInfoGrid>`)
13
+ - Folder root: `index.vue` when the deduped name is already multi-word (`card/video/index.vue` →
14
+ `<CardVideo>`); `main.vue` only when `index.vue` would give one word (`filter/main.vue` → `<FilterMain>`).
15
+ Never a single-word name — clashes with HTML elements.
16
+ - BEM root class = kebab-case of the name: `<CardVideo>` → `.card-video`
17
+ - `pathPrefix: true` mandatory — layers break component exports under `false` (nuxt/nuxt#30099)
18
+
19
+ **App override:** an app defining the same name outranks the layer and wins auto-import; callers untouched.
20
+ Don't fork a component when a prop expresses the difference.
21
+
22
+ ## Markup
23
+
24
+ `<div>` is the fallback, not the default.
25
+
26
+ | Need | Tag |
27
+ |---|---|
28
+ | text | `<span>` inline, `<p>` paragraph — never bare `<div>` |
29
+ | structure | `header`, `nav`, `main`, `aside`, `footer`, `section`, `article` |
30
+ | sequence of similar items | `<ul>`/`<ol>` + `<li>` — chip rows and tag clouds included |
31
+ | action / navigation / collapsible | `<button>` / `<a>` / `<details>`+`<summary>` |
32
+ | forms | `<label>`, `<fieldset>`, `<legend>`, native `<input>` types |
33
+ | emphasis, figures | `<em>`/`<strong>`, `<figure>`/`<figcaption>` — not style-only spans |
34
+
35
+ ## TypeScript
36
+
37
+ - Semicolons always. No code comments unless asked.
38
+ - Domain string sets: `as const` PascalCase objects in `runtime/constants/`, type via
39
+ `typeof X[keyof typeof X]`. Never `enum` — it survives into the bundle, and `import type` erases it so
40
+ `X.Member` is `undefined` at runtime in a published layer; you also can't assign a raw API/URL/cookie string
41
+ to an enum. String values, not numeric (`0` is falsy — `if (spotType)` misreads member `0` as absent;
42
+ `AdSpotType` is numeric, a wart, don't copy). Framework unions (Nuxt status) stay unions.
43
+ - `.vue` templates are NOT typechecked — `tsc` doesn't read them, `vue-tsc` isn't a gate. A clean `tsc` says
44
+ nothing about template expressions. Read them, or `npx vue-tsc --noEmit` ad hoc past pre-existing noise.
package/DOCS/STYLES.md ADDED
@@ -0,0 +1,136 @@
1
+ # Styles
2
+
3
+ Canonical for every itube site — the layer owns 100% of the SCSS; apps have an empty `assets/scss/` override
4
+ slot. Read before touching any `<style lang="scss">`, `assets/scss/**`, or a theme token. Paths are
5
+ layer-relative; from an app: `node_modules/itube-specs/DOCS/STYLES.md`. Human-facing version → `README.md`.
6
+
7
+ ## Co-location
8
+
9
+ - Component and page styles in the SFC's own `<style lang="scss">`. **No `scoped`** — flat BEM isolates.
10
+ - Nothing per-component or per-page in `assets/scss/`.
11
+ - Tree: `assets/scss/` → `vars/` (palette, vars, tokens, z-index), `themes/`, `mixins/`, `base/`, `layout/`,
12
+ `main.scss`. `css` entry is `main.scss`, loads only `vars/tokens`, `base`, `layout`.
13
+ - **Never `@use 'vars'`/`'mixins'` in an SFC block** — auto-injected by the *app's*
14
+ `vite.css.preprocessorOptions.scss.additionalData` + `loadPaths`.
15
+ - Sass-loaded global partials: `@use` relatively within their own root, bare specifiers across roots
16
+ (layer↔app) so `loadPaths` resolves.
17
+ - The layer has no `sass` package — layer styles compile only inside an app, never in the layer's vitest.
18
+
19
+ ## Themes
20
+
21
+ Dir per theme: `assets/scss/themes/<name>/_theme.scss`. App picks one via `theme:` in its
22
+ `config/site.config.ts` (read by its `nuxt.config.ts` *and* `app.config.ts` — `useAppConfig()` doesn't exist at
23
+ config time), appends `themes/<theme>/` to `loadPaths`; `@forward 'theme'` in `vars/_index.scss` resolves
24
+ there because no `_theme.scss` sits beside it.
25
+
26
+ **Dir-per-theme is load-bearing** — Sass needs a literal `@use`/`@forward` path, so only the folder can vary.
27
+
28
+ `loadPaths = [<app>/assets/scss, <layer>/assets/scss/themes/<theme>, <layer>/assets/scss]`. App first → a site
29
+ overrides any partial by name (its `vars/_theme.scss` wins over loadPaths).
30
+
31
+ **Every token must exist in every theme** — a missing one is a hard Sass error that kills the app build. Two
32
+ guards, both pre-commit + CI:
33
+
34
+ | Script | Proves | Blind spot |
35
+ |---|---|---|
36
+ | `check:themes` | themes declare the same top-level `$name:` set (`$surfaces` / `$button-variant-overrides` contents ignored — a theme may leave either empty) | token declared in **no** theme |
37
+ | `check:styles` | every `<style>` block compiles against every theme, same preamble + loadPaths as Vite; names the failing theme (~1.5s) | — |
38
+
39
+ ## BEM
40
+
41
+ `.block {}`, `.block__element {}`, `.block.--modifier {}` — modifier prefix `--`.
42
+
43
+ **No nesting**, every selector flat with its full class name. `&` only for pseudo-classes, pseudo-elements,
44
+ state modifiers (`&::after`, `&:focus-visible`, `&.--open`).
45
+
46
+ ```scss
47
+ .layout-header {}
48
+ .layout-header__logo {} // not .layout-header { &__logo {} }
49
+ ```
50
+
51
+ ## Hover
52
+
53
+ **Always `@include hover(<properties>)`, never `&:hover`.** Wraps in `@media (hover: hover)` (on touch a hover
54
+ sticks after a tap), declares `--transition` for the listed properties, pairs `:hover` with `:focus-visible`.
55
+
56
+ ```scss
57
+ .ui-btn.--muted {
58
+ @include hover(background-color, border) { background-color: $button-muted-bg--hover; }
59
+ }
60
+ ```
61
+
62
+ Applies to the current selector only, so hovering one element to restyle another is written flat inside your
63
+ own media query:
64
+
65
+ ```scss
66
+ @media (hover: hover) {
67
+ .card-round:is(:hover, :focus-visible) .card-round__img { transform: scale(1.05); }
68
+ }
69
+ ```
70
+
71
+ ## Tokens
72
+
73
+ Never hardcode a color, size or z-index.
74
+
75
+ | File | Holds | Type |
76
+ |---|---|---|
77
+ | `vars/_palette.scss` | `$color-indigo-600` — raw palette | SCSS var |
78
+ | `themes/<theme>/_theme.scss` | `$chips-text`, `$button-bg--hover` — colors, one per theme | SCSS var |
79
+ | `vars/_vars.scss` | `$radius-*`, `$blur-*`, `$spacing` — static non-color | SCSS var |
80
+ | `vars/_tokens.scss` | `--header-height` — changes at a breakpoint or set via JS | CSS custom prop |
81
+
82
+ - Changes at a breakpoint or set via JS → CSS custom property. Everything else → SCSS var.
83
+ - **Palette is private**, consumed only by theme files. A component must never use `$color-*` — add the token
84
+ to **every** theme first.
85
+ - **No semantic tier** (`$text-muted`, `$role-border-subtle`) — themes reference palette directly.
86
+ - Theme files hold **colors only**; radius/blur/static → `vars/_vars.scss`.
87
+
88
+ ### Context-dependent colors
89
+
90
+ Never a theme flag in a component. Two mixins, driven from the theme file:
91
+
92
+ - `surface($name)` — block declares itself an island (`.ui-popup { @include surface(overlay); }`), contents
93
+ pick up colors by cascade. Theme supplies deltas via `$surfaces`; no differentiation → `$surfaces: ()`.
94
+ Components read `var(--input-bg, #{$input-bg})`, so a single color can still be overridden directly.
95
+ - `button-variant($key)` — place asks, theme answers by name via `$button-variant-overrides`. The element's
96
+ class still reflects the `theme=` prop, so the mixin prints `--button-variant` as a DevTools marker.
97
+
98
+ `appConfig.theme` — structural differences only (different element, extra icon, `v-if`).
99
+
100
+ ### Naming
101
+
102
+ `$<block>-<element>?-<modifier>?-<property>-<state>?`
103
+
104
+ - block = component root · element = BEM element, optional · modifier = variant, optional
105
+ - property — always present, always short: `bg`, `text`, `border`, `radius`, `icon`, `shadow`. Never the full
106
+ CSS name (`$button-background` → `$button-bg`, `$input-border-radius` → `$input-radius`). `color` is banned
107
+ as a property — ambiguous, use `text`.
108
+ - state — last, after `--`: `hover`, `active`, `focus`, `disabled`, `checked`, `current`, `error`
109
+
110
+ State goes last even when it reads worse: `$video-playlist-card-bg--active`, NOT
111
+ `$video-playlist-card-active-bg`. A state-looking word can be a sub-element: `$filter-popup-active-chip-bg` is
112
+ `active-chip`.
113
+
114
+ Text-only elements (`title`, `name`, `subtext`, `description`, `label`, `count`) may drop `-text`:
115
+ `$video-card-title`.
116
+
117
+ ## Typography and breakpoints
118
+
119
+ - Type: `@include font-sm` (14/20), `font-xs` (12/16), `font-base` (16/24) — never raw
120
+ `font-size`/`line-height`.
121
+ - Breakpoints: `@include from-br(sm)`, `to-br(xs)`. Container: `from-cq($bp)`. JS mirror of `$brkpnts` in
122
+ `config/`, guarded by `check:breakpoints`.
123
+ - `$brkpnts` holds layout tiers only — points the whole page frame turns on, needed by many components. A
124
+ one-off threshold of a single block (how many columns fit, where a word wraps) stays a px literal next to
125
+ that block: `from-br`/`to-br` take a number too.
126
+ - Card grids (videos, channels, playlists, categories) all key off `mixins/_cards-grid.scss` — one ladder
127
+ 1 → 2 (370px) → 3 (900px) → 4 (1440px). Take columns from `@include cards-grid-columns`, set `display` and
128
+ `gap` yourself; don't hand-roll a per-grid column ladder or an `auto-fill` variant.
129
+ - `$brkpnt-burger` (= `sd`, 1024px) is the burger ↔ desktop-menu line: below it the nav and the right half of
130
+ the header collapse into the burger. Header, nav, search, niche, language and `--header-height` key off this
131
+ variable, not off `sm` — move the line here, not per file. Utility classes: `._to-sd` / `._from-sd`.
132
+
133
+ **All mixins** (auto-injected, never `@use`): `font-4xs|3xs|xs|2xs|sm|md|base|lg|xl|2xl|3xl|4xl`,
134
+ `from-br|to-br`, `from-cq|to-cq`, `hover`, `surface`, `button-variant`, `page|page-title|page-footer-links`,
135
+ `cards-grid-columns`, `video-card|video-card-grid|video-card-poster`, `area-point`. Signatures — SCSS-mixins table in the app's
136
+ `DOCS/CATALOG.md`.
package/README.md CHANGED
@@ -143,6 +143,12 @@ assets/scss/themes/dark/_theme.scss
143
143
  подставляет приложение, и компилирует по разу на тему. То же, что делает Vite, — значит ловит и неизвестные
144
144
  миксины, и неверные аргументы `@include`, и показывает, в какой теме падает.
145
145
 
146
+ **Промежуточного семантического слоя нет** — темы ссылаются на палитру напрямую, без тира вида
147
+ `$text-muted` / `$role-border-subtle`. Сайты форкаются с принципиально разными схемами, и каждому
148
+ селектору может понадобиться произвольный цвет, а не одна из горстки ролей. Такой слой либо оброс бы
149
+ ролью на селектор (и перестал быть семантическим), либо компоненты начали бы тянуться в обход него.
150
+ Имя токена по компоненту, который он обслуживает, держит соответствие честным.
151
+
146
152
  Контекстные различия (контрол внутри попапа, вариант кнопки, зависящий от темы) решают миксины
147
153
  `surface()` и `button-variant()` из `mixins/`: место объявляет себя, а значения даёт тема через карты
148
154
  `$surfaces` / `$button-variant-overrides`. Компоненты про темы не знают. `appConfig.theme` — только
@@ -6,6 +6,9 @@
6
6
  // Виден только ниже sm (960px), на sm и шире скрыт.
7
7
  @include from-br(sm) { ._to-sm { display: none !important; } }
8
8
 
9
+ // Виден только ниже sd (1024px, граница бургера), на sd и шире скрыт.
10
+ @include from-br(sd) { ._to-sd { display: none !important; } }
11
+
9
12
  // Виден только ниже md (1350px), на md и шире скрыт.
10
13
  @include from-br(md) { ._to-md { display: none !important; } }
11
14
 
@@ -18,6 +21,9 @@
18
21
  // Виден с sm (960px) и шире, ниже скрыт.
19
22
  @include to-br(sm) { ._from-sm { display: none !important; } }
20
23
 
24
+ // Виден с sd (1024px, граница бургера) и шире, ниже скрыт.
25
+ @include to-br(sd) { ._from-sd { display: none !important; } }
26
+
21
27
  // Виден с md (1350px) и шире, ниже скрыт.
22
28
  @include to-br(md) { ._from-md { display: none !important; } }
23
29
 
@@ -17,7 +17,7 @@
17
17
  padding-block: 12px;
18
18
  border-bottom: 1px solid $layout-header-border;
19
19
 
20
- @include from-br(sm) {
20
+ @include from-br($brkpnt-burger) {
21
21
  padding-inline: 32px;
22
22
  padding-block: 12px;
23
23
  }
@@ -0,0 +1,18 @@
1
+ @use 'font-sizes' as *;
2
+ @use 'vars' as *;
3
+
4
+ // Плашка с кодом локали в переключателях языка (шапка, футер, список языков).
5
+ // Цвета не задаёт — фон и текст ставит вызывающий компонент своими токенами.
6
+ @mixin locale-badge {
7
+ @include font-xs;
8
+
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ padding: 2px 6px;
13
+ border-radius: $radius-small;
14
+ font-weight: 600;
15
+ line-height: 1;
16
+ letter-spacing: var(--tracking-wide);
17
+ text-transform: uppercase;
18
+ }
@@ -1,19 +1,40 @@
1
1
  @use 'sass:map';
2
2
  @use 'sass:list';
3
+ @use 'sass:math';
3
4
  @use 'sass:meta';
4
5
 
5
- // Брейкпоинты соответствуют брейкпоинтам vuetify https://v2.vuetifyjs.com/en/features/breakpoints/
6
+ // Тиры раскладки. Сюда попадают только точки, вокруг которых перестраивается общий каркас
7
+ // страницы, и потому нужные многим компонентам. Разовый порог одного блока (сколько колонок
8
+ // влезло, где переносится слово) держим литералом рядом с этим блоком — from-br/to-br
9
+ // принимают и число в px.
6
10
 
7
11
  $brkpnts: (
8
12
  xs: 475px,
9
13
  xm: 650px,
10
14
  sm: 960px,
15
+ sd: 1024px,
11
16
  md: 1350px,
12
17
  ml: 1500px,
13
18
  lg: 1792px,
14
19
  xl: 2560px,
15
20
  );
16
21
 
22
+ /**
23
+ Граница «бургер ↔ десктопное меню»: ниже неё навигация и правая часть шапки свёрнуты
24
+ в бургер, выше — разложены в шапку и в строку навигации под ней. Отдельная переменная,
25
+ а не литерал `sd`, чтобы двигать эту границу, не перебирая шапку по файлам.
26
+ */
27
+ $brkpnt-burger: sd;
28
+
29
+ // Литерал-порог можно писать и как 370, и как 370px — приводим к px.
30
+ @function _to-px($bp) {
31
+ @if math.is-unitless($bp) {
32
+ @return $bp * 1px;
33
+ }
34
+
35
+ @return $bp;
36
+ }
37
+
17
38
  /**
18
39
  Mobile first брейкпоинты
19
40
  @include from-br(sm) { ... }
@@ -27,7 +48,7 @@ $brkpnts: (
27
48
  @content;
28
49
  }
29
50
  } @else if meta.type-of($bp) == 'number' {
30
- $pixelyzed: $bp * 1px;
51
+ $pixelyzed: _to-px($bp);
31
52
 
32
53
  @media (min-width: $pixelyzed) {
33
54
  @content;
@@ -49,7 +70,7 @@ $brkpnts: (
49
70
  @content;
50
71
  }
51
72
  } @else if meta.type-of($bp) == 'number' {
52
- $pixelyzed: $bp * 1px - 1px;
73
+ $pixelyzed: _to-px($bp) - 1px;
53
74
 
54
75
  @media (max-width: $pixelyzed) {
55
76
  @content;
@@ -0,0 +1,32 @@
1
+ @use 'breakpoints' as *;
2
+
3
+ /**
4
+ Пороги плотности сеток карточек — тумб, каналов, плейлистов, категорий. Это не тиры
5
+ раскладки, а ответ на вопрос «сколько карточек влезает по ширине», поэтому литералы,
6
+ а не ключи $brkpnts: 900px и 1440px намеренно не совпадают с sm/md и в общую карту не идут.
7
+ */
8
+ $cards-grid-2-cols: 370px;
9
+ $cards-grid-3-cols: 900px;
10
+ $cards-grid-4-cols: 1440px;
11
+
12
+ /**
13
+ Колонки сетки карточек: 1, 2 с 370px, 3 с 900px, 4 с 1440px. Только grid-template-columns —
14
+ `display` и `gap` остаются на вызывающей стороне, они у сеток разные.
15
+ minmax(0, 1fr) обязателен, не `1fr`: нешринкуемая мета-строка (карточка истории) задаёт
16
+ колонке min-content, из-за которого грид вылезает за контейнер на телефоне.
17
+ */
18
+ @mixin cards-grid-columns {
19
+ grid-template-columns: minmax(0, 1fr);
20
+
21
+ @include from-br($cards-grid-2-cols) {
22
+ grid-template-columns: repeat(2, minmax(0, 1fr));
23
+ }
24
+
25
+ @include from-br($cards-grid-3-cols) {
26
+ grid-template-columns: repeat(3, minmax(0, 1fr));
27
+ }
28
+
29
+ @include from-br($cards-grid-4-cols) {
30
+ grid-template-columns: repeat(4, minmax(0, 1fr));
31
+ }
32
+ }
@@ -1,7 +1,9 @@
1
1
  @forward 'font-sizes';
2
2
  @forward 'accessibility';
3
+ @forward 'badge';
3
4
  @forward 'breakpoints';
4
5
  @forward 'button';
6
+ @forward 'cards-grid';
5
7
  @forward 'page';
6
8
  @forward 'surface';
7
9
  @forward 'video-card';
@@ -13,11 +13,15 @@
13
13
 
14
14
  // Заголовок страницы: font-lg, uppercase, 700. В разметке — глобальный класс `._title`.
15
15
  @mixin page-title {
16
- @include font-lg;
16
+ @include font-base;
17
17
 
18
18
  text-transform: uppercase;
19
19
  letter-spacing: var(--tracking-wide);
20
20
  font-weight: 700;
21
+
22
+ @include from-br(xm) {
23
+ @include font-lg;
24
+ }
21
25
  }
22
26
 
23
27
  // Блок SEO-ссылок внизу страницы: прижат к низу (margin-top: auto) и отбит линией.
@@ -1,4 +1,5 @@
1
1
  @use 'breakpoints' as *;
2
+ @use 'cards-grid' as *;
2
3
  @use 'font-sizes' as *;
3
4
  @use 'vars' as *;
4
5
 
@@ -10,27 +11,16 @@
10
11
  align-content: start;
11
12
  }
12
13
 
13
- /**
14
- Сетка карточек видео: 2 колонки, 3 с xs, 4 с md.
15
- minmax(0, 1fr) обязателен, не `1fr`: мета-строка карточки истории нешринкуема и задаёт
16
- колонке min-content, из-за которого грид вылезает за контейнер на телефоне.
17
- */
14
+ // Сетка карточек видео: колонки общие для всех сеток карточек, свой только зазор.
18
15
  @mixin video-card-grid {
19
16
  display: grid;
20
17
  gap: 12px;
21
- grid-template-columns: repeat(2, minmax(0, 1fr));
22
18
 
23
- @include from-br(xs) {
24
- grid-template-columns: repeat(3, minmax(0, 1fr));
25
- }
19
+ @include cards-grid-columns;
26
20
 
27
21
  @include from-br(sm) {
28
22
  gap: 24px;
29
23
  }
30
-
31
- @include from-br(md) {
32
- grid-template-columns: repeat(4, minmax(0, 1fr));
33
- }
34
24
  }
35
25
 
36
26
  // Постер карточки: 16/9, скруглённый, с обрезкой — база для абсолютных бейджей поверх.
@@ -367,6 +367,8 @@ $language-icon: $color-zinc-400;
367
367
  $language--current: $color-zinc-500;
368
368
  $language-title: $color-zinc-500;
369
369
  $language-item-text: $color-zinc-300;
370
+ $language-code-bg: $color-zinc-700;
371
+ $language-code-text: $color-zinc-200;
370
372
  $language-item-bg--hover: $color-zinc-800;
371
373
  $language-item-text--current: $color-white;
372
374
  $language-item-bg--current: $color-zinc-700;
@@ -390,11 +392,12 @@ $niche-item-bg--current: $color-zinc-700;
390
392
  $footer-language-bg: $color-zinc-800-50;
391
393
  $footer-language-bg--hover: $color-zinc-700;
392
394
  $footer-language-text: $color-zinc-400;
393
- $footer-language-menu-bg: $color-zinc-800;
394
- $footer-language-menu-border: $color-zinc-700;
395
- $footer-language-item-text: $color-zinc-300;
396
- $footer-language-item-bg--active: $color-indigo-600-20;
397
- $footer-language-item-text--active: $color-indigo-400;
395
+ $footer-language-badge-bg: $color-zinc-800;
396
+ $footer-language-badge-text: $color-zinc-200;
397
+ $footer-language-menu-bg: $color-night;
398
+ $footer-language-menu-border: $color-zinc-800;
399
+ $footer-language-menu-shadow: 0 20px 25px -5px #{$mono-black-25}, 0 8px 10px -6px #{$mono-black-25};
400
+ $footer-language-menu-item--hover: $color-zinc-800-50;
398
401
 
399
402
  // ─── Burger ───
400
403
  $burger-backdrop: $mono-black-50;
@@ -422,9 +425,12 @@ $header-bg: $color-black;
422
425
  $header-burger-icon: $color-zinc-200;
423
426
  $header-user-icon: $color-zinc-500;
424
427
  $header-language-trigger: $color-zinc-400;
425
- $header-language-angle--hover: $color-white;
428
+ $header-language-trigger--hover: $color-white;
429
+ $header-language-badge-bg: $color-zinc-800;
430
+ $header-language-badge-text: $color-zinc-200;
426
431
  $header-language-menu-bg: $color-night;
427
432
  $header-language-menu-border: $color-zinc-800;
433
+ $header-language-menu-shadow: 0 20px 25px -5px #{$mono-black-25}, 0 8px 10px -6px #{$mono-black-25};
428
434
  $header-language-menu-item--hover: $color-zinc-800-50;
429
435
 
430
436
  // ─── notifications ───
@@ -540,6 +546,10 @@ $swiper-nav-bg--hover: transparent;
540
546
  $swiper-nav-text: $color-white;
541
547
  $swiper-nav-shadow: transparent;
542
548
 
549
+ // ─── Section swiper cards ───
550
+ $section-swiper-cards-link: $color-zinc-400;
551
+ $section-swiper-cards-link--hover: $color-white;
552
+
543
553
  // ─── Select ───
544
554
  $select-icon: $color-zinc-400;
545
555
  $select-bg: $color-zinc-800-60;
@@ -424,6 +424,8 @@ $language-icon: $color-gray-400;
424
424
  $language--current: $color-gray-400;
425
425
  $language-title: $color-gray-400;
426
426
  $language-item-text: $color-gray-300;
427
+ $language-code-bg: $color-slate-700;
428
+ $language-code-text: $color-white;
427
429
  $language-item-bg--hover: $color-white-5;
428
430
  $language-item-text--current: $color-white;
429
431
  $language-item-bg--current: $color-slate-750;
@@ -447,11 +449,12 @@ $niche-item-bg--current: $color-indigo-soft;
447
449
  $footer-language-bg: $color-slate-750;
448
450
  $footer-language-bg--hover: $color-slate-700;
449
451
  $footer-language-text: $color-white;
450
- $footer-language-menu-bg: $color-slate-750;
451
- $footer-language-menu-border: $color-slate-700;
452
- $footer-language-item-text: $color-gray-300;
453
- $footer-language-item-bg--active: $color-indigo-soft;
454
- $footer-language-item-text--active: $color-white;
452
+ $footer-language-badge-bg: $color-slate-850;
453
+ $footer-language-badge-text: $color-white;
454
+ $footer-language-menu-bg: $color-slate-850;
455
+ $footer-language-menu-border: $color-white-10;
456
+ $footer-language-menu-shadow: 0 20px 25px -5px #{$mono-black-25}, 0 8px 10px -6px #{$mono-black-25};
457
+ $footer-language-menu-item--hover: $color-white-5;
455
458
 
456
459
  // ─── Burger ───
457
460
  $burger-backdrop: $mono-black-50;
@@ -479,9 +482,12 @@ $header-bg: $color-slate-800;
479
482
  $header-burger-icon: $color-gray-200;
480
483
  $header-user-icon: $color-gray-500;
481
484
  $header-language-trigger: $color-gray-500;
482
- $header-language-angle--hover: $color-gray-300;
485
+ $header-language-trigger--hover: $color-gray-300;
486
+ $header-language-badge-bg: $color-slate-750;
487
+ $header-language-badge-text: $color-white;
483
488
  $header-language-menu-bg: $color-slate-850;
484
489
  $header-language-menu-border: $color-white-10;
490
+ $header-language-menu-shadow: 0 20px 25px -5px #{$mono-black-25}, 0 8px 10px -6px #{$mono-black-25};
485
491
  $header-language-menu-item--hover: $color-white-5;
486
492
  $notifications-icon: $color-gray-300;
487
493
  $notifications-icon--mobile: $color-gray-200;
@@ -591,6 +597,10 @@ $swiper-nav-bg: $color-white;
591
597
  $swiper-nav-bg--hover: $color-gray-300;
592
598
  $swiper-nav-shadow: $mono-black-10;
593
599
 
600
+ // ─── Section swiper cards ───
601
+ $section-swiper-cards-link: $color-gray-600;
602
+ $section-swiper-cards-link--hover: $color-gray-900;
603
+
594
604
  // ─── Select ───
595
605
  $select-bg: $color-white;
596
606
  $select-icon: $color-gray-500;
@@ -38,9 +38,13 @@
38
38
  --container-padding-inline: 24px;
39
39
  --chips-height: 32px;
40
40
  --chips-height-small: 28px;
41
- --header-height: 71px;
42
41
  --control-min-width: 296px;
43
42
  --max-width-container: 1600px;
44
43
  --card-round-size: 96px;
45
44
  }
45
+
46
+ // Высота шапки меняется вместе с её раскладкой, а не на общем тире sm.
47
+ @include from-br($brkpnt-burger) {
48
+ --header-height: 71px;
49
+ }
46
50
  }
@@ -1,4 +1,4 @@
1
- <!-- Адаптивная сетка карточек категорий (auto-fill) -->
1
+ <!-- Сетка карточек категорий, 1-4 колонки -->
2
2
  <template>
3
3
  <div class="grid-categories">
4
4
  <CardCategory
@@ -22,15 +22,12 @@ defineProps<{
22
22
 
23
23
  <style lang="scss">
24
24
  .grid-categories {
25
- --min-width: 150px;
26
-
27
25
  display: grid;
28
26
  gap: 12px;
29
- grid-template-columns: repeat(auto-fill, minmax(var(--min-width), 1fr));
30
27
 
31
- @include from-br(sm) {
32
- --min-width: 250px;
28
+ @include cards-grid-columns;
33
29
 
30
+ @include from-br(sm) {
34
31
  gap: 16px;
35
32
  }
36
33
  }
@@ -1,4 +1,4 @@
1
- <!-- Сетка карточек каналов, 2-4 колонки -->
1
+ <!-- Сетка карточек каналов, 1-4 колонки -->
2
2
  <template>
3
3
  <div
4
4
  id="anchor"
@@ -28,16 +28,6 @@ defineProps<{
28
28
  display: grid;
29
29
  gap: 24px;
30
30
 
31
- @include from-br(xs) {
32
- grid-template-columns: repeat(2, 1fr);
33
- }
34
-
35
- @include from-br(sm) {
36
- grid-template-columns: repeat(3, 1fr);
37
- }
38
-
39
- @include from-br(md) {
40
- grid-template-columns: repeat(4, 1fr);
41
- }
31
+ @include cards-grid-columns;
42
32
  }
43
33
  </style>
@@ -1,4 +1,4 @@
1
- <!-- Сетка карточек плейлистов, 2-4 колонки; попап шаринга — один на список -->
1
+ <!-- Сетка карточек плейлистов, 1-4 колонки; попап шаринга — один на список -->
2
2
  <template>
3
3
  <div class="grid-playlists">
4
4
  <CardPlaylist
@@ -64,12 +64,6 @@ function onShare(card: IPlaylistCard) {
64
64
  display: grid;
65
65
  gap: 24px;
66
66
 
67
- @include from-br(xs) {
68
- grid-template-columns: repeat(2, 1fr);
69
- }
70
-
71
- @include from-br(sm) {
72
- grid-template-columns: repeat(4, 1fr);
73
- }
67
+ @include cards-grid-columns;
74
68
  }
75
69
  </style>
@@ -74,6 +74,7 @@
74
74
  />
75
75
 
76
76
  <LayoutLanguage
77
+ burger
77
78
  class="layout-burger__language"
78
79
  icon-size="20"
79
80
  />
@@ -117,13 +118,13 @@ const { isAuthorized } = useUser(AuthorizationApiService);
117
118
  const burgerRef = ref();
118
119
 
119
120
  const breakpoints = CSS_BREAKPOINTS;
120
- const { isMobile } = checkDeviceWidth(breakpoints);
121
+ const { isBurgerMenu } = checkDeviceWidth(breakpoints);
121
122
 
122
123
  const { scrollLock, scrollUnlock } = useGlobalScrollLock();
123
124
 
124
125
  watch(() => props.modelValue, (value) => {
125
126
  if (value) {
126
- if (isMobile.value) {
127
+ if (isBurgerMenu.value) {
127
128
  burgerRef.value.showModal();
128
129
  scrollLock();
129
130
  } else {
@@ -132,7 +133,7 @@ watch(() => props.modelValue, (value) => {
132
133
  } else {
133
134
  burgerRef.value.close();
134
135
 
135
- if (isMobile.value) {
136
+ if (isBurgerMenu.value) {
136
137
  scrollUnlock();
137
138
  }
138
139
  }