@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyservice-developers/vue-dev-kit",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Vue 2 and Vue 3 developer toolkit - components and helpers",
5
5
  "type": "module",
6
6
  "main": "./dist/vue3/vue-dev-kit.cjs",
@@ -31,6 +31,7 @@
31
31
  ],
32
32
  "scripts": {
33
33
  "dev": "vite",
34
+ "playground": "vite --config playground/vite.config.js playground",
34
35
  "build:vue2": "vite build --config vite.vue2.config.js",
35
36
  "build:vue3": "vite build --config vite.vue3.config.js",
36
37
  "build": "npm run build:vue2 && npm run build:vue3",
@@ -2,11 +2,26 @@ import { DefineComponent } from 'vue'
2
2
 
3
3
  // ============ Components ============
4
4
 
5
+ export interface HeaderDropdownItem {
6
+ name: string
7
+ path: string
8
+ lastVisit: number
9
+ }
10
+
5
11
  export interface HeaderProps {
6
12
  title?: string
7
13
  subtitle?: string
8
14
  showBackButton?: boolean
9
15
  backButtonTitle?: string
16
+ backEvent?: (() => void) | null
17
+ dropdownItems?: HeaderDropdownItem[]
18
+ dropdownTitle?: string
19
+ visitLabel?: string
20
+ }
21
+
22
+ export interface HeaderEmits {
23
+ 'back': () => void
24
+ 'navigate': (path: string) => void
10
25
  }
11
26
 
12
27
  export interface HeaderSlots {
@@ -1 +1 @@
1
- export { webviewCheck, isIosWebview, isAndroidWebview, isCefWebview, isWebview } from './webviewCheck'
1
+ export { webviewCheck, isIosWebview, isAndroidWebview, isCefWebview, isWebview, isInIframe } from './webviewCheck'
@@ -44,3 +44,11 @@ export function isWebview() {
44
44
  const check = webviewCheck()
45
45
  return check !== 'browser'
46
46
  }
47
+
48
+ export function isInIframe() {
49
+ try {
50
+ return window.self !== window.top
51
+ } catch (e) {
52
+ return true
53
+ }
54
+ }
@@ -1,130 +1,324 @@
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
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
21
  <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>
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>
30
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>
31
55
  </div>
56
+ <slot name="subtitle">
57
+ <p v-if="subtitle" class="topmenu-description">{{ subtitle }}</p>
58
+ </slot>
32
59
  </div>
60
+ </div>
33
61
 
34
- <div class="header-actions">
35
- <div></div>
36
- <slot></slot>
37
- <div></div>
38
- </div>
62
+ <div class="topmenubox-button">
63
+ <!-- Порожні блоки ремонтують відображення на windows в додатку, не видаляти! -->
64
+ <div></div>
65
+ <slot></slot>
66
+ <div></div>
39
67
  </div>
40
68
  </div>
41
69
  </header>
42
70
  </template>
43
71
 
44
72
  <script>
73
+ import { isInIframe } from '../../shared/utils/webviewCheck'
74
+
45
75
  export default {
46
- name: "Header",
76
+ name: 'Header',
47
77
  props: {
48
78
  title: {
49
79
  type: String,
50
- default: "",
80
+ default: ''
51
81
  },
52
82
  subtitle: {
53
83
  type: String,
54
- default: "",
84
+ default: ''
55
85
  },
56
86
  showBackButton: {
57
87
  type: Boolean,
58
- default: true,
88
+ default: true
59
89
  },
60
90
  backButtonTitle: {
61
91
  type: String,
62
- default: "Назад",
92
+ default: 'Назад'
63
93
  },
64
94
  backEvent: {
65
95
  type: Function,
66
- default: null,
96
+ default: null
97
+ },
98
+ dropdownItems: {
99
+ type: Array,
100
+ default: () => []
101
+ },
102
+ dropdownTitle: {
103
+ type: String,
104
+ default: 'Останні відвідані розділи'
67
105
  },
106
+ visitLabel: {
107
+ type: String,
108
+ default: 'Останнє відвідування'
109
+ }
110
+ },
111
+ data() {
112
+ return {
113
+ isDropdownOpen: false
114
+ }
68
115
  },
69
116
  computed: {
70
- // Перевіряємо чи сторінка в iframe
71
- isInIframe() {
72
- try {
73
- return window.self !== window.top;
74
- } catch (e) {
75
- return true;
76
- }
117
+ sortedItems() {
118
+ return [...this.dropdownItems].sort((a, b) => b.lastVisit - a.lastVisit)
77
119
  },
78
- // Показуємо кнопку якщо є backEvent АБО (showBackButton=true І сторінка в iframe)
79
120
  shouldShowBackButton() {
80
- return this.backEvent || (this.showBackButton && this.isInIframe);
81
- },
121
+ return this.backEvent || (this.showBackButton && isInIframe())
122
+ }
123
+ },
124
+ mounted() {
125
+ document.addEventListener('click', this.handleClickOutside, true)
126
+ },
127
+ beforeDestroy() {
128
+ document.removeEventListener('click', this.handleClickOutside, true)
82
129
  },
83
130
  methods: {
84
- // Обробник кнопки "Назад" - викликає backEvent або відправляє повідомлення батьківському вікну
85
131
  handleBack() {
86
- if (this.backEvent) {
87
- this.backEvent();
88
- } else {
89
- 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
90
139
  }
91
140
  },
92
- },
93
- };
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()
169
+ }
170
+ }
171
+ }
172
+ }
94
173
  </script>
95
174
 
96
175
  <style scoped>
97
- .sky-header {
98
- position: sticky;
99
- top: 0;
100
- left: 0;
101
- right: 0;
102
- 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);
103
183
  border-bottom: 1px solid var(--sky-header-border-color, #dee2e6);
104
- z-index: var(--sky-header-z-index, 100);
105
- padding: var(--sky-header-padding, 10px 0);
184
+ z-index: var(--sky-header-z-index, 4);
185
+ position: relative;
106
186
  }
107
187
 
108
- .header-content {
109
- padding: var(--sky-header-content-padding, 4px 14px);
110
- margin: 0 auto;
111
- }
112
-
113
- .header-top {
188
+ .topmenubox {
189
+ width: 100%;
114
190
  display: flex;
191
+ padding: 4px;
115
192
  justify-content: space-between;
116
193
  align-items: center;
194
+ background-color: transparent;
117
195
  }
118
196
 
119
- .header-title-wrapper {
197
+ .header-left {
120
198
  display: flex;
121
199
  align-items: center;
122
200
  gap: 12px;
123
201
  }
124
202
 
125
- .header-title-content {
203
+ .titleAndDesc {
126
204
  display: flex;
127
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;
128
322
  }
129
323
 
130
324
  .btn-back {
@@ -155,41 +349,27 @@ export default {
155
349
  background-color: var(--sky-header-back-btn-active-bg, #e9ecef);
156
350
  }
157
351
 
158
- .header-title {
159
- margin: 0;
160
- font-size: var(--sky-header-title-size, 18px);
161
- font-weight: var(--sky-header-title-weight, 500);
162
- color: var(--sky-header-title-color, #252525);
163
- line-height: 1.5;
164
- user-select: none;
165
- }
166
-
167
- .header-subtitle {
168
- font-size: var(--sky-header-subtitle-size, 14px);
169
- color: var(--sky-header-subtitle-color, #6c757d);
170
- font-weight: 400;
171
- line-height: 1.5;
172
- }
173
-
174
- .header-actions {
175
- display: flex;
176
- 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
+ }
177
360
  }
178
361
 
179
- @media (max-width: 768px) {
180
- /* .header-content {
181
- padding: 12px 16px;
182
- } */
183
-
184
- .header-top {
185
- flex-direction: column;
186
- align-items: flex-start;
187
- /* gap: 12px; */
362
+ /* Responsive: 500-1099px — smaller description */
363
+ @media (min-width: 500px) and (max-width: 1099px) {
364
+ .topmenu-description {
365
+ font-size: 11px;
188
366
  }
367
+ }
189
368
 
190
- .header-actions {
191
- width: 100%;
192
- 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));
193
373
  }
194
374
  }
195
375
  </style>
@@ -50,8 +50,8 @@
50
50
  <div v-if="isIos" class="sky-dialog-swipe-area" />
51
51
  <slot></slot>
52
52
  </div>
53
+
53
54
  <!-- Footer -->
54
- <div></div>
55
55
  <div
56
56
  v-if="showFooter"
57
57
  class="sky-dialog-footer"