@skyservice-developers/vue-dev-kit 1.1.2 → 1.2.1

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.
@@ -47,7 +47,7 @@ const props = defineProps({
47
47
  },
48
48
  enableAnimation: {
49
49
  type: Boolean,
50
- default: true
50
+ default: false
51
51
  },
52
52
  closeOnEsc: {
53
53
  type: Boolean,
@@ -116,7 +116,7 @@ const props = defineProps({
116
116
  },
117
117
  enableAnimation: {
118
118
  type: Boolean,
119
- default: true,
119
+ default: false,
120
120
  },
121
121
  closeOnEsc: {
122
122
  type: Boolean,
@@ -12,32 +12,31 @@
12
12
  >
13
13
  <div ref="dialogContent" class="sky-dialog-content">
14
14
  <!-- Header -->
15
- <button class="sky-dialog-back" :title="closeText" @click="close">
16
- <svg
17
- width="15"
18
- height="15"
19
- viewBox="0 0 451.847 451.847"
20
- style="transform: rotate(90deg)"
15
+ <div class="sky-dialog-header">
16
+ <button class="sky-dialog-back" :title="closeText" @click="close">
17
+ <svg
18
+ width="15"
19
+ height="15"
20
+ viewBox="0 0 451.847 451.847"
21
+ style="transform: rotate(90deg)"
22
+ >
23
+ <path
24
+ fill="currentColor"
25
+ 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"
26
+ />
27
+ </svg>
28
+ </button>
29
+ <div
30
+ class="sky-dialog-title"
31
+ :class="{ 'sky-dialog-title-with-subtitle': subtitle }"
21
32
  >
22
- <path
23
- fill="currentColor"
24
- 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"
25
- />
26
- </svg>
27
- </button>
28
-
29
- <div
30
- class="sky-dialog-title"
31
- :class="{ 'sky-dialog-title-with-subtitle': subtitle }"
32
- >
33
- {{ title }}
34
- <span v-if="subtitle" class="sky-dialog-subtitle">{{
35
- subtitle
36
- }}</span>
33
+ {{ title }}
34
+ <span v-if="subtitle" class="sky-dialog-subtitle">{{
35
+ subtitle
36
+ }}</span>
37
+ </div>
37
38
  </div>
38
39
 
39
- <div class="sky-dialog-clearfix" />
40
-
41
40
  <!-- Body -->
42
41
  <div
43
42
  ref="dialogPaper"
@@ -50,8 +49,8 @@
50
49
  <div v-if="isIos" class="sky-dialog-swipe-area" />
51
50
  <slot></slot>
52
51
  </div>
52
+
53
53
  <!-- Footer -->
54
- <div></div>
55
54
  <div
56
55
  v-if="showFooter"
57
56
  class="sky-dialog-footer"
@@ -109,7 +108,7 @@ const props = defineProps({
109
108
  },
110
109
  enableAnimation: {
111
110
  type: Boolean,
112
- default: true,
111
+ default: false,
113
112
  },
114
113
  closeOnEsc: {
115
114
  type: Boolean,
@@ -265,37 +264,52 @@ onUnmounted(() => {
265
264
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.24);
266
265
  }
267
266
 
267
+ .sky-dialog-header {
268
+ display: flex;
269
+ align-items: center;
270
+ padding: 10px 14px;
271
+ }
272
+
268
273
  .sky-dialog-back {
269
- cursor: pointer;
270
- margin: 10px;
271
- padding: 14px 12px 6px;
272
- float: left;
273
- line-height: 1;
274
+ display: flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ width: 32px;
278
+ height: 32px;
279
+ padding: 0;
274
280
  background: transparent;
275
281
  border: none;
282
+ cursor: pointer;
276
283
  border-radius: 6px;
277
284
  color: var(--sky-dialog-back-color, #374151);
278
285
  transition: background-color 0.2s;
286
+ margin-right: 12px;
287
+ flex-shrink: 0;
288
+ }
289
+
290
+ .sky-dialog-back svg {
291
+ display: block;
279
292
  }
280
293
 
281
294
  .sky-dialog-back:hover {
282
295
  background-color: var(--sky-dialog-back-hover-bg, #f8f9fa);
283
296
  }
284
297
 
298
+ .sky-dialog-back:active {
299
+ background-color: var(--sky-dialog-back-active-bg, #e9ecef);
300
+ }
301
+
285
302
  .sky-dialog-title {
286
- max-width: calc(100% - 80px);
287
303
  font-size: var(--sky-dialog-title-size, 13pt);
288
- padding: 21px 0;
289
- padding-right: 0;
290
- float: left;
291
304
  white-space: nowrap;
292
305
  overflow: hidden;
293
306
  text-overflow: ellipsis;
294
307
  color: var(--sky-dialog-title-color, #252525);
308
+ min-width: 0;
295
309
  }
296
310
 
297
311
  .sky-dialog-title-with-subtitle {
298
- padding: 13px 0;
312
+ line-height: 1.2;
299
313
  }
300
314
 
301
315
  .sky-dialog-subtitle {
@@ -308,9 +322,6 @@ onUnmounted(() => {
308
322
  text-overflow: ellipsis;
309
323
  }
310
324
 
311
- .sky-dialog-clearfix {
312
- clear: both;
313
- }
314
325
 
315
326
  .sky-dialog-paper {
316
327
  height: 100%;
@@ -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>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <BaseTeleport to="body">
3
- <transition name="modal-fade">
3
+ <transition>
4
4
  <div
5
5
  v-if="modelValue"
6
6
  class="sky-modal-overlay"
@@ -12,15 +12,12 @@
12
12
  <svg
13
13
  width="15"
14
14
  height="15"
15
- viewBox="0 0 24 24"
16
- fill="none"
17
- stroke="currentColor"
18
- stroke-width="2"
15
+ viewBox="0 0 451.847 451.847"
16
+ style="transform: rotate(90deg)"
19
17
  >
20
18
  <path
21
- d="M19 12H5M12 19l-7-7 7-7"
22
- stroke-linecap="round"
23
- stroke-linejoin="round"
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"
24
21
  />
25
22
  </svg>
26
23
  </button>
@@ -169,8 +166,8 @@ onUnmounted(() => {
169
166
  display: flex;
170
167
  align-items: center;
171
168
  justify-content: center;
172
- width: 40px;
173
- height: 40px;
169
+ width: 32px;
170
+ height: 32px;
174
171
  padding: 0;
175
172
  background: transparent;
176
173
  border: none;
@@ -181,10 +178,18 @@ onUnmounted(() => {
181
178
  margin-right: 12px;
182
179
  }
183
180
 
181
+ .sky-modal-back svg {
182
+ display: block;
183
+ }
184
+
184
185
  .sky-modal-back:hover {
185
186
  background-color: var(--sky-modal-back-hover-bg, #f8f9fa);
186
187
  }
187
188
 
189
+ .sky-modal-back:active {
190
+ background-color: var(--sky-modal-back-active-bg, #e9ecef);
191
+ }
192
+
188
193
  .sky-modal-title-wrapper {
189
194
  flex: 1;
190
195
  min-width: 0;