itube-specs 0.0.361 → 0.0.362

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.
@@ -4,11 +4,11 @@
4
4
  class="s-dropdown"
5
5
  :style="`--max-height: ${getMaxHeight}`"
6
6
  :class="[
7
- {'--top': top},
8
- {'--right': right},
9
- {'--center': center},
10
- {'--open': open},
11
- {'--separate': separateLastChild},
7
+ { '--top': top },
8
+ { '--right': right },
9
+ { '--center': center },
10
+ { '--open': open },
11
+ { '--separate': separateLastChild },
12
12
  ]"
13
13
  @mouseleave="mouseHandler(false)"
14
14
  @mouseover="mouseHandler(true)"
@@ -19,13 +19,14 @@
19
19
  >
20
20
  <slot name="trigger"></slot>
21
21
  </div>
22
+
22
23
  <transition name="s-dropdown" mode="out-in">
23
24
  <dialog
24
25
  v-if="useVIf ? open && $slots.items : true"
25
26
  :style="!useVIf ? { display: (open && $slots.items) ? '' : 'none' } : {}"
26
27
  ref="menuRef"
27
28
  class="s-dropdown__menu"
28
- :class="{'--open': open}"
29
+ :class="{ '--open': open }"
29
30
  @click="close"
30
31
  >
31
32
  <button
@@ -38,11 +39,13 @@
38
39
  type="button"
39
40
  @click="close"
40
41
  >
41
- <SIcon name="close" size="16"/>
42
+ <SIcon name="close" size="16" />
42
43
  </button>
44
+
43
45
  <slot name="header"></slot>
46
+
44
47
  <div class="s-dropdown__menu-items" v-if="$slots.items">
45
- <slot name="items" v-bind="'s-dropdown__item'"></slot>
48
+ <slot name="items" v-bind="{ class: 's-dropdown__item' }"></slot>
46
49
  </div>
47
50
  </dialog>
48
51
  </transition>
@@ -50,12 +53,12 @@
50
53
  </template>
51
54
 
52
55
  <script setup lang="ts">
53
- // На компоненте обязательно вызывать пропсы слота и вешать слот пропс (className) на непосредственных детей
56
+ import { ref, watch, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
54
57
  import { onClickOutside, useScrollLock } from '@vueuse/core';
55
58
  import { isMobile } from '../../runtime';
56
59
  import type { CssBreakpoints } from '../../types';
57
60
 
58
- const dropdownRef = ref<HTMLDialogElement | null>(null);
61
+ const dropdownRef = ref<HTMLElement | null>(null);
59
62
  const menuRef = ref<HTMLDialogElement | null>(null);
60
63
 
61
64
  const props = withDefaults(defineProps<{
@@ -67,57 +70,77 @@ const props = withDefaults(defineProps<{
67
70
  openByClick?: boolean;
68
71
  useVIf?: boolean;
69
72
  }>(), {
70
- useVIf: false
73
+ useVIf: false,
71
74
  });
72
75
 
73
76
  const open = ref(false);
74
77
 
75
78
  const breakpoints = useAppConfig().cssBreakpoints as Record<CssBreakpoints, number>;
76
79
 
77
- // Блокируем скролл body при открытии на мобильных
78
- const bodyScrollLocked = useScrollLock(document.body);
80
+ // SSR-safe: тело страницы доступно только на клиенте
81
+ const bodyElement = ref<HTMLElement | null>(null);
82
+ const bodyScrollLocked = useScrollLock(bodyElement);
83
+
84
+ onMounted(() => {
85
+ bodyElement.value = document.body;
86
+ });
79
87
 
88
+ // Закрытие дропдауна
80
89
  function close() {
81
90
  open.value = false;
82
91
 
83
- if (isMobile(breakpoints).value) {
92
+ if (isMobile(breakpoints).value && process.client) {
84
93
  nextTick(() => {
85
- menuRef.value?.close?.();
86
- bodyScrollLocked.value = false; // разблокируем
94
+ menuRef.value?.close();
95
+ bodyScrollLocked.value = false;
87
96
  });
88
97
  }
89
98
  }
90
99
 
100
+ // Открытие по клику или ховеру
91
101
  function openDropdown() {
92
102
  if (props.openByClick) {
93
103
  open.value = !open.value;
94
104
  }
95
105
 
96
- if (isMobile(breakpoints).value) {
106
+ if (isMobile(breakpoints).value && process.client) {
97
107
  open.value = true;
98
108
 
99
109
  nextTick(() => {
100
110
  if (menuRef.value?.showModal) {
101
111
  menuRef.value.showModal();
102
- bodyScrollLocked.value = true; // блокируем
112
+ bodyScrollLocked.value = true;
103
113
  }
104
114
  });
105
115
  }
106
116
  }
107
117
 
108
- watch(open, (isOpen) => {
109
- if (isOpen) {
110
- onClickOutside(dropdownRef, () => {
111
- close();
112
- });
118
+ // Ховер-логика только на десктопе и без openByClick
119
+ function mouseHandler(isEnter: boolean) {
120
+ if (!isMobile(breakpoints).value && !props.openByClick && process.client) {
121
+ open.value = isEnter;
113
122
  }
114
- }, { immediate: true });
123
+ }
124
+
125
+ // Клик вне дропдауна
126
+ watch(
127
+ open,
128
+ (isOpen) => {
129
+ if (isOpen && process.client && dropdownRef.value) {
130
+ onClickOutside(dropdownRef.value, () => {
131
+ close();
132
+ });
133
+ }
134
+ },
135
+ { immediate: true }
136
+ );
115
137
 
116
- function mouseHandler(event: boolean) {
117
- if (!isMobile(breakpoints).value && !props.openByClick) {
118
- open.value = event;
138
+ // Гарантированная разблокировка скролла при уходе со страницы
139
+ onBeforeUnmount(() => {
140
+ if (process.client) {
141
+ bodyScrollLocked.value = false;
119
142
  }
120
- }
143
+ });
121
144
 
122
145
  const getMaxHeight = computed(() => {
123
146
  return props.maxHeight ? `${props.maxHeight}px` : 'none';
@@ -2,16 +2,13 @@
2
2
  <dialog
3
3
  ref="notification"
4
4
  class="s-notification"
5
- :class="{'--popup': popup}"
5
+ :class="{ '--popup': popup }"
6
6
  @click="closeByBackdropClick"
7
7
  @cancel="close"
8
8
  >
9
9
  <div class="s-notification__wrapper">
10
10
  <header class="s-notification__header">
11
- <p
12
- v-if="$slots.title"
13
- class="s-notification__title"
14
- >
11
+ <p v-if="$slots.title" class="s-notification__title">
15
12
  <slot name="title"></slot>
16
13
  </p>
17
14
  <button
@@ -21,16 +18,13 @@
21
18
  aria-label="close"
22
19
  @click="close"
23
20
  >
24
- <SIcon class="s-notification__close-icon" name="close" size="16"/>
21
+ <SIcon class="s-notification__close-icon" name="close" size="16" />
25
22
  </button>
26
23
  </header>
27
24
  <div class="s-notification__content">
28
25
  <slot></slot>
29
26
  </div>
30
- <div
31
- v-if="$slots.footer"
32
- class="s-notification__footer"
33
- >
27
+ <div v-if="$slots.footer" class="s-notification__footer">
34
28
  <slot name="footer"></slot>
35
29
  </div>
36
30
  </div>
@@ -38,58 +32,71 @@
38
32
  </template>
39
33
 
40
34
  <script setup lang="ts">
41
- import { ref, watch, onBeforeUnmount } from 'vue';
42
- import { useScrollLock } from '@vueuse/core';
43
- import { onBackdropClick } from '../../runtime'; // оставляем, если используется где-то ещё
35
+ import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
36
+ import { useScrollLock } from '@vueuse/core'
37
+ import { onBackdropClick } from '../../runtime'
44
38
 
45
- const notification = ref<HTMLDialogElement | null>(null);
39
+ const notification = ref<HTMLDialogElement | null>(null)
46
40
 
47
41
  const emit = defineEmits<{
48
- (eventName: 'update:modelValue', value: boolean): void
49
- (eventName: 'close'): void
50
- }>();
42
+ (e: 'update:modelValue', value: boolean): void
43
+ (e: 'close'): void
44
+ }>()
51
45
 
52
46
  const props = defineProps<{
53
47
  modelValue: boolean
54
48
  popup?: boolean
55
- }>();
49
+ }>()
50
+
51
+ // Создаём элемент ТОЛЬКО на клиенте
52
+ const bodyElement = ref<HTMLElement | null>(null)
53
+ const isBodyScrollLocked = useScrollLock(bodyElement)
56
54
 
57
- // Блокировка скролла тела страницы (body) при открытом модальном окне
58
- const bodyElement = ref(document.body);
59
- const isBodyScrollLocked = useScrollLock(bodyElement);
55
+ // Инициализируем body только после монтирования на клиенте
56
+ onMounted(() => {
57
+ bodyElement.value = document.body
58
+ })
60
59
 
60
+ // Реактивно управляем открытием/закрытием и блокировкой скролла
61
61
  watch(
62
62
  () => props.modelValue,
63
- (newVal) => {
64
- if (newVal && props.popup) {
65
- // Открываем dialog и блокируем скролл
66
- notification.value?.showModal();
67
- isBodyScrollLocked.value = true;
68
- } else if (!newVal && props.popup) {
69
- // Закрываем и разблокируем
70
- notification.value?.close();
71
- isBodyScrollLocked.value = false;
72
- } else if (newVal && !props.popup) {
73
- notification.value?.show();
74
- } else if (!newVal && !props.popup) {
75
- notification.value?.close();
63
+ (isOpen) => {
64
+ if (!notification.value) return
65
+
66
+ if (isOpen && props.popup) {
67
+ notification.value.showModal()
68
+ // Блокировка только если мы уже на клиенте и body доступен
69
+ if (process.client && bodyElement.value) {
70
+ isBodyScrollLocked.value = true
71
+ }
72
+ } else if (!isOpen && props.popup) {
73
+ notification.value.close()
74
+ if (process.client) {
75
+ isBodyScrollLocked.value = false
76
+ }
77
+ } else if (isOpen && !props.popup) {
78
+ notification.value.show()
79
+ } else if (!isOpen && !props.popup) {
80
+ notification.value.close()
76
81
  }
77
82
  },
78
83
  { immediate: true }
79
- );
84
+ )
80
85
 
81
- // Дополнительная очистка при размонтировании (на всякий случай)
86
+ // Гарантированная разблокировка при уходе со страницы
82
87
  onBeforeUnmount(() => {
83
- isBodyScrollLocked.value = false;
84
- });
88
+ if (process.client) {
89
+ isBodyScrollLocked.value = false
90
+ }
91
+ })
85
92
 
86
93
  function close() {
87
- emit('close');
88
- emit('update:modelValue', false);
94
+ emit('close')
95
+ emit('update:modelValue', false)
89
96
  }
90
97
 
91
98
  function closeByBackdropClick(event: Event) {
92
- const target = event.target as HTMLDialogElement;
93
- onBackdropClick(target, close);
99
+ const target = event.target as HTMLDialogElement
100
+ onBackdropClick(target, close)
94
101
  }
95
102
  </script>
@@ -1,146 +1,144 @@
1
1
  <template>
2
- <dialog
3
- ref="popupRef"
4
- class="s-popup"
5
- :class="[
6
- {'--sheet': sheet},
7
- {'--transparent-backdrop': transparentBackdrop},
8
- {'--aside': $slots.aside}
9
- ]"
10
- @click="closeByBackdropClick"
11
- @cancel="close"
12
- >
13
- <div class="s-popup__wrapper">
14
- <div
15
- v-if="sheet"
16
- class="s-popup__sheet"
17
- >
18
- <button
19
- class="s-popup__sheet-handler"
20
- type="button"
21
- @click="close"
22
- ></button>
23
- </div>
2
+ <dialog
3
+ ref="popupRef"
4
+ class="s-popup"
5
+ :class="[
6
+ { '--sheet': sheet },
7
+ { '--transparent-backdrop': transparentBackdrop },
8
+ { '--aside': $slots.aside }
9
+ ]"
10
+ @click="closeByBackdropClick"
11
+ @cancel="close"
12
+ >
13
+ <div class="s-popup__wrapper">
14
+ <div v-if="sheet" class="s-popup__sheet">
15
+ <button
16
+ class="s-popup__sheet-handler"
17
+ type="button"
18
+ @click="close"
19
+ ></button>
20
+ </div>
24
21
 
25
- <aside
26
- v-if="$slots.aside && !isMobile(breakpoints).value"
27
- class="s-popup__aside"
28
- >
29
- <slot name="aside"></slot>
30
- </aside>
31
-
32
- <header
33
- class="s-popup__header"
34
- >
35
- <button
36
- v-if="back"
37
- class="s-popup__back"
38
- type="button"
39
- @click="$emit('back')"
40
- >
41
- <SIcon name="angle-left" size="24" />
42
- </button>
43
- <div
44
- v-if="$slots.title"
45
- class="s-popup__title"
22
+ <aside
23
+ v-if="$slots.aside && !isMobile(breakpoints).value"
24
+ class="s-popup__aside"
46
25
  >
47
- <slot name="title"></slot>
26
+ <slot name="aside"></slot>
27
+ </aside>
28
+
29
+ <header class="s-popup__header">
30
+ <button
31
+ v-if="back"
32
+ class="s-popup__back"
33
+ type="button"
34
+ @click="$emit('back')"
35
+ >
36
+ <SIcon name="angle-left" size="24" />
37
+ </button>
38
+
39
+ <div v-if="$slots.title" class="s-popup__title">
40
+ <slot name="title"></slot>
41
+ </div>
42
+
43
+ <button
44
+ class="s-popup__close"
45
+ type="button"
46
+ @click="close"
47
+ >
48
+ <SIcon name="close" size="24" />
49
+ </button>
50
+ </header>
51
+
52
+ <div v-if="$slots.fixedContent" class="s-popup__subheader">
53
+ <slot name="fixedContent"></slot>
54
+ </div>
55
+
56
+ <div v-if="$slots.default" class="s-popup__content">
57
+ <slot></slot>
58
+ </div>
59
+
60
+ <div v-if="$slots.footer" class="s-popup__footer">
61
+ <slot name="footer"></slot>
48
62
  </div>
49
- <button
50
- class="s-popup__close"
51
- type="button"
52
- @click="close"
53
- >
54
- <SIcon name="close" size="24" />
55
- </button>
56
- </header>
57
- <div
58
- v-if="$slots.fixedContent"
59
- class="s-popup__subheader"
60
- >
61
- <slot name="fixedContent"></slot>
62
- </div>
63
- <div
64
- v-if="$slots.default"
65
- class="s-popup__content"
66
- >
67
- <slot></slot>
68
- </div>
69
- <div
70
- v-if="$slots.footer"
71
- class="s-popup__footer"
72
- >
73
- <slot name="footer"></slot>
74
63
  </div>
75
- </div>
76
-
77
- <transition mode="out-in">
78
- <SSnackbar
79
- v-if="snackbarText && isSnackBarInPopup"
80
- />
81
- </transition>
82
- </dialog>
64
+
65
+ <transition mode="out-in">
66
+ <SSnackbar v-if="snackbarText && isSnackBarInPopup" />
67
+ </transition>
68
+ </dialog>
83
69
  </template>
84
70
 
85
71
  <script setup lang="ts">
72
+ import { ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
73
+ import { useScrollLock } from '@vueuse/core';
86
74
  import { onBackdropClick, isMobile } from '../../runtime';
87
75
  import type { CssBreakpoints } from '../../types';
88
- import { useScrollLock } from '@vueuse/core';
76
+ import { useSnackbar } from '~/composables/useSnackbar'; // предполагаю, что это composable
89
77
 
90
- const {resetSnackbar} = useSnackbar();
78
+ const popupRef = ref<HTMLDialogElement | null>(null);
91
79
 
92
- const breakpoints = useAppConfig().cssBreakpoints as Record<CssBreakpoints, number>;
93
- const popupRef = ref()
94
80
  const emit = defineEmits<{
95
- (eventName: 'update:modelValue', value: boolean): void
96
- (eventName: 'back'): void
97
- (eventName: 'close'): void
98
- }>()
81
+ (e: 'update:modelValue', value: boolean): void;
82
+ (e: 'back'): void;
83
+ (e: 'close'): void;
84
+ }>();
99
85
 
100
86
  const props = defineProps<{
101
- modelValue: boolean
102
- back?: boolean
103
- transparentBackdrop?: boolean
104
- sheet?: boolean
105
- title?: string
106
- notModal?: boolean
107
- }>()
108
-
109
- const isLocked = useScrollLock(
110
- typeof document !== 'undefined' ? document.body : null
111
- );
87
+ modelValue: boolean;
88
+ back?: boolean;
89
+ transparentBackdrop?: boolean;
90
+ sheet?: boolean;
91
+ title?: string;
92
+ notModal?: boolean;
93
+ }>();
112
94
 
113
- watch(
114
- () => props.modelValue,
115
- (newVal) => {
116
- if (newVal) {
117
- nextTick(() => {
118
- isLocked.value = true;
119
-
120
- if (props.notModal) {
121
- popupRef.value?.show();
122
- } else {
123
- popupRef.value?.showModal();
124
- }
125
- popupRef.value?.focus({ preventScroll: true });
126
- });
127
- } else {
128
- isLocked.value = false;
129
- }
130
- },
131
- { immediate: true }
132
- );
95
+ const breakpoints = useAppConfig().cssBreakpoints as Record<CssBreakpoints, number>;
96
+
97
+ // SSR-safe scroll lock
98
+ const bodyElement = ref<HTMLElement | null>(null);
99
+ const isLocked = useScrollLock(bodyElement);
133
100
 
134
101
  onMounted(() => {
102
+ bodyElement.value = document.body;
103
+
104
+ // Включаем режим отображения снакбара внутри попапа
135
105
  isSnackBarInPopup.value = true;
136
106
  resetSnackbar();
137
107
  });
138
108
 
139
109
  onBeforeUnmount(() => {
140
110
  isSnackBarInPopup.value = false;
141
- isLocked.value = false;
111
+ if (process.client) {
112
+ isLocked.value = false;
113
+ }
142
114
  });
143
115
 
116
+ // Реактивное открытие/закрытие + блокировка скролла
117
+ watch(
118
+ () => props.modelValue,
119
+ async (isOpen) => {
120
+ if (!process.client || !popupRef.value) return;
121
+
122
+ if (isOpen) {
123
+ await nextTick(); // гарантируем, что dialog уже в DOM
124
+
125
+ if (props.notModal) {
126
+ popupRef.value.show();
127
+ } else {
128
+ popupRef.value.showModal();
129
+ }
130
+
131
+ popupRef.value.focus({ preventScroll: true });
132
+ isLocked.value = true;
133
+ } else {
134
+ isLocked.value = false;
135
+ // dialog автоматически закрывается при удалении из DOM, но на всякий случай
136
+ popupRef.value.close();
137
+ }
138
+ },
139
+ { immediate: true }
140
+ );
141
+
144
142
  function close() {
145
143
  emit('close');
146
144
  emit('update:modelValue', false);
@@ -151,5 +149,6 @@ function closeByBackdropClick(event: Event) {
151
149
  onBackdropClick(target, close);
152
150
  }
153
151
 
154
- const { snackbarText, isSnackBarInPopup } = useSnackbar();
152
+ // Доступ к состоянию снакбара
153
+ const { snackbarText, isSnackBarInPopup, resetSnackbar } = useSnackbar();
155
154
  </script>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.361",
4
+ "version": "0.0.362",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {