@bagelink/vue 1.6.4 → 1.6.9

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,511 @@
1
+ <script setup lang="ts" generic="T = any">
2
+ import type { Swiper as SwiperType } from 'swiper'
3
+ import type { AutoplayOptions, CoverflowEffectOptions, PaginationOptions, SwiperOptions } from 'swiper/types'
4
+ import { Icon } from '@bagelink/vue'
5
+ import { Autoplay, EffectCoverflow, EffectCube, EffectFade, EffectFlip, Navigation, Pagination } from 'swiper/modules'
6
+ import { Swiper, SwiperSlide } from 'swiper/vue'
7
+ import { computed, ref, watch } from 'vue'
8
+ import 'swiper/css'
9
+ import 'swiper/css/navigation'
10
+ import 'swiper/css/pagination'
11
+ import 'swiper/css/effect-fade'
12
+ import 'swiper/css/effect-coverflow'
13
+ import 'swiper/css/effect-cube'
14
+ import 'swiper/css/effect-flip'
15
+
16
+ type SwiperEffect = 'slide' | 'fade' | 'cube' | 'coverflow' | 'flip'
17
+ type SwiperDirection = 'horizontal' | 'vertical'
18
+ type SwiperVariant = 'default' | 'testimonial' | 'gallery' | 'cards' | 'coverflow' | 'hero'
19
+
20
+ interface VariantConfig {
21
+ slidesPerView?: number | 'auto'
22
+ spaceBetween?: number
23
+ effect?: SwiperEffect
24
+ loop?: boolean
25
+ autoplay?: boolean | AutoplayOptions
26
+ pagination?: boolean | PaginationOptions
27
+ navigation?: boolean
28
+ speed?: number
29
+ centeredSlides?: boolean
30
+ coverflowEffect?: Partial<CoverflowEffectOptions>
31
+ breakpoints?: SwiperOptions['breakpoints']
32
+ }
33
+
34
+ const props = withDefaults(
35
+ defineProps<{
36
+ // Items to render
37
+ items?: T[]
38
+
39
+ // Active index (v-model support)
40
+ index?: number
41
+
42
+ // Preset variant
43
+ variant?: SwiperVariant
44
+
45
+ // Slide dimensions
46
+ slideWidth?: number | string
47
+
48
+ // Basic swiper options
49
+ effect?: SwiperEffect
50
+ direction?: SwiperDirection
51
+ speed?: number
52
+ loop?: boolean
53
+ initialSlide?: number
54
+
55
+ // Slides display
56
+ slidesPerView?: number | 'auto'
57
+ spaceBetween?: number
58
+ centeredSlides?: boolean
59
+
60
+ // Interaction
61
+ grabCursor?: boolean
62
+ keyboard?: boolean
63
+ mousewheel?: boolean
64
+
65
+ // Auto height
66
+ autoHeight?: boolean
67
+
68
+ // Navigation
69
+ navigation?: boolean
70
+
71
+ // Pagination
72
+ pagination?: boolean | PaginationOptions
73
+
74
+ // Autoplay
75
+ autoplay?: boolean | AutoplayOptions
76
+
77
+ // Effect-specific options
78
+ coverflowEffect?: Partial<CoverflowEffectOptions>
79
+
80
+ // Responsive breakpoints
81
+ breakpoints?: SwiperOptions['breakpoints']
82
+
83
+ // Advanced: allow override with raw options
84
+ advancedOptions?: SwiperOptions
85
+ }>(),
86
+ {
87
+ items: undefined,
88
+ index: undefined,
89
+ variant: 'default',
90
+ slideWidth: undefined,
91
+ effect: undefined,
92
+ direction: 'horizontal',
93
+ speed: undefined,
94
+ loop: undefined,
95
+ initialSlide: 0,
96
+ slidesPerView: undefined,
97
+ spaceBetween: undefined,
98
+ centeredSlides: undefined,
99
+ grabCursor: true,
100
+ keyboard: true,
101
+ mousewheel: false,
102
+ autoHeight: false,
103
+ navigation: undefined,
104
+ pagination: undefined,
105
+ autoplay: undefined,
106
+ coverflowEffect: undefined,
107
+ breakpoints: undefined,
108
+ advancedOptions: undefined,
109
+ },
110
+ )
111
+
112
+ const emit = defineEmits<{
113
+ 'update:index': [index: number]
114
+ }>()
115
+
116
+ defineSlots<{
117
+ 'default'?: (props: { item: T, index: number, currentIndex: number }) => any
118
+ 'prev-button'?: () => any
119
+ 'next-button'?: () => any
120
+ 'navigation'?: (props: {
121
+ items: T[]
122
+ currentIndex: number
123
+ slideTo: (index: number) => void
124
+ prev: () => void
125
+ next: () => void
126
+ isFirst: boolean
127
+ isLast: boolean
128
+ }) => any
129
+ 'pagination'?: () => any
130
+ }>()
131
+
132
+ const variantPresets: Record<SwiperVariant, VariantConfig> = {
133
+ default: {},
134
+ testimonial: {
135
+ slidesPerView: 1,
136
+ autoplay: { delay: 5000, disableOnInteraction: false },
137
+ pagination: { clickable: true },
138
+ loop: true,
139
+ speed: 600,
140
+ },
141
+ gallery: {
142
+ slidesPerView: 1,
143
+ spaceBetween: 0,
144
+ navigation: true,
145
+ loop: true,
146
+ },
147
+ cards: {
148
+ slidesPerView: 3,
149
+ spaceBetween: 20,
150
+ navigation: true,
151
+ breakpoints: {
152
+ 320: { slidesPerView: 1, spaceBetween: 10 },
153
+ 768: { slidesPerView: 2, spaceBetween: 15 },
154
+ 1024: { slidesPerView: 3, spaceBetween: 20 },
155
+ },
156
+ },
157
+ coverflow: {
158
+ effect: 'coverflow',
159
+ slidesPerView: 'auto',
160
+ centeredSlides: true,
161
+ loop: true,
162
+ navigation: true,
163
+ coverflowEffect: {
164
+ rotate: -2,
165
+ stretch: 0,
166
+ depth: 100,
167
+ slideShadows: true,
168
+ modifier: 1,
169
+ scale: 0.98,
170
+ },
171
+ },
172
+ hero: {
173
+ slidesPerView: 1,
174
+ autoplay: { delay: 4000 },
175
+ pagination: { clickable: true },
176
+ loop: true,
177
+ speed: 800,
178
+ effect: 'fade',
179
+ },
180
+ }
181
+
182
+ const swiperEl = ref<SwiperType | null>(null)
183
+ const isFirstSlide = ref(true)
184
+ const isLastSlide = ref(false)
185
+ const currentIndex = ref(props.index ?? props.initialSlide ?? 0)
186
+ let isUpdatingFromExternal = false // Guard to prevent update loops
187
+
188
+ const slideStyles = computed(() => {
189
+ const styles: Record<string, string> = {}
190
+
191
+ if (props.slideWidth !== undefined) {
192
+ styles.width = typeof props.slideWidth === 'number' ? `${props.slideWidth}px` : props.slideWidth
193
+ }
194
+
195
+ return styles
196
+ })
197
+
198
+ // Apply variant preset configuration
199
+ const variantConfig = computed(() => variantPresets[props.variant])
200
+
201
+ // Smart default: if slideWidth is set and slidesPerView wasn't set,
202
+ // automatically use 'auto' to let slides determine their own width
203
+ const effectiveSlidesPerView = computed(() => {
204
+ // Priority: explicit prop > variant config > smart default
205
+ if (props.slidesPerView !== undefined) {
206
+ return props.slidesPerView
207
+ }
208
+
209
+ if (variantConfig.value.slidesPerView !== undefined) {
210
+ return variantConfig.value.slidesPerView
211
+ }
212
+
213
+ // Smart default: if slideWidth is provided, use 'auto'
214
+ if (props.slideWidth !== undefined) {
215
+ return 'auto'
216
+ }
217
+
218
+ // Final fallback
219
+ return 1
220
+ })
221
+
222
+ const swiperParams = computed((): SwiperOptions => {
223
+ // Helper to get value with priority: prop > variant > default
224
+ const getValue = <K extends keyof VariantConfig>(
225
+ propValue: any,
226
+ variantKey: K,
227
+ defaultValue: any
228
+ ) => {
229
+ if (propValue !== undefined) return propValue
230
+ if (variantConfig.value[variantKey] !== undefined) return variantConfig.value[variantKey]
231
+ return defaultValue
232
+ }
233
+
234
+ const params: SwiperOptions = {
235
+ modules: [Navigation, Pagination, Autoplay, EffectFade, EffectCoverflow, EffectCube, EffectFlip],
236
+ effect: getValue(props.effect, 'effect', 'slide'),
237
+ direction: props.direction ?? 'horizontal',
238
+ speed: getValue(props.speed, 'speed', 300),
239
+ loop: getValue(props.loop, 'loop', false),
240
+ initialSlide: props.index ?? props.initialSlide,
241
+ slidesPerView: effectiveSlidesPerView.value,
242
+ spaceBetween: getValue(props.spaceBetween, 'spaceBetween', 0),
243
+ centeredSlides: getValue(props.centeredSlides, 'centeredSlides', false),
244
+ grabCursor: props.grabCursor,
245
+ keyboard: props.keyboard,
246
+ mousewheel: props.mousewheel,
247
+ autoHeight: props.autoHeight,
248
+ }
249
+
250
+ // Navigation (priority: prop > variant > default)
251
+ const navigationEnabled = getValue(props.navigation, 'navigation', false)
252
+ params.navigation = !!navigationEnabled
253
+
254
+ // Pagination (priority: prop > variant > default)
255
+ const paginationConfig = getValue(props.pagination, 'pagination', false)
256
+ params.pagination = paginationConfig
257
+ ? (typeof paginationConfig === 'boolean' ? { clickable: true } : paginationConfig)
258
+ : false
259
+
260
+ // Autoplay (priority: prop > variant > default)
261
+ const autoplayConfig = getValue(props.autoplay, 'autoplay', false)
262
+ params.autoplay = autoplayConfig
263
+ ? (typeof autoplayConfig === 'boolean' ? { delay: 3000 } : autoplayConfig)
264
+ : false
265
+
266
+ // Effect-specific options (merge variant and explicit)
267
+ const effectType = params.effect || 'slide'
268
+ if (effectType === 'coverflow') {
269
+ const baseCoverflowEffect = {
270
+ rotate: 0,
271
+ stretch: 0,
272
+ depth: 100,
273
+ modifier: 1,
274
+ slideShadows: true,
275
+ }
276
+ params.coverflowEffect = {
277
+ ...baseCoverflowEffect,
278
+ ...variantConfig.value.coverflowEffect,
279
+ ...props.coverflowEffect,
280
+ }
281
+ }
282
+
283
+ // Breakpoints (priority: prop > variant)
284
+ const breakpointsConfig = getValue(props.breakpoints, 'breakpoints', undefined)
285
+ if (breakpointsConfig) {
286
+ params.breakpoints = breakpointsConfig
287
+ }
288
+
289
+ // Advanced options override
290
+ if (props.advancedOptions) {
291
+ return { ...params, ...props.advancedOptions }
292
+ }
293
+
294
+ return params
295
+ })
296
+
297
+ // Format params for Swiper Vue component with proper types
298
+ const swiperVueProps = computed(() => ({
299
+ modules: swiperParams.value.modules,
300
+ effect: swiperParams.value.effect as 'slide' | 'fade' | 'cube' | 'coverflow' | 'flip',
301
+ direction: swiperParams.value.direction,
302
+ speed: swiperParams.value.speed,
303
+ loop: swiperParams.value.loop,
304
+ initialSlide: swiperParams.value.initialSlide,
305
+ slidesPerView: swiperParams.value.slidesPerView,
306
+ spaceBetween: swiperParams.value.spaceBetween as number,
307
+ centeredSlides: swiperParams.value.centeredSlides,
308
+ grabCursor: swiperParams.value.grabCursor,
309
+ keyboard: swiperParams.value.keyboard as boolean | undefined,
310
+ mousewheel: swiperParams.value.mousewheel as boolean | undefined,
311
+ autoHeight: swiperParams.value.autoHeight,
312
+ navigation: swiperParams.value.navigation as boolean | undefined,
313
+ pagination: swiperParams.value.pagination,
314
+ autoplay: swiperParams.value.autoplay,
315
+ coverflowEffect: swiperParams.value.coverflowEffect,
316
+ breakpoints: swiperParams.value.breakpoints,
317
+ }))
318
+
319
+ const swiperInstance = computed(() => swiperEl.value)
320
+
321
+ function updateNavigationState() {
322
+ if (swiperEl.value && !isUpdatingFromExternal) {
323
+ isFirstSlide.value = swiperEl.value.isBeginning
324
+ isLastSlide.value = swiperEl.value.isEnd
325
+
326
+ // Emit index update for v-model support
327
+ // Use activeIndex for non-loop mode, realIndex for loop mode
328
+ const newIndex = props.loop ? swiperEl.value.realIndex : swiperEl.value.activeIndex
329
+ currentIndex.value = newIndex
330
+
331
+ // Emit to keep external state in sync
332
+ emit('update:index', newIndex)
333
+ }
334
+ }
335
+
336
+ function slideTo(index: number) {
337
+ if (!swiperEl.value) return
338
+ swiperEl.value.slideTo(index)
339
+ }
340
+
341
+ function handleSlideNav(dir: 'prev' | 'next') {
342
+ if (!swiperEl.value) return
343
+
344
+ if (dir === 'next') {
345
+ swiperEl.value.slideNext()
346
+ } else {
347
+ swiperEl.value.slidePrev()
348
+ }
349
+ }
350
+
351
+ function onSwiper(swiper: SwiperType) {
352
+ swiperEl.value = swiper
353
+ updateNavigationState()
354
+ }
355
+
356
+ watch(
357
+ () => props.initialSlide,
358
+ (index) => {
359
+ if (index !== undefined && swiperEl.value) {
360
+ setTimeout(() => {
361
+ if (swiperEl.value) swiperEl.value.slideTo(index)
362
+ }, 1)
363
+ }
364
+ },
365
+ { immediate: true },
366
+ )
367
+
368
+ // Watch for external index changes (v-model support)
369
+ watch(
370
+ () => props.index,
371
+ (newIndex) => {
372
+ if (newIndex !== undefined) {
373
+ currentIndex.value = newIndex
374
+
375
+ if (swiperEl.value) {
376
+ const activeIndex = props.loop ? swiperEl.value.realIndex : swiperEl.value.activeIndex
377
+ if (newIndex !== activeIndex) {
378
+ isUpdatingFromExternal = true
379
+ swiperEl.value.slideTo(newIndex)
380
+ // Reset guard after a short delay to allow transition
381
+ setTimeout(() => {
382
+ isUpdatingFromExternal = false
383
+ }, 50)
384
+ }
385
+ }
386
+ }
387
+ },
388
+ )
389
+
390
+ // Watch for param changes and update swiper
391
+ watch(
392
+ swiperParams,
393
+ (newParams) => {
394
+ if (swiperEl.value) {
395
+ Object.assign(swiperEl.value.params, newParams)
396
+ swiperEl.value.update()
397
+ }
398
+ },
399
+ { deep: true },
400
+ )
401
+
402
+ defineExpose({
403
+ swiper: swiperInstance,
404
+ slideNext: () => { handleSlideNav('next') },
405
+ slidePrev: () => { handleSlideNav('prev') },
406
+ slideTo,
407
+ update: () => swiperEl.value?.update(),
408
+ isFirst: isFirstSlide,
409
+ isLast: isLastSlide,
410
+ currentIndex,
411
+ })
412
+ </script>
413
+
414
+ <template>
415
+ <div class="swi-wrap">
416
+ <Swiper
417
+ v-bind="swiperVueProps" class="swiper" @swiper="onSwiper" @slide-change="updateNavigationState"
418
+ @reach-beginning="updateNavigationState" @reach-end="updateNavigationState"
419
+ @from-edge="updateNavigationState"
420
+ >
421
+ <SwiperSlide v-for="(item, idx) in items" :key="idx" :style="slideStyles">
422
+ <slot :item="item" :index="idx" :currentIndex="currentIndex" />
423
+ </SwiperSlide>
424
+ </Swiper>
425
+
426
+ <!-- Custom Navigation Slot -->
427
+ <slot
428
+ name="navigation" :items="items || []" :currentIndex="currentIndex" :slide-to="slideTo"
429
+ :prev="() => handleSlideNav('prev')" :next="() => handleSlideNav('next')" :is-first="isFirstSlide"
430
+ :is-last="isLastSlide"
431
+ >
432
+ <!-- Default Navigation -->
433
+ <div v-if="navigation" class="swi-ctrl">
434
+ <div class="swi-prev hover" @click="handleSlideNav('prev')">
435
+ <slot name="prev-button">
436
+ <Icon name="chevron_left" />
437
+ </slot>
438
+ </div>
439
+ <div class="swi-next hover" @click="handleSlideNav('next')">
440
+ <slot name="next-button">
441
+ <Icon name="chevron_right" />
442
+ </slot>
443
+ </div>
444
+ </div>
445
+ </slot>
446
+
447
+ <!-- Custom Pagination Slot -->
448
+ <slot v-if="pagination" name="pagination" />
449
+ </div>
450
+ </template>
451
+
452
+ <style>
453
+ .swiper {
454
+ width: 100%;
455
+ }
456
+
457
+ :root {
458
+ --swiper-navigation-color: white;
459
+ --swiper-pagination-color: white;
460
+ }
461
+
462
+ .swi-ctrl {
463
+ position: absolute;
464
+ top: 50%;
465
+ transform: translateY(-50%);
466
+ width: 100%;
467
+ z-index: 99;
468
+ display: flex;
469
+ justify-content: space-between;
470
+ padding: 0rem 4rem;
471
+ height: 0;
472
+ pointer-events: none;
473
+ }
474
+
475
+ .swi-ctrl>* {
476
+ pointer-events: auto;
477
+ }
478
+
479
+ .swi-ctrl img {
480
+ height: 20px;
481
+ }
482
+
483
+ .swi-wrap {
484
+ position: relative;
485
+ }
486
+
487
+ .swi-prev,
488
+ .swi-next {
489
+ background: var(--blue);
490
+ height: 40px;
491
+ width: 40px;
492
+ border-radius: 100%;
493
+ padding: 10px;
494
+ display: flex;
495
+ justify-content: center;
496
+ align-items: center;
497
+ cursor: pointer;
498
+ transition: opacity 0.3s ease;
499
+ }
500
+
501
+ .swi-prev:hover,
502
+ .swi-next:hover {
503
+ opacity: 0.8;
504
+ }
505
+
506
+ @media screen and (max-width: 900px) {
507
+ .swi-ctrl {
508
+ padding: 0rem 0.5rem;
509
+ }
510
+ }
511
+ </style>
@@ -41,6 +41,7 @@ export { default as Rating } from './Rating.vue'
41
41
  export { default as RouterWrapper } from './RouterWrapper.vue'
42
42
  export { default as Slider } from './Slider.vue'
43
43
  export { default as Spreadsheet } from './Spreadsheet/Index.vue'
44
+ export { default as Swiper } from './Swiper.vue'
44
45
  export { default as Title } from './Title.vue'
45
46
  export { default as ToolBar } from './ToolBar.vue'
46
47
  export { default as TopBar } from './TopBar.vue'
@@ -55,10 +55,66 @@
55
55
  border-radius: 100%;
56
56
  }
57
57
 
58
- .aspect-ratio-1 {
58
+ .aspect-ratio-1,
59
+ .ratio-1 {
59
60
  aspect-ratio: 1;
60
61
  }
61
62
 
63
+ .aspect-ratio-4-3,
64
+ .ratio-4-3 {
65
+ aspect-ratio: 4 / 3;
66
+ }
67
+
68
+ .aspect-ratio-3-4,
69
+ .ratio-3-4 {
70
+ aspect-ratio: 3 / 4;
71
+ }
72
+
73
+ .aspect-ratio-16-9,
74
+ .ratio-16-9 {
75
+ aspect-ratio: 16 / 9;
76
+ }
77
+
78
+ .aspect-ratio-9-16,
79
+ .ratio-9-16 {
80
+ aspect-ratio: 9 / 16;
81
+ }
82
+
83
+ .aspect-ratio-3-2,
84
+ .ratio-3-2 {
85
+ aspect-ratio: 3 / 2;
86
+ }
87
+
88
+ .aspect-ratio-2-3,
89
+ .ratio-2-3 {
90
+ aspect-ratio: 2 / 3;
91
+ }
92
+
93
+ .aspect-ratio-5-4,
94
+ .ratio-5-4 {
95
+ aspect-ratio: 5 / 4;
96
+ }
97
+
98
+ .aspect-ratio-4-5,
99
+ .ratio-4-5 {
100
+ aspect-ratio: 4 / 5;
101
+ }
102
+
103
+ .aspect-ratio-2-1,
104
+ .ratio-2-1 {
105
+ aspect-ratio: 2 / 1;
106
+ }
107
+
108
+ .aspect-ratio-1-2,
109
+ .ratio-1-2 {
110
+ aspect-ratio: 1 / 2;
111
+ }
112
+
113
+ .aspect-ratio-21-9,
114
+ .ratio-21-9 {
115
+ aspect-ratio: 21 / 9;
116
+ }
117
+
62
118
  .vertical-align-middle,
63
119
  .vertical-middle {
64
120
  vertical-align: middle;
@@ -91,10 +91,68 @@
91
91
  border-radius: 100%;
92
92
  }
93
93
 
94
- .m_aspect-ratio-1 {
94
+
95
+ .m_aspect-ratio-1,
96
+ .m_ratio-1 {
95
97
  aspect-ratio: 1;
96
98
  }
97
99
 
100
+ .m_aspect-ratio-4-3,
101
+ .m_ratio-4-3 {
102
+ aspect-ratio: 4 / 3;
103
+ }
104
+
105
+ .m_aspect-ratio-3-4,
106
+ .m_ratio-3-4 {
107
+ aspect-ratio: 3 / 4;
108
+ }
109
+
110
+ .m_aspect-ratio-16-9,
111
+ .m_ratio-16-9 {
112
+ aspect-ratio: 16 / 9;
113
+ }
114
+
115
+ .m_aspect-ratio-9-16,
116
+ .m_ratio-9-16 {
117
+ aspect-ratio: 9 / 16;
118
+ }
119
+
120
+ .m_aspect-ratio-3-2,
121
+ .m_ratio-3-2 {
122
+ aspect-ratio: 3 / 2;
123
+ }
124
+
125
+ .m_aspect-ratio-2-3,
126
+ .m_ratio-2-3 {
127
+ aspect-ratio: 2 / 3;
128
+ }
129
+
130
+ .m_aspect-ratio-5-4,
131
+ .m_ratio-5-4 {
132
+ aspect-ratio: 5 / 4;
133
+ }
134
+
135
+ .m_aspect-ratio-4-5,
136
+ .m_ratio-4-5 {
137
+ aspect-ratio: 4 / 5;
138
+ }
139
+
140
+ .m_aspect-ratio-2-1,
141
+ .m_ratio-2-1 {
142
+ aspect-ratio: 2 / 1;
143
+ }
144
+
145
+ .m_aspect-ratio-1-2,
146
+ .m_ratio-1-2 {
147
+ aspect-ratio: 1 / 2;
148
+ }
149
+
150
+ .m_aspect-ratio-21-9,
151
+ .m_ratio-21-9 {
152
+ aspect-ratio: 21 / 9;
153
+ }
154
+
155
+
98
156
  .m_vertical-align-middle,
99
157
  .m_vertical-middle {
100
158
  vertical-align: middle;