adata-ui 2.1.41-beta.15 → 2.1.41-beta.17

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.
@@ -51,9 +51,9 @@ defineProps<Props>()
51
51
 
52
52
  <!-- текст -->
53
53
  <div class="flex flex-col items-center gap-2">
54
- <h3 class=" text-primary">
54
+ <p class=" text-primary text-body-lg">
55
55
  {{ title }}
56
- </h3>
56
+ </p>
57
57
 
58
58
  <p
59
59
  v-if="subtitle"
@@ -105,22 +105,24 @@ function onSelect(loc: LocaleItem) {
105
105
  role="option"
106
106
  :aria-selected="localeCode(loc) === locale"
107
107
  >
108
+ <!-- Текущий язык — не ссылка: якорь на саму страницу краулеры считают циклической ссылкой -->
109
+ <span
110
+ v-if="localeCode(loc) === locale"
111
+ :data-test-id="`header-switch-${localeCode(loc)}-language-button${testIdSuffix}`"
112
+ class="flex w-full items-center justify-between gap-3 rounded-md bg-brand-subtle px-3 py-2 text-body-md font-medium text-brand"
113
+ @click="isOpen = false"
114
+ >
115
+ <span>{{ t(`lang.${localeCode(loc)}.long`) }}</span>
116
+ <check class="size-4 shrink-0 text-brand" />
117
+ </span>
108
118
  <nuxt-link
119
+ v-else
109
120
  :data-test-id="`header-switch-${localeCode(loc)}-language-button${testIdSuffix}`"
110
121
  :to="switchLocalePath(localeCode(loc))"
111
- class="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-body-md transition-colors duration-100"
112
- :class="[
113
- localeCode(loc) === locale
114
- ? 'bg-brand-subtle font-medium text-brand'
115
- : 'text-quaternary hover:bg-action-secondary-hover',
116
- ]"
122
+ class="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-body-md text-quaternary transition-colors duration-100 hover:bg-action-secondary-hover"
117
123
  @click.prevent="onSelect(loc)"
118
124
  >
119
125
  <span>{{ t(`lang.${localeCode(loc)}.long`) }}</span>
120
- <check
121
- v-if="localeCode(loc) === locale"
122
- class="size-4 shrink-0 text-brand"
123
- />
124
126
  </nuxt-link>
125
127
  </li>
126
128
  </ul>
@@ -253,6 +253,7 @@ function onForgotPassword() {
253
253
  size="lg"
254
254
  autocomplete="email"
255
255
  :start-icon="AIconMail"
256
+ start-icon-class="size-4 text-icon-secondary"
256
257
  :placeholder="t('modals.id.login.placeholders.email')"
257
258
  :error="getError('username')"
258
259
  data-test-id="login-form-email-input"
@@ -268,6 +269,7 @@ function onForgotPassword() {
268
269
  :input-id="passwordFieldId"
269
270
  size="lg"
270
271
  :start-icon="AIconLock"
272
+ start-icon-class="size-4 text-icon-secondary"
271
273
  :placeholder="t('modals.id.login.placeholders.password')"
272
274
  :error="getError('password')"
273
275
  data-test-id="login-form-password-input"
@@ -116,6 +116,7 @@ async function onSubmit() {
116
116
  size="lg"
117
117
  autocomplete="new-password"
118
118
  :start-icon="AIconLock"
119
+ start-icon-class="size-4 text-icon-secondary"
119
120
  :placeholder="t('modals.id.newPassword.placeholder.1')"
120
121
  :error="getError('password')"
121
122
  />
@@ -133,6 +134,7 @@ async function onSubmit() {
133
134
  size="lg"
134
135
  autocomplete="new-password"
135
136
  :start-icon="AIconLock"
137
+ start-icon-class="size-4 text-icon-secondary"
136
138
  :placeholder="t('modals.id.newPassword.placeholder.2')"
137
139
  :error="getError('password_confirmation')"
138
140
  />
@@ -97,6 +97,7 @@ watch(recoveryModal, (value) => {
97
97
  size="lg"
98
98
  autocomplete="email"
99
99
  :start-icon="AIconMail"
100
+ start-icon-class="size-4 text-icon-secondary"
100
101
  :placeholder="t('modals.id.recovery.placeholder')"
101
102
  :error="getError('emailField')"
102
103
  />
@@ -168,6 +168,7 @@ watch(registrationModal, (value) => {
168
168
  size="lg"
169
169
  autocomplete="email"
170
170
  :start-icon="AIconMail"
171
+ start-icon-class="size-4 text-icon-secondary"
171
172
  :placeholder="t('modals.id.register.placeholders.email')"
172
173
  :error="getError('email')"
173
174
  />
@@ -183,6 +184,7 @@ watch(registrationModal, (value) => {
183
184
  size="lg"
184
185
  autocomplete="new-password"
185
186
  :start-icon="AIconLock"
187
+ start-icon-class="size-4 text-icon-secondary"
186
188
  :placeholder="t('modals.id.register.placeholders.password')"
187
189
  :error="getError('password')"
188
190
  />
@@ -200,6 +202,7 @@ watch(registrationModal, (value) => {
200
202
  size="lg"
201
203
  autocomplete="new-password"
202
204
  :start-icon="AIconLock"
205
+ start-icon-class="size-4 text-icon-secondary"
203
206
  :placeholder="t('modals.id.register.placeholders.password_confirmation')"
204
207
  :error="getError('password_confirmation')"
205
208
  />
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts" generic="T extends NavigationItem">
2
+ import type { NavigationItem } from './types'
2
3
  import Avatar from '#adata-ui/icons/avatar.vue'
3
4
  import IconLogout from '#adata-ui/icons/logout.vue'
4
5
  import { useUrls } from '#adata-ui/composables/useUrls'
@@ -9,6 +10,7 @@ const props = withDefaults(defineProps<Props>(), {
9
10
  balance: 0,
10
11
  rate: 'Базовый',
11
12
  showLogIn: true,
13
+ columns: 3,
12
14
  })
13
15
  const emit = defineEmits(['update:modelValue', 'logout'])
14
16
  const { t } = useI18n()
@@ -17,6 +19,7 @@ const urls = useUrls()
17
19
 
18
20
  interface Props {
19
21
  grow?: boolean
22
+ columns?: 3 | 4
20
23
  items: T[]
21
24
  modelValue: string | null
22
25
  replenish?: string
@@ -57,13 +60,16 @@ const profile: string = 'modals.mobile_navigation.type_profile'
57
60
 
58
61
  <template>
59
62
  <header class="mx-3 mb-3 overflow-hidden rounded-full border border-default bg-main/10 shadow-lg backdrop-blur-lg backdrop-saturate-140">
60
- <div class="grid w-full grid-cols-3 justify-items-center">
63
+ <div
64
+ class="grid w-full justify-items-center"
65
+ :class="columns === 4 ? 'grid-cols-4' : 'grid-cols-3'"
66
+ >
61
67
  <slot>
62
68
  <button
63
- v-for="(item, index) in items"
64
- :key="index"
69
+ v-for="item in items"
70
+ :key="item.value"
65
71
  class="h-16 w-auto min-w-16 max-w-[168px] lg:hover:bg-brand-subtle lg:hover:text-link-hover"
66
- :class="{ grow, 'text-brand': modelValue === item.value }"
72
+ :class="{ grow, 'text-brand': modelValue === item.value || item.active }"
67
73
  @click="onClick(item.value)"
68
74
  >
69
75
  <span class="flex flex-col items-center justify-center text-body-sm lg:text-body-md">
@@ -0,0 +1,6 @@
1
+ export interface NavigationItem {
2
+ label: string
3
+ value: string
4
+ icon?: any
5
+ active?: boolean
6
+ }
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import type { NavigationItem } from '#adata-ui/components/navigation/bottom-navigation/types'
2
3
  import type { ProjectKeys } from '#adata-ui/components/navigation/header/types'
3
4
  import ContactsMobileModel from '#adata-ui/components/modals/ContactsMobileModel.vue'
4
5
  import ContentNavigationModal from '#adata-ui/components/modals/ContentNavigationModal.vue'
@@ -21,6 +22,7 @@ interface Props {
21
22
  showLogIn?: boolean
22
23
  module?: ProjectKeys
23
24
  oldVersion?: string
25
+ extraItem?: NavigationItem
24
26
  }
25
27
  const props = withDefaults(defineProps<Props>(), {
26
28
  limitRemaining: 0,
@@ -29,8 +31,12 @@ const props = withDefaults(defineProps<Props>(), {
29
31
  rate: 'Базовый',
30
32
  showLogIn: true,
31
33
  module: 'pk',
34
+ extraItem: undefined,
32
35
  })
33
- const emit = defineEmits(['logout'])
36
+ const emit = defineEmits<{
37
+ logout: []
38
+ select: [value: string]
39
+ }>()
34
40
  useCurrentModule().value = props.module
35
41
  const { landing } = useUrls()
36
42
  const activeModule = ref<string | null>(null)
@@ -38,24 +44,34 @@ const isMain = ref(false)
38
44
  const isContacts = ref(false)
39
45
  const isProfile = ref(false)
40
46
  const { t } = useI18n()
41
- const items = [
42
- {
43
- label: 'modals.mobile_navigation.type_tariff',
44
- icon: IconInvoice,
45
- value: 'tariff',
46
- },
47
- {
48
- label: 'modals.mobile_navigation.type_main',
49
- icon: IconMenu,
50
- value: 'main',
51
- },
52
- ]
47
+ const items = computed<NavigationItem[]>(() => {
48
+ const baseItems: NavigationItem[] = [
49
+ {
50
+ label: 'modals.mobile_navigation.type_tariff',
51
+ icon: IconInvoice,
52
+ value: 'tariff',
53
+ },
54
+ {
55
+ // с четвёртым пунктом «Продукты и решения» не влезает
56
+ label: props.extraItem
57
+ ? 'modals.mobile_navigation.type_main_short'
58
+ : 'modals.mobile_navigation.type_main',
59
+ icon: IconMenu,
60
+ value: 'main',
61
+ },
62
+ ]
63
+ return props.extraItem ? [props.extraItem, ...baseItems] : baseItems
64
+ })
53
65
  function onLogout() {
54
66
  emit('logout')
55
67
  isProfile.value = false
56
68
  }
57
69
  watch(activeModule, (e) => {
58
- if (e === 'main') {
70
+ if (!e) return
71
+ if (e === props.extraItem?.value) {
72
+ emit('select', e)
73
+ }
74
+ else if (e === 'main') {
59
75
  isMain.value = true
60
76
  }
61
77
  else if (e === 'contacts') {
@@ -103,6 +119,7 @@ watch(activeModule, (e) => {
103
119
  :rate="rate"
104
120
  :balance="balance"
105
121
  :items="items"
122
+ :columns="extraItem ? 4 : 3"
106
123
  @logout="onLogout"
107
124
  />
108
125
  </div>
package/lang/en.ts CHANGED
@@ -704,6 +704,7 @@ const EnLocale: RuLocale = {
704
704
  },
705
705
  mobile_navigation: {
706
706
  type_main: 'Products and Solutions',
707
+ type_main_short: 'Products',
707
708
  type_contacts: 'Contacts',
708
709
  type_tariff: 'Tariffs',
709
710
  type_login: 'Login',
package/lang/kk.ts CHANGED
@@ -705,6 +705,7 @@ const KkLocale: RuLocale = {
705
705
  },
706
706
  mobile_navigation: {
707
707
  type_main: 'Өнімдер мен шешімдер',
708
+ type_main_short: 'Өнімдер',
708
709
  type_contacts: 'Байланыс',
709
710
  type_tariff: 'Тарифтер',
710
711
  type_login: 'Кіру',
package/lang/ru.ts CHANGED
@@ -705,6 +705,7 @@ const RuLocale = {
705
705
  },
706
706
  mobile_navigation: {
707
707
  type_main: 'Продукты и решения',
708
+ type_main_short: 'Продукты',
708
709
  type_contacts: 'Контакты',
709
710
  type_tariff: 'Тарифы',
710
711
  type_login: 'Войти',
package/nuxt.config.ts CHANGED
@@ -3,6 +3,30 @@ import { createResolver } from '@nuxt/kit'
3
3
  import tailwindcss from '@tailwindcss/vite'
4
4
 
5
5
  const { resolve } = createResolver(import.meta.url)
6
+
7
+ // Стенд: `adtdev` | `adada` | `adata`. То же значение, из которого `useUrls()`
8
+ // собирает адреса сервисов. `NUXT_PUBLIC_MODE` — предпочтительное имя (по нему
9
+ // же Nuxt переопределяет `runtimeConfig.public.mode`); `MODE` оставлен для
10
+ // консьюмеров слоя, которые ещё не переименовали переменную.
11
+ const mode = process.env.NUXT_PUBLIC_MODE ?? process.env.MODE
12
+
13
+ // Тема живёт в куке `colorMode` и должна быть одна на все поддомены
14
+ // (`<mode>.kz`, `pk.<mode>.kz`, `avto.<mode>.kz`, `id.<mode>.kz` …), поэтому
15
+ // домен куки считаем из `mode`, а не держим отдельной переменной окружения:
16
+ // `COLOR_MODE_COOKIE_DOMAIN` жила своей жизнью и её просто забывали — из 12
17
+ // консьюмеров слоя она была прописана только в pk-frontend, в
18
+ // adata-main-front-v2 не во всех `.env`, в avto-front её не было вообще. Чем
19
+ // именно это ломало синхронизацию — см. `server/middleware/color-mode-cookie.ts`,
20
+ // он же разово вычищает host-only куки, выставленные до этого фикса.
21
+ //
22
+ // В dev домен не ставим: на localhost браузер выбросит куку с
23
+ // `Domain=.adtdev.kz`, и переключатель темы перестанет переживать перезагрузку.
24
+ // `nuxt build` идёт с `NODE_ENV=production`, `nuxt dev` — с `development`, так
25
+ // что сборка для любого стенда (dev/staging/master) домен получает.
26
+ const colorModeCookieDomain = process.env.NODE_ENV === 'production' && mode
27
+ ? `.${mode}.kz`
28
+ : undefined
29
+
6
30
  export default defineNuxtConfig({
7
31
 
8
32
  app: {
@@ -43,7 +67,7 @@ export default defineNuxtConfig({
43
67
  cookieAttrs: {
44
68
  path: '/',
45
69
  maxAge: 60 * 60 * 24 * 365,
46
- domain: process.env.COLOR_MODE_COOKIE_DOMAIN,
70
+ domain: colorModeCookieDomain,
47
71
  },
48
72
  },
49
73
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "adata-ui",
3
3
  "type": "module",
4
- "version": "2.1.41-beta.15",
4
+ "version": "2.1.41-beta.17",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground",
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Разовая миграция куки `colorMode`: убираем host-only дубль.
3
+ *
4
+ * Пока домен куки приезжал отдельной переменной `COLOR_MODE_COOKIE_DOMAIN`
5
+ * (её прописали далеко не везде — см. комментарий к `colorMode` в
6
+ * `nuxt.config.ts`), часть консьюмеров слоя писала `colorMode` без `Domain`, то
7
+ * есть host-only на свой поддомен. Причём писала на КАЖДОЙ загрузке страницы:
8
+ * `setPreferenceToStorage()` в клиентском плагине @nuxtjs/color-mode вызывается
9
+ * из `watch(preference, { immediate: true })`, а не только по клику. Поэтому
10
+ * рядом с доменной кукой `.<mode>.kz` от соседнего сервиса в браузере
11
+ * оказываются ДВЕ куки с именем `colorMode`, и браузер отдаёт обе.
12
+ *
13
+ * Дальше ломается сам модуль:
14
+ * 1. Его inline-скрипт в `<head>` читает куку как `split('; colorMode=')` и
15
+ * возвращает значение только при одном вхождении. При двух — `undefined`,
16
+ * то есть откат на `preference`, у нас это `light`.
17
+ * 2. Состояние с SSR помечено `unknown: true`, и из-за этого флага хук
18
+ * `app:mounted` затирает `colorMode.preference` значением из
19
+ * inline-скрипта — тем самым `light` — и сохраняет его в куку.
20
+ *
21
+ * Итог: заход на «сломанный» поддомен сбрасывал тему в светлую сразу для всех
22
+ * остальных, потому что доменная кука общая. Новая сборка сама это не вылечит:
23
+ * host-only кука уже стоит в браузере и живёт год с последнего до-фиксного
24
+ * визита.
25
+ *
26
+ * Чистим здесь, а не в клиентском плагине, по двум причинам. Во-первых,
27
+ * `Set-Cookie` из ответа браузер применяет до того, как выполнит inline-скрипт
28
+ * в `<head>`, поэтому корректной оказывается уже первая загрузка — без вспышки
29
+ * светлой темы и без перезаписи общей куки. Во-вторых, здесь не нужно трогать
30
+ * внутренности модуля (`window.__NUXT_COLOR_MODE__`).
31
+ *
32
+ * `Set-Cookie` без `Domain` адресует ровно host-only запись — доменную такой
33
+ * набор атрибутов не задевает.
34
+ *
35
+ * Мидлвар можно снести, когда истекут куки, выставленные до фикса.
36
+ */
37
+
38
+ const KEY = 'colorMode'
39
+
40
+ export default defineEventHandler((event) => {
41
+ const cookieHeader = getRequestHeader(event, 'cookie')
42
+
43
+ if (!cookieHeader) return
44
+
45
+ // Только документы: `Set-Cookie` на ответе статики или API нам не нужен, а на
46
+ // ответах с `cache-control: public` (такие route rules есть у консьюмеров) он
47
+ // ещё и попал бы в общий кеш.
48
+ if (!getRequestHeader(event, 'accept')?.includes('text/html')) return
49
+
50
+ const duplicated = cookieHeader
51
+ .split(';')
52
+ .filter(pair => pair.trimStart().startsWith(`${KEY}=`))
53
+ .length > 1
54
+
55
+ if (!duplicated) return
56
+
57
+ setCookie(event, KEY, '', { path: '/', maxAge: 0 })
58
+ })