@skyservice-developers/vue-dev-kit 1.1.2 → 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,49 +1,85 @@
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
13
- width="15"
14
- height="15"
15
- viewBox="0 0 451.847 451.847"
16
- style="transform: rotate(90deg)"
17
- >
18
- <path
19
- fill="currentColor"
20
- 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"
21
- />
22
- </svg>
23
- </button>
24
- <div class="header-title-content">
25
29
  <slot name="title">
26
- <h4 class="header-title">{{ title }}</h4>
27
- </slot>
28
- <slot name="subtitle">
29
- <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>
30
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>
31
63
  </div>
64
+ <slot name="subtitle">
65
+ <p v-if="subtitle" class="topmenu-description">{{ subtitle }}</p>
66
+ </slot>
32
67
  </div>
68
+ </div>
33
69
 
34
- <div class="header-actions">
35
- <!-- Порожні блоки ремонтують відображення на windows в додатку, не видаляти! -->
36
- <div></div>
37
- <slot></slot>
38
- <div></div>
39
- </div>
70
+ <div class="topmenubox-button">
71
+ <!-- Порожні блоки ремонтують відображення на windows в додатку, не видаляти! -->
72
+ <div></div>
73
+ <slot></slot>
74
+ <div></div>
40
75
  </div>
41
76
  </div>
42
77
  </header>
43
78
  </template>
44
79
 
45
80
  <script setup>
46
- import { computed } from "vue";
81
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
82
+ import { isInIframe } from '../../shared/utils/webviewCheck'
47
83
 
48
84
  const props = defineProps({
49
85
  title: {
@@ -66,64 +102,238 @@ const props = defineProps({
66
102
  type: Function,
67
103
  default: null,
68
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
+ },
69
117
  });
70
118
 
71
- // Перевіряємо чи сторінка в iframe
72
- const isInIframe = computed(() => {
73
- try {
74
- return window.self !== window.top;
75
- } catch (e) {
76
- return true;
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)
126
+ })
127
+
128
+ const toggleDropdown = () => {
129
+ if (sortedItems.value.length) {
130
+ isDropdownOpen.value = !isDropdownOpen.value
77
131
  }
78
- });
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)
175
+ })
79
176
 
80
177
  // Показуємо кнопку якщо є backEvent АБО (showBackButton=true І сторінка в iframe)
81
178
  const shouldShowBackButton = computed(() => {
82
- return props.backEvent || (props.showBackButton && isInIframe.value);
179
+ return props.backEvent || (props.showBackButton && isInIframe());
83
180
  });
84
181
 
85
- // Обробник кнопки "Назад" - викликає backEvent або відправляє повідомлення батьківському вікну
86
182
  const handleBack = () => {
87
- if (props.backEvent) {
88
- props.backEvent();
89
- } else {
90
- window.parent.postMessage({ type: "exit" }, "*");
91
- }
92
- };
183
+ if (props.backEvent) return props.backEvent()
184
+
185
+ window.parent.postMessage({ type: 'exit' }, '*')
186
+ }
93
187
  </script>
94
188
 
95
189
  <style scoped>
96
- .sky-header {
97
- position: sticky;
98
- top: 0;
99
- left: 0;
100
- right: 0;
101
- 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);
102
197
  border-bottom: 1px solid var(--sky-header-border-color, #dee2e6);
103
- z-index: var(--sky-header-z-index, 100);
104
- padding: var(--sky-header-padding, 10px 0);
198
+ z-index: var(--sky-header-z-index, 4);
199
+ position: relative;
105
200
  }
106
201
 
107
- .header-content {
108
- padding: var(--sky-header-content-padding, 4px 14px);
109
- margin: 0 auto;
110
- }
111
-
112
- .header-top {
202
+ .topmenubox {
203
+ width: 100%;
113
204
  display: flex;
205
+ padding: 4px;
114
206
  justify-content: space-between;
115
207
  align-items: center;
208
+ background-color: transparent;
116
209
  }
117
210
 
118
- .header-title-wrapper {
211
+ .header-left {
119
212
  display: flex;
120
213
  align-items: center;
121
214
  gap: 12px;
122
215
  }
123
216
 
124
- .header-title-content {
217
+ .titleAndDesc {
125
218
  display: flex;
126
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);
127
337
  }
128
338
 
129
339
  .btn-back {
@@ -154,41 +364,27 @@ const handleBack = () => {
154
364
  background-color: var(--sky-header-back-btn-active-bg, #e9ecef);
155
365
  }
156
366
 
157
- .header-title {
158
- margin: 0;
159
- font-size: var(--sky-header-title-size, 18px);
160
- font-weight: var(--sky-header-title-weight, 500);
161
- color: var(--sky-header-title-color, #252525);
162
- line-height: 1.5;
163
- user-select: none;
164
- }
165
-
166
- .header-subtitle {
167
- font-size: var(--sky-header-subtitle-size, 14px);
168
- color: var(--sky-header-subtitle-color, #6c757d);
169
- font-weight: 400;
170
- line-height: 1.5;
171
- }
172
-
173
- .header-actions {
174
- display: flex;
175
- 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
+ }
176
375
  }
177
376
 
178
- @media (max-width: 768px) {
179
- /* .header-content {
180
- padding: 12px 16px;
181
- } */
182
-
183
- .header-top {
184
- flex-direction: column;
185
- align-items: flex-start;
186
- /* gap: 12px; */
377
+ /* Responsive: 500-1099px — smaller description */
378
+ @media (min-width: 500px) and (max-width: 1099px) {
379
+ .topmenu-description {
380
+ font-size: 11px;
187
381
  }
382
+ }
188
383
 
189
- .header-actions {
190
- width: 100%;
191
- 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));
192
388
  }
193
389
  }
194
390
  </style>