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