@skyservice-developers/vue-dev-kit 1.0.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.
@@ -0,0 +1,401 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <transition :name="enableAnimation ? 'dialog-slide' : ''">
4
+ <div
5
+ v-if="modelValue"
6
+ class="sky-dialogbox sky-dialogbox-next"
7
+ :style="[zIndex ? { 'z-index': zIndex } : null]"
8
+ >
9
+ <div class="sky-dialog-overlay" :class="{ 'sky-dialog-animate': enableAnimation }">
10
+ <div ref="dialogContent" class="sky-dialog-content">
11
+ <!-- Header -->
12
+ <button class="sky-dialog-back" :title="closeText" @click="close">
13
+ <svg width="15" height="15" viewBox="0 0 451.847 451.847" style="transform: rotate(90deg)">
14
+ <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"/>
15
+ </svg>
16
+ </button>
17
+
18
+ <div class="sky-dialog-title" :class="{ 'sky-dialog-title-with-subtitle': subtitle }">
19
+ {{ title }}
20
+ <span v-if="subtitle" class="sky-dialog-subtitle">{{ subtitle }}</span>
21
+ </div>
22
+
23
+ <div class="sky-dialog-clearfix" />
24
+
25
+ <!-- Body -->
26
+ <div
27
+ ref="dialogPaper"
28
+ class="sky-dialog-paper"
29
+ @touchstart="handleTouchStart"
30
+ @touchend="handleTouchEnd"
31
+ >
32
+ <!-- iOS swipe back area -->
33
+ <div v-if="isIos" class="sky-dialog-swipe-area" />
34
+ <slot></slot>
35
+ </div>
36
+
37
+ <!-- Footer -->
38
+ <div v-if="$slots.buttons" class="sky-dialog-footer" :class="{ 'sky-dialog-footer-animate': enableAnimation }">
39
+ <slot name="buttons"></slot>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </transition>
45
+ </Teleport>
46
+ </template>
47
+
48
+ <script setup>
49
+ import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
50
+ import { isIosWebview, isAndroidWebview } from '../utils/webviewCheck'
51
+
52
+ const props = defineProps({
53
+ modelValue: {
54
+ type: Boolean,
55
+ default: false
56
+ },
57
+ title: {
58
+ type: String,
59
+ default: ''
60
+ },
61
+ subtitle: {
62
+ type: String,
63
+ default: ''
64
+ },
65
+ zIndex: {
66
+ type: [Number, String],
67
+ default: null
68
+ },
69
+ closeText: {
70
+ type: String,
71
+ default: 'Назад'
72
+ },
73
+ enableAnimation: {
74
+ type: Boolean,
75
+ default: true
76
+ },
77
+ closeOnEsc: {
78
+ type: Boolean,
79
+ default: true
80
+ }
81
+ })
82
+
83
+ const emit = defineEmits(['update:modelValue', 'close', 'save'])
84
+
85
+ const dialogContent = ref(null)
86
+ const dialogPaper = ref(null)
87
+ const touchStartX = ref(0)
88
+
89
+ const isIos = computed(() => {
90
+ try {
91
+ return isIosWebview()
92
+ } catch {
93
+ return false
94
+ }
95
+ })
96
+
97
+ const isAndroid = computed(() => {
98
+ try {
99
+ return isAndroidWebview()
100
+ } catch {
101
+ return false
102
+ }
103
+ })
104
+
105
+ const close = () => {
106
+ emit('update:modelValue', false)
107
+ emit('close')
108
+ }
109
+
110
+ const handleKeydown = (e) => {
111
+ if (e.key === 'Escape' && props.closeOnEsc && props.modelValue) {
112
+ close()
113
+ }
114
+ if (e.key === 'Enter' && props.modelValue) {
115
+ emit('save')
116
+ }
117
+ }
118
+
119
+ // Touch handling for iOS swipe back
120
+ const handleTouchStart = (e) => {
121
+ if (e.touches[0].clientX < 35) {
122
+ touchStartX.value = e.touches[0].clientX
123
+ }
124
+ }
125
+
126
+ const handleTouchEnd = (e) => {
127
+ if (touchStartX.value > 0 && touchStartX.value < 35) {
128
+ const touchEndX = e.changedTouches[0].clientX
129
+ if (touchEndX - touchStartX.value > 50) {
130
+ close()
131
+ }
132
+ }
133
+ touchStartX.value = 0
134
+ }
135
+
136
+ // Android notch fix
137
+ const androidFix = () => {
138
+ if (!isAndroid.value || !dialogContent.value) return
139
+
140
+ try {
141
+ if (typeof Android !== 'undefined' && Android.getDisplayCutoutTop) {
142
+ const cutoutTop = Android.getDisplayCutoutTop()
143
+ if (cutoutTop && window.devicePixelRatio > 1.0) {
144
+ const paddingTop = cutoutTop / window.devicePixelRatio
145
+ dialogContent.value.style.paddingTop = paddingTop + 'px'
146
+ }
147
+ }
148
+ } catch (err) {
149
+ // Android interface not available
150
+ }
151
+ }
152
+
153
+ // Body scroll lock
154
+ watch(() => props.modelValue, (value) => {
155
+ if (value) {
156
+ document.body.style.overflow = 'hidden'
157
+ nextTick(() => {
158
+ androidFix()
159
+ })
160
+ } else {
161
+ document.body.style.overflow = ''
162
+ }
163
+ })
164
+
165
+ onMounted(() => {
166
+ document.addEventListener('keydown', handleKeydown)
167
+ window.addEventListener('resize', androidFix)
168
+ })
169
+
170
+ onUnmounted(() => {
171
+ document.removeEventListener('keydown', handleKeydown)
172
+ window.removeEventListener('resize', androidFix)
173
+ document.body.style.overflow = ''
174
+ })
175
+ </script>
176
+
177
+ <style>
178
+ /* Global styles (не scoped через баг з Teleport) */
179
+ .sky-dialogbox-next {
180
+ display: block;
181
+ position: fixed;
182
+ padding: 0;
183
+ top: 0;
184
+ left: 0;
185
+ width: 100%;
186
+ height: 100%;
187
+ z-index: var(--sky-dialog-z-index, 9998);
188
+ background: rgba(0, 0, 0, 0.6);
189
+ backdrop-filter: blur(2px);
190
+ }
191
+ </style>
192
+
193
+ <style scoped>
194
+ .sky-dialog-overlay {
195
+ display: flex;
196
+ justify-content: center;
197
+ align-items: center;
198
+ position: fixed;
199
+ padding: 0;
200
+ top: 0;
201
+ left: 0;
202
+ width: 100%;
203
+ height: 100%;
204
+ z-index: 9999;
205
+ }
206
+
207
+ .sky-dialog-content {
208
+ background: var(--sky-dialog-bg, white);
209
+ width: 100%;
210
+ height: 100%;
211
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.24);
212
+ }
213
+
214
+ .sky-dialog-back {
215
+ cursor: pointer;
216
+ margin: 10px;
217
+ padding: 14px 12px 6px;
218
+ float: left;
219
+ line-height: 1;
220
+ background: transparent;
221
+ border: none;
222
+ border-radius: 6px;
223
+ color: var(--sky-dialog-back-color, #374151);
224
+ transition: background-color 0.2s;
225
+ }
226
+
227
+ .sky-dialog-back:hover {
228
+ background-color: var(--sky-dialog-back-hover-bg, #f8f9fa);
229
+ }
230
+
231
+ .sky-dialog-title {
232
+ max-width: calc(100% - 80px);
233
+ font-size: var(--sky-dialog-title-size, 13pt);
234
+ padding: 21px 0;
235
+ padding-right: 0;
236
+ float: left;
237
+ white-space: nowrap;
238
+ overflow: hidden;
239
+ text-overflow: ellipsis;
240
+ color: var(--sky-dialog-title-color, #252525);
241
+ }
242
+
243
+ .sky-dialog-title-with-subtitle {
244
+ padding: 13px 0;
245
+ }
246
+
247
+ .sky-dialog-subtitle {
248
+ display: block;
249
+ font-size: var(--sky-dialog-subtitle-size, 12pt);
250
+ line-height: 24px;
251
+ color: var(--sky-dialog-subtitle-color, #6c757d);
252
+ white-space: nowrap;
253
+ overflow: hidden;
254
+ text-overflow: ellipsis;
255
+ }
256
+
257
+ .sky-dialog-clearfix {
258
+ clear: both;
259
+ }
260
+
261
+ .sky-dialog-paper {
262
+ height: 100%;
263
+ overflow-y: auto;
264
+ overflow-x: hidden;
265
+ -webkit-overflow-scrolling: touch;
266
+ position: relative;
267
+ }
268
+
269
+ .sky-dialog-swipe-area {
270
+ position: absolute;
271
+ width: 35px;
272
+ height: 100%;
273
+ left: 0;
274
+ }
275
+
276
+ .sky-dialog-footer {
277
+ padding: 5px 10px;
278
+ display: flex;
279
+ justify-content: center;
280
+ width: 100%;
281
+ transform: translateY(-52px);
282
+ gap: 10px;
283
+ }
284
+
285
+ /* Кнопки в футері: 1 = 100%, 2 = по 50% */
286
+ .sky-dialog-footer > :deep(*) {
287
+ flex: 1;
288
+ min-width: 0;
289
+ }
290
+
291
+ .sky-dialog-footer:has(> :deep(*:only-child)) > :deep(*) {
292
+ max-width: 100%;
293
+ }
294
+
295
+ .sky-dialog-footer:has(> :deep(*:nth-child(2)):not(:has(> :deep(*:nth-child(3))))) > :deep(*) {
296
+ flex: 1 1 50%;
297
+ }
298
+
299
+ /* Desktop */
300
+ @media only screen and (min-width: 1400px) {
301
+ .sky-dialog-content {
302
+ width: 100%;
303
+ margin: 0 auto;
304
+ }
305
+ }
306
+
307
+ /* Tablet and Desktop */
308
+ @media screen and (min-width: 710px) {
309
+ .sky-dialog-paper {
310
+ height: calc(100% - 150px);
311
+ max-height: calc(100% - 150px);
312
+ background-color: #fff;
313
+ margin: 0 10px 60px 10px;
314
+ }
315
+ }
316
+
317
+ /* Mobile */
318
+ @media screen and (max-width: 709px) {
319
+ .sky-dialog-paper {
320
+ height: calc(100% - 142px);
321
+ max-height: calc(100% - 142px);
322
+ background-color: #fff;
323
+ margin: 0 10px 10px 10px;
324
+ max-width: 100vw !important;
325
+ }
326
+
327
+ .sky-dialog-footer {
328
+ transform: translateY(-6px);
329
+ }
330
+ }
331
+
332
+ @media screen and (max-width: 500px) {
333
+ .sky-dialog-subtitle {
334
+ display: block;
335
+ white-space: nowrap;
336
+ overflow: hidden;
337
+ text-overflow: ellipsis;
338
+ }
339
+
340
+ .sky-dialog-title-with-subtitle {
341
+ padding: 12px 24px !important;
342
+ }
343
+ }
344
+
345
+ @media screen and (max-width: 374px) {
346
+ .sky-dialog-subtitle {
347
+ font-size: 9pt;
348
+ }
349
+ }
350
+
351
+ /* iPhone safe area support */
352
+ @supports (padding-top: env(safe-area-inset-top)) {
353
+ .sky-dialog-paper {
354
+ height: calc(100% - 150px - env(safe-area-inset-top));
355
+ }
356
+
357
+ .sky-dialog-footer {
358
+ padding-bottom: calc(env(safe-area-inset-bottom) + 8px);
359
+ }
360
+ }
361
+
362
+ /* Animations */
363
+ .sky-dialog-animate {
364
+ animation: sky-dialog-slide-in 0.4s ease-in-out;
365
+ }
366
+
367
+ .sky-dialog-footer-animate {
368
+ animation: sky-dialog-footer-in 0.4s ease-in-out;
369
+ }
370
+
371
+ @keyframes sky-dialog-slide-in {
372
+ 0% {
373
+ opacity: 0;
374
+ margin-top: -1600px;
375
+ }
376
+ 100% {
377
+ opacity: 1;
378
+ margin-top: 0;
379
+ }
380
+ }
381
+
382
+ @keyframes sky-dialog-footer-in {
383
+ 0% {
384
+ opacity: 0;
385
+ bottom: -100px;
386
+ }
387
+ 50% {
388
+ opacity: 0.25;
389
+ bottom: -50px;
390
+ }
391
+ 100% {
392
+ opacity: 1;
393
+ bottom: 15px;
394
+ }
395
+ }
396
+
397
+ /* Transition */
398
+ .dialog-slide-leave-active {
399
+ animation: sky-dialog-slide-in 0.4s reverse;
400
+ }
401
+ </style>
@@ -0,0 +1,175 @@
1
+ <template>
2
+ <header class="sky-header">
3
+ <div class="header-content">
4
+ <div class="header-top">
5
+ <div class="header-title-wrapper">
6
+ <button
7
+ v-if="shouldShowBackButton"
8
+ class="btn-back"
9
+ @click="handleBack"
10
+ :title="backButtonTitle"
11
+ >
12
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
13
+ <path d="M19 12H5M12 19l-7-7 7-7" stroke-linecap="round" stroke-linejoin="round"/>
14
+ </svg>
15
+ </button>
16
+ <div class="header-title-content">
17
+ <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
+ </slot>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="header-actions">
27
+ <slot></slot>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </header>
32
+ </template>
33
+
34
+ <script setup>
35
+ import { computed } from 'vue'
36
+
37
+ const props = defineProps({
38
+ title: {
39
+ type: String,
40
+ default: ''
41
+ },
42
+ subtitle: {
43
+ type: String,
44
+ default: ''
45
+ },
46
+ showBackButton: {
47
+ type: Boolean,
48
+ default: true
49
+ },
50
+ backButtonTitle: {
51
+ type: String,
52
+ default: 'Назад'
53
+ }
54
+ })
55
+
56
+ // Перевіряємо чи сторінка в iframe
57
+ const isInIframe = computed(() => {
58
+ try {
59
+ return window.self !== window.top
60
+ } catch (e) {
61
+ return true
62
+ }
63
+ })
64
+
65
+ // Показуємо кнопку тільки якщо showBackButton=true І сторінка в iframe
66
+ const shouldShowBackButton = computed(() => {
67
+ return props.showBackButton && isInIframe.value
68
+ })
69
+
70
+ // Обробник кнопки "Назад" - відправляє повідомлення батьківському вікну
71
+ const handleBack = () => {
72
+ window.parent.postMessage({ type: 'exit' }, '*')
73
+ }
74
+ </script>
75
+
76
+ <style scoped>
77
+ .sky-header {
78
+ position: sticky;
79
+ top: 0;
80
+ left: 0;
81
+ right: 0;
82
+ background: var(--sky-header-bg, white);
83
+ border-bottom: 1px solid var(--sky-header-border-color, #dee2e6);
84
+ z-index: var(--sky-header-z-index, 100);
85
+ padding: var(--sky-header-padding, 10px 0);
86
+ }
87
+
88
+ .header-content {
89
+ padding: var(--sky-header-content-padding, 4px 14px);
90
+ margin: 0 auto;
91
+ }
92
+
93
+ .header-top {
94
+ display: flex;
95
+ justify-content: space-between;
96
+ align-items: center;
97
+ }
98
+
99
+ .header-title-wrapper {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 12px;
103
+ }
104
+
105
+ .header-title-content {
106
+ display: flex;
107
+ flex-direction: column;
108
+ }
109
+
110
+ .btn-back {
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ width: 32px;
115
+ height: 32px;
116
+ padding: 0;
117
+ background: transparent;
118
+ border: none;
119
+ cursor: pointer;
120
+ border-radius: 6px;
121
+ transition: background-color 0.2s;
122
+ color: var(--sky-header-back-btn-color, #374151);
123
+ }
124
+
125
+ .btn-back img,
126
+ .btn-back svg {
127
+ display: block;
128
+ }
129
+
130
+ .btn-back:hover {
131
+ background-color: var(--sky-header-back-btn-hover-bg, #f8f9fa);
132
+ }
133
+
134
+ .btn-back:active {
135
+ background-color: var(--sky-header-back-btn-active-bg, #e9ecef);
136
+ }
137
+
138
+ .header-title {
139
+ margin: 0;
140
+ font-size: var(--sky-header-title-size, 18px);
141
+ font-weight: var(--sky-header-title-weight, 500);
142
+ color: var(--sky-header-title-color, #252525);
143
+ line-height: 1.5;
144
+ user-select: none;
145
+ }
146
+
147
+ .header-subtitle {
148
+ font-size: var(--sky-header-subtitle-size, 14px);
149
+ color: var(--sky-header-subtitle-color, #6c757d);
150
+ font-weight: 400;
151
+ line-height: 1.5;
152
+ }
153
+
154
+ .header-actions {
155
+ display: flex;
156
+ gap: var(--sky-header-actions-gap, 12px);
157
+ }
158
+
159
+ @media (max-width: 768px) {
160
+ .header-content {
161
+ padding: 12px 16px;
162
+ }
163
+
164
+ .header-top {
165
+ flex-direction: column;
166
+ align-items: flex-start;
167
+ gap: 12px;
168
+ }
169
+
170
+ .header-actions {
171
+ width: 100%;
172
+ justify-content: flex-end;
173
+ }
174
+ }
175
+ </style>