@skyservice-developers/vue-dev-kit 1.1.1 → 1.2.0

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,118 +1,339 @@
1
1
  <template>
2
- <header class="sky-header">
3
- <div class="header-content">
4
- <div class="header-top">
5
- <div class="header-title-wrapper">
2
+ <header class="sky-header-container">
3
+ <div class="topmenubox">
4
+ <div class="header-left">
5
+ <button
6
+ v-if="shouldShowBackButton"
7
+ class="btn-back"
8
+ @click="handleBack"
9
+ :title="backButtonTitle"
10
+ >
11
+ <svg
12
+ width="15"
13
+ height="15"
14
+ viewBox="0 0 451.847 451.847"
15
+ style="transform: rotate(90deg)"
16
+ >
17
+ <path
18
+ fill="currentColor"
19
+ d="M225.923,354.706c-8.098,0-16.195-3.092-22.369-9.263L9.27,151.157c-12.359-12.359-12.359-32.397,0-44.751c12.354-12.354,32.388-12.354,44.748,0l171.905,171.915l171.906-171.909c12.359-12.354,32.391-12.354,44.744,0c12.365,12.354,12.365,32.392,0,44.751L248.292,345.449C242.115,351.621,234.018,354.706,225.923,354.706z"
20
+ />
21
+ </svg>
22
+ </button>
23
+ <div ref="dropdownRef" class="titleAndDesc">
6
24
  <button
7
- v-if="shouldShowBackButton"
8
- class="btn-back"
9
- @click="handleBack"
10
- :title="backButtonTitle"
25
+ class="title-dropdown-toggle"
26
+ :class="{ 'title-dropdown-toggle-active': sortedItems.length }"
27
+ @click="toggleDropdown"
11
28
  >
12
- <svg width="15" height="15" viewBox="0 0 451.847 451.847" style="transform: rotate(90deg)">
13
- <path fill="currentColor" d="M225.923,354.706c-8.098,0-16.195-3.092-22.369-9.263L9.27,151.157c-12.359-12.359-12.359-32.397,0-44.751c12.354-12.354,32.388-12.354,44.748,0l171.905,171.915l171.906-171.909c12.359-12.354,32.391-12.354,44.744,0c12.365,12.354,12.365,32.392,0,44.751L248.292,345.449C242.115,351.621,234.018,354.706,225.923,354.706z"/>
14
- </svg>
15
- </button>
16
- <div class="header-title-content">
17
29
  <slot name="title">
18
- <h4 class="header-title">{{ title }}</h4>
19
- </slot>
20
- <slot name="subtitle">
21
- <div v-if="subtitle" class="header-subtitle">{{ subtitle }}</div>
30
+ <h4 class="notPadding" style="margin-bottom: 4px">
31
+ <span class="topmenu-title">{{ title }}</span>
32
+ <svg
33
+ v-if="sortedItems.length"
34
+ class="arrow"
35
+ :class="{ open: isDropdownOpen }"
36
+ width="12"
37
+ height="12"
38
+ viewBox="0 0 451.847 451.847"
39
+ style="flex-shrink: 0"
40
+ >
41
+ <path
42
+ fill="currentColor"
43
+ d="M225.923,354.706c-8.098,0-16.195-3.092-22.369-9.263L9.27,151.157c-12.359-12.359-12.359-32.397,0-44.751c12.354-12.354,32.388-12.354,44.748,0l171.905,171.915l171.906-171.909c12.359-12.354,32.391-12.354,44.744,0c12.365,12.354,12.365,32.392,0,44.751L248.292,345.449C242.115,351.621,234.018,354.706,225.923,354.706z"
44
+ />
45
+ </svg>
46
+ </h4>
22
47
  </slot>
48
+ </button>
49
+ <div v-if="isDropdownOpen && sortedItems.length" class="title-dropdown">
50
+ <div class="title-dropdown-header">{{ dropdownTitle }}</div>
51
+ <div class="title-dropdown-divider"></div>
52
+ <div
53
+ v-for="(item, index) in sortedItems"
54
+ :key="index"
55
+ class="title-dropdown-item"
56
+ @click="selectItem(item)"
57
+ >
58
+ <p class="pageName">{{ capitalize(item.name) }}</p>
59
+ <small class="pageVisit">
60
+ ({{ visitLabel }} {{ getTimeAgo(item.lastVisit) }})
61
+ </small>
62
+ </div>
23
63
  </div>
64
+ <slot name="subtitle">
65
+ <p v-if="subtitle" class="topmenu-description">{{ subtitle }}</p>
66
+ </slot>
24
67
  </div>
68
+ </div>
25
69
 
26
- <div class="header-actions">
27
- <slot></slot>
28
- </div>
70
+ <div class="topmenubox-button">
71
+ <!-- Порожні блоки ремонтують відображення на windows в додатку, не видаляти! -->
72
+ <div></div>
73
+ <slot></slot>
74
+ <div></div>
29
75
  </div>
30
76
  </div>
31
77
  </header>
32
78
  </template>
33
79
 
34
80
  <script setup>
35
- import { computed } from 'vue'
81
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
82
+ import { isInIframe } from '../../shared/utils/webviewCheck'
36
83
 
37
84
  const props = defineProps({
38
85
  title: {
39
86
  type: String,
40
- default: ''
87
+ default: "",
41
88
  },
42
89
  subtitle: {
43
90
  type: String,
44
- default: ''
91
+ default: "",
45
92
  },
46
93
  showBackButton: {
47
94
  type: Boolean,
48
- default: true
95
+ default: true,
49
96
  },
50
97
  backButtonTitle: {
51
98
  type: String,
52
- default: 'Назад'
99
+ default: "Назад",
53
100
  },
54
101
  backEvent: {
55
102
  type: Function,
56
- default: null
57
- }
103
+ default: null,
104
+ },
105
+ dropdownItems: {
106
+ type: Array,
107
+ default: () => [],
108
+ },
109
+ dropdownTitle: {
110
+ type: String,
111
+ default: "Останні відвідані розділи",
112
+ },
113
+ visitLabel: {
114
+ type: String,
115
+ default: "Останнє відвідування",
116
+ },
117
+ });
118
+
119
+ const emit = defineEmits(['back', 'navigate'])
120
+
121
+ const dropdownRef = ref(null)
122
+ const isDropdownOpen = ref(false)
123
+
124
+ const sortedItems = computed(() => {
125
+ return [...props.dropdownItems].sort((a, b) => b.lastVisit - a.lastVisit)
58
126
  })
59
127
 
60
- // Перевіряємо чи сторінка в iframe
61
- const isInIframe = computed(() => {
62
- try {
63
- return window.self !== window.top
64
- } catch (e) {
65
- return true
128
+ const toggleDropdown = () => {
129
+ if (sortedItems.value.length) {
130
+ isDropdownOpen.value = !isDropdownOpen.value
66
131
  }
132
+ }
133
+
134
+ const closeDropdown = () => {
135
+ isDropdownOpen.value = false
136
+ }
137
+
138
+ const selectItem = (item) => {
139
+ emit('navigate', item.path)
140
+ closeDropdown()
141
+ }
142
+
143
+ const capitalize = (str) => {
144
+ if (!str) return ''
145
+ return str.charAt(0).toUpperCase() + str.slice(1)
146
+ }
147
+
148
+ const getTimeAgo = (lastVisit) => {
149
+ const now = Date.now()
150
+ const diff = now - lastVisit
151
+
152
+ const seconds = Math.floor(diff / 1000)
153
+ const minutes = Math.floor(seconds / 60)
154
+ const hours = Math.floor(minutes / 60)
155
+ const days = Math.floor(hours / 24)
156
+
157
+ if (days > 0) return `${days}д тому`
158
+ if (hours > 0) return `${hours}год тому`
159
+ if (minutes > 0) return `${minutes}хв тому`
160
+ return `${seconds}с тому`
161
+ }
162
+
163
+ const handleClickOutside = (e) => {
164
+ if (dropdownRef.value && !dropdownRef.value.contains(e.target)) {
165
+ closeDropdown()
166
+ }
167
+ }
168
+
169
+ onMounted(() => {
170
+ document.addEventListener('click', handleClickOutside, true)
171
+ })
172
+
173
+ onUnmounted(() => {
174
+ document.removeEventListener('click', handleClickOutside, true)
67
175
  })
68
176
 
69
177
  // Показуємо кнопку якщо є backEvent АБО (showBackButton=true І сторінка в iframe)
70
178
  const shouldShowBackButton = computed(() => {
71
- return props.backEvent || (props.showBackButton && isInIframe.value)
72
- })
179
+ return props.backEvent || (props.showBackButton && isInIframe());
180
+ });
73
181
 
74
- // Обробник кнопки "Назад" - викликає backEvent або відправляє повідомлення батьківському вікну
75
182
  const handleBack = () => {
76
- if (props.backEvent) {
77
- props.backEvent()
78
- } else {
79
- window.parent.postMessage({ type: 'exit' }, '*')
80
- }
183
+ if (props.backEvent) return props.backEvent()
184
+
185
+ window.parent.postMessage({ type: 'exit' }, '*')
81
186
  }
82
187
  </script>
83
188
 
84
189
  <style scoped>
85
- .sky-header {
86
- position: sticky;
87
- top: 0;
88
- left: 0;
89
- right: 0;
90
- background: var(--sky-header-bg, white);
190
+ .sky-header-container {
191
+ width: 100%;
192
+ min-height: var(--sky-header-min-height, 82px);
193
+ background-color: var(--sky-header-bg, transparent);
194
+ display: flex;
195
+ flex-direction: row;
196
+ padding: var(--sky-header-padding, 10px);
91
197
  border-bottom: 1px solid var(--sky-header-border-color, #dee2e6);
92
- z-index: var(--sky-header-z-index, 100);
93
- padding: var(--sky-header-padding, 10px 0);
198
+ z-index: var(--sky-header-z-index, 4);
199
+ position: relative;
94
200
  }
95
201
 
96
- .header-content {
97
- padding: var(--sky-header-content-padding, 4px 14px);
98
- margin: 0 auto;
99
- }
100
-
101
- .header-top {
202
+ .topmenubox {
203
+ width: 100%;
102
204
  display: flex;
205
+ padding: 4px;
103
206
  justify-content: space-between;
104
207
  align-items: center;
208
+ background-color: transparent;
105
209
  }
106
210
 
107
- .header-title-wrapper {
211
+ .header-left {
108
212
  display: flex;
109
213
  align-items: center;
110
214
  gap: 12px;
111
215
  }
112
216
 
113
- .header-title-content {
217
+ .titleAndDesc {
114
218
  display: flex;
115
219
  flex-direction: column;
220
+ position: relative;
221
+ }
222
+
223
+ .notPadding {
224
+ margin: 0;
225
+ padding: 0;
226
+ font-size: var(--sky-header-title-size, 18px);
227
+ font-weight: var(--sky-header-title-weight, 500);
228
+ color: var(--sky-header-title-color, #252525);
229
+ line-height: 1.5;
230
+ user-select: none;
231
+ display: flex;
232
+ align-items: center;
233
+ flex-wrap: nowrap;
234
+ }
235
+
236
+ .topmenu-title {
237
+ white-space: pre-line;
238
+ }
239
+
240
+ .title-dropdown-toggle {
241
+ display: flex;
242
+ flex-direction: row;
243
+ padding: 0;
244
+ margin: 0;
245
+ background: transparent;
246
+ border: none;
247
+ text-align: left;
248
+ font: inherit;
249
+ color: inherit;
250
+ }
251
+
252
+ .title-dropdown-toggle-active {
253
+ cursor: pointer;
254
+ }
255
+
256
+ .arrow {
257
+ width: 12px;
258
+ position: relative;
259
+ margin-left: 5px;
260
+ flex-shrink: 0;
261
+ transition: transform 0.25s ease;
262
+ color: var(--sky-header-title-color, #252525);
263
+ }
264
+
265
+ .arrow.open {
266
+ transform: rotate(180deg);
267
+ }
268
+
269
+ .title-dropdown {
270
+ position: absolute;
271
+ top: 100%;
272
+ left: 0;
273
+ min-width: 240px;
274
+ background: white;
275
+ border-radius: 5px;
276
+ box-shadow: 0 1px 12px rgba(0, 0, 0, 0.1);
277
+ border: none;
278
+ z-index: 10;
279
+ padding: 4px 0;
280
+ margin-top: 4px;
281
+ }
282
+
283
+ .title-dropdown-header {
284
+ padding: 4px 24px;
285
+ font-size: 13px;
286
+ color: #6c757d;
287
+ }
288
+
289
+ .title-dropdown-divider {
290
+ height: 0;
291
+ margin: 4px 0;
292
+ border-top: 1px solid #e9ecef;
293
+ }
294
+
295
+ .title-dropdown-item {
296
+ padding: 4px 24px;
297
+ cursor: pointer;
298
+ transition: background-color 0.1s;
299
+ }
300
+
301
+ .title-dropdown-item:hover {
302
+ background-color: #f8f9fa;
303
+ }
304
+
305
+ .title-dropdown-item:active {
306
+ background-color: #e9ecef;
307
+ }
308
+
309
+ .pageName {
310
+ padding-bottom: 0;
311
+ margin: 0;
312
+ font-weight: 500;
313
+ font-size: 14px;
314
+ }
315
+
316
+ .pageVisit {
317
+ color: gray;
318
+ font-weight: 400;
319
+ font-size: 11px;
320
+ }
321
+
322
+ .topmenu-description {
323
+ margin: 0;
324
+ margin-bottom: 5px;
325
+ color: var(--sky-header-subtitle-color, #5d5d5d);
326
+ font-size: 13px;
327
+ font-weight: 400;
328
+ line-height: 1.5;
329
+ }
330
+
331
+ .topmenubox-button {
332
+ display: flex;
333
+ flex-direction: row;
334
+ align-items: center;
335
+ justify-content: space-between;
336
+ gap: var(--sky-header-actions-gap, 12px);
116
337
  }
117
338
 
118
339
  .btn-back {
@@ -143,41 +364,27 @@ const handleBack = () => {
143
364
  background-color: var(--sky-header-back-btn-active-bg, #e9ecef);
144
365
  }
145
366
 
146
- .header-title {
147
- margin: 0;
148
- font-size: var(--sky-header-title-size, 18px);
149
- font-weight: var(--sky-header-title-weight, 500);
150
- color: var(--sky-header-title-color, #252525);
151
- line-height: 1.5;
152
- user-select: none;
153
- }
154
-
155
- .header-subtitle {
156
- font-size: var(--sky-header-subtitle-size, 14px);
157
- color: var(--sky-header-subtitle-color, #6c757d);
158
- font-weight: 400;
159
- line-height: 1.5;
160
- }
161
-
162
- .header-actions {
163
- display: flex;
164
- gap: var(--sky-header-actions-gap, 12px);
367
+ /* Responsive: <500px — hide description, smaller title */
368
+ @media (max-width: 499px) {
369
+ .topmenu-description {
370
+ display: none;
371
+ }
372
+ .notPadding {
373
+ font-size: 13px;
374
+ }
165
375
  }
166
376
 
167
- @media (max-width: 768px) {
168
- /* .header-content {
169
- padding: 12px 16px;
170
- } */
171
-
172
- .header-top {
173
- flex-direction: column;
174
- align-items: flex-start;
175
- /* gap: 12px; */
377
+ /* Responsive: 500-1099px — smaller description */
378
+ @media (min-width: 500px) and (max-width: 1099px) {
379
+ .topmenu-description {
380
+ font-size: 11px;
176
381
  }
382
+ }
177
383
 
178
- .header-actions {
179
- width: 100%;
180
- justify-content: flex-end;
384
+ /* iOS safe area */
385
+ @supports (padding-top: env(safe-area-inset-top)) {
386
+ .sky-header-container {
387
+ padding-top: calc(10px + env(safe-area-inset-top));
181
388
  }
182
389
  }
183
390
  </style>
@@ -1,17 +1,34 @@
1
1
  <template>
2
2
  <BaseTeleport to="body">
3
3
  <transition name="modal-fade">
4
- <div v-if="modelValue" class="sky-modal-overlay" @click.self="handleOverlayClick">
4
+ <div
5
+ v-if="modelValue"
6
+ class="sky-modal-overlay"
7
+ @click.self="handleOverlayClick"
8
+ >
5
9
  <div class="sky-modal" :style="modalStyle">
6
10
  <div class="sky-modal-header">
7
11
  <button class="sky-modal-back" @click="close" :title="closeTitle">
8
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
9
- <path d="M19 12H5M12 19l-7-7 7-7" stroke-linecap="round" stroke-linejoin="round"/>
12
+ <svg
13
+ width="15"
14
+ height="15"
15
+ viewBox="0 0 24 24"
16
+ fill="none"
17
+ stroke="currentColor"
18
+ stroke-width="2"
19
+ >
20
+ <path
21
+ d="M19 12H5M12 19l-7-7 7-7"
22
+ stroke-linecap="round"
23
+ stroke-linejoin="round"
24
+ />
10
25
  </svg>
11
26
  </button>
12
27
  <div class="sky-modal-title-wrapper">
13
28
  <h4 class="sky-modal-title">{{ title }}</h4>
14
- <div v-if="subtitle" class="sky-modal-subtitle">{{ subtitle }}</div>
29
+ <div v-if="subtitle" class="sky-modal-subtitle">
30
+ {{ subtitle }}
31
+ </div>
15
32
  </div>
16
33
  </div>
17
34
 
@@ -20,7 +37,10 @@
20
37
  </div>
21
38
 
22
39
  <div v-if="$slots.footer" class="sky-modal-footer">
40
+ <!-- Порожні блоки ремонтують відображення на windows в додатку, не видаляти! -->
41
+ <div></div>
23
42
  <slot name="footer"></slot>
43
+ <div></div>
24
44
  </div>
25
45
  </div>
26
46
  </div>
@@ -29,85 +49,87 @@
29
49
  </template>
30
50
 
31
51
  <script setup>
32
- import { computed, watch, onMounted, onUnmounted } from 'vue'
33
- import BaseTeleport from './BaseTeleport.vue';
34
-
52
+ import { computed, watch, onMounted, onUnmounted } from "vue";
53
+ import BaseTeleport from "./BaseTeleport.vue";
35
54
 
36
55
  const props = defineProps({
37
56
  modelValue: {
38
57
  type: Boolean,
39
- default: false
58
+ default: false,
40
59
  },
41
60
  title: {
42
61
  type: String,
43
- default: ''
62
+ default: "",
44
63
  },
45
64
  subtitle: {
46
65
  type: String,
47
- default: ''
66
+ default: "",
48
67
  },
49
68
  closeTitle: {
50
69
  type: String,
51
- default: 'Закрити'
70
+ default: "Закрити",
52
71
  },
53
72
  closeOnOverlay: {
54
73
  type: Boolean,
55
- default: true
74
+ default: true,
56
75
  },
57
76
  closeOnEsc: {
58
77
  type: Boolean,
59
- default: true
78
+ default: true,
60
79
  },
61
80
  width: {
62
81
  type: String,
63
- default: '100%'
82
+ default: "100%",
64
83
  },
65
84
  height: {
66
85
  type: String,
67
- default: '100%'
68
- }
69
- })
86
+ default: "100%",
87
+ },
88
+ });
70
89
 
71
- const emit = defineEmits(['update:modelValue', 'close'])
90
+ const emit = defineEmits(["update:modelValue", "close"]);
72
91
 
73
92
  const modalStyle = computed(() => ({
74
93
  width: props.width,
75
- height: props.height
76
- }))
94
+ height: props.height,
95
+ }));
77
96
 
78
97
  const close = () => {
79
- emit('update:modelValue', false)
80
- emit('close')
81
- }
98
+ emit("update:modelValue", false);
99
+ emit("close");
100
+ };
82
101
 
83
102
  const handleOverlayClick = () => {
84
103
  if (props.closeOnOverlay) {
85
- close()
104
+ close();
86
105
  }
87
- }
106
+ };
88
107
 
89
108
  const handleKeydown = (e) => {
90
- if (e.key === 'Escape' && props.closeOnEsc && props.modelValue) {
91
- close()
109
+ if (e.key === "Escape" && props.closeOnEsc && props.modelValue) {
110
+ close();
92
111
  }
93
- }
112
+ };
94
113
 
95
- watch(() => props.modelValue, (value) => {
96
- if (value) {
97
- document.body.style.overflow = 'hidden'
98
- } else {
99
- document.body.style.overflow = ''
100
- }
101
- })
114
+ watch(
115
+ () => props.modelValue,
116
+ (value) => {
117
+ if (value) {
118
+ document.body.style.overflow = "hidden";
119
+ } else {
120
+ document.body.style.overflow = "";
121
+ }
122
+ },
123
+ );
102
124
 
103
125
  onMounted(() => {
104
- document.addEventListener('keydown', handleKeydown)
105
- })
126
+ document.addEventListener("keydown", handleKeydown);
127
+ });
106
128
 
107
129
  onUnmounted(() => {
108
- document.removeEventListener('keydown', handleKeydown)
109
- document.body.style.overflow = ''
110
- })
130
+ document.removeEventListener("keydown", handleKeydown);
131
+ document.body.style.overflow = "";
132
+ });
111
133
  </script>
112
134
 
113
135
  <style scoped>