srcdev-nuxt-components 4.0.0 → 4.0.2
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,263 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="carousel-basic" :class="[elementClasses]" ref="carouselWrapperRef" role="region" aria-label="Image carousel">
|
|
3
|
+
<!-- Screen reader announcement for current item -->
|
|
4
|
+
<div aria-live="polite" aria-atomic="true" class="sr-only">Item {{ currentIndex + 1 }} of {{ itemCount }}</div>
|
|
5
|
+
|
|
6
|
+
<LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
|
|
7
|
+
<div tabindex="0" class="item-container" :class="{ 'allow-overflow': allowCarouselOverflow }" ref="carouselContainerRef" role="group" aria-label="Carousel items">
|
|
8
|
+
<div v-for="(item, index) in carouselDataIds" :key="index" class="item" ref="carouselItems" :aria-current="currentIndex === index ? 'true' : 'false'">
|
|
9
|
+
<slot :name="item"></slot>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</LayoutRow>
|
|
13
|
+
|
|
14
|
+
<LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
|
|
15
|
+
<div class="timeline-container">
|
|
16
|
+
<div v-for="index in itemCount" :key="index" class="timeline-item">
|
|
17
|
+
<div class="count">Step {{ index }}</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</LayoutRow>
|
|
21
|
+
|
|
22
|
+
<LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
|
|
23
|
+
<div tabindex="0" class="controls-container" ref="controlsContainerRef">
|
|
24
|
+
<div class="markers-container">
|
|
25
|
+
<ul class="markers-list">
|
|
26
|
+
<li v-for="index in itemCount" :key="index" class="markers-item">
|
|
27
|
+
<button @click.prevent="jumpToFrame(index - 1)" class="btn-marker" :class="[{ active: currentIndex === index - 1 }]" :aria-label="`Jump to item ${Math.floor(index + 1)}`"></button>
|
|
28
|
+
</li>
|
|
29
|
+
</ul>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="buttons-container">
|
|
32
|
+
<button type="button" @click.prevent="actionPrevious()" class="btn-action" aria-label="Go to previous item">Prev</button>
|
|
33
|
+
<button type="button" @click.prevent="actionNext()" class="btn-action" aria-label="Go to next item">Next</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</LayoutRow>
|
|
37
|
+
</section>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
import { useEventListener, useResizeObserver, useSwipe } from '@vueuse/core';
|
|
42
|
+
|
|
43
|
+
const props = defineProps({
|
|
44
|
+
carouselDataIds: {
|
|
45
|
+
type: Array as PropType<string[]>,
|
|
46
|
+
default: () => [],
|
|
47
|
+
},
|
|
48
|
+
styleClassPassthrough: {
|
|
49
|
+
type: Array as PropType<string[]>,
|
|
50
|
+
default: () => [],
|
|
51
|
+
},
|
|
52
|
+
transitionSpeed: {
|
|
53
|
+
type: Number,
|
|
54
|
+
default: 200,
|
|
55
|
+
},
|
|
56
|
+
allowCarouselOverflow: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: false,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
63
|
+
|
|
64
|
+
const carouselWrapperRef = ref<HTMLDivElement | null>(null);
|
|
65
|
+
const carouselContainerRef = ref<HTMLDivElement | null>(null);
|
|
66
|
+
const carouselItemsRef = useTemplateRef<HTMLDivElement[]>('carouselItems');
|
|
67
|
+
const controlsContainerRef = ref<HTMLDivElement | null>(null);
|
|
68
|
+
const carouselInitComplete = ref(false);
|
|
69
|
+
|
|
70
|
+
const currentIndex = ref(0);
|
|
71
|
+
const itemCount = ref(props.carouselDataIds.length);
|
|
72
|
+
const offset = ref(0);
|
|
73
|
+
const transitionSpeedStr = props.transitionSpeed + 'ms';
|
|
74
|
+
const itemTransform = computed(() => {
|
|
75
|
+
return `translateX(calc(${offset.value} * (${itemWidth.value} + var(--_item-gap))))`;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const itemWidth = ref('0px');
|
|
79
|
+
|
|
80
|
+
const actionPrevious = () => {
|
|
81
|
+
if (offset.value >= 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
offset.value = Math.min(offset.value + 1);
|
|
86
|
+
doAction();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const actionNext = () => {
|
|
90
|
+
if (offset.value <= -1 * (itemCount.value - 1)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
offset.value = Math.min(offset.value - 1);
|
|
95
|
+
doAction();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const doAction = () => {
|
|
99
|
+
currentIndex.value = Math.abs(offset.value);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const jumpToFrame = (index: number) => {
|
|
103
|
+
if (index >= 0 && index < itemCount.value) {
|
|
104
|
+
offset.value = -index;
|
|
105
|
+
doAction();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const initialSetup = () => {
|
|
110
|
+
if (carouselItemsRef?.value && carouselItemsRef.value.length > 0 && carouselItemsRef.value[0]) {
|
|
111
|
+
itemWidth.value = carouselItemsRef.value[0].offsetWidth + 'px';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
carouselInitComplete.value = true;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const { direction } = useSwipe(carouselContainerRef, {
|
|
118
|
+
passive: false,
|
|
119
|
+
onSwipeEnd() {
|
|
120
|
+
if (direction.value === 'left') {
|
|
121
|
+
actionNext();
|
|
122
|
+
} else if (direction.value === 'right') {
|
|
123
|
+
actionPrevious();
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
useEventListener(carouselContainerRef, 'keydown', (event: KeyboardEvent) => {
|
|
129
|
+
if (event.key === 'ArrowLeft') {
|
|
130
|
+
actionPrevious();
|
|
131
|
+
} else if (event.key === 'ArrowRight') {
|
|
132
|
+
actionNext();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
useEventListener(controlsContainerRef, 'keydown', (event: KeyboardEvent) => {
|
|
137
|
+
if (event.key === 'ArrowLeft') {
|
|
138
|
+
actionPrevious();
|
|
139
|
+
} else if (event.key === 'ArrowRight') {
|
|
140
|
+
actionNext();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
useResizeObserver(carouselWrapperRef, async () => {
|
|
145
|
+
initialSetup();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
onMounted(() => {
|
|
149
|
+
initialSetup();
|
|
150
|
+
});
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<style lang="css">
|
|
154
|
+
.carousel-basic {
|
|
155
|
+
--_item-gap: 10px;
|
|
156
|
+
|
|
157
|
+
display: grid;
|
|
158
|
+
grid-template-columns: 1fr;
|
|
159
|
+
gap: 10px;
|
|
160
|
+
|
|
161
|
+
.sr-only {
|
|
162
|
+
position: absolute;
|
|
163
|
+
width: 1px;
|
|
164
|
+
height: 1px;
|
|
165
|
+
padding: 0;
|
|
166
|
+
margin: -1px;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
clip: rect(0, 0, 0, 0);
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
border: 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.timeline-container {
|
|
174
|
+
display: flex;
|
|
175
|
+
gap: var(--_item-gap);
|
|
176
|
+
overflow-x: hidden;
|
|
177
|
+
|
|
178
|
+
.timeline-item {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex: 0 0 100%;
|
|
181
|
+
max-inline-size: 800px;
|
|
182
|
+
align-items: center;
|
|
183
|
+
transform: v-bind(itemTransform);
|
|
184
|
+
position: relative;
|
|
185
|
+
|
|
186
|
+
&::before {
|
|
187
|
+
content: '';
|
|
188
|
+
position: absolute;
|
|
189
|
+
height: 2px;
|
|
190
|
+
background-color: #fff;
|
|
191
|
+
left: 70px;
|
|
192
|
+
right: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.count {
|
|
196
|
+
width: fit-content;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.item-container {
|
|
202
|
+
display: flex;
|
|
203
|
+
gap: var(--_item-gap);
|
|
204
|
+
overflow-x: hidden;
|
|
205
|
+
position: relative;
|
|
206
|
+
|
|
207
|
+
&.allow-overflow {
|
|
208
|
+
overflow-x: initial;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.item {
|
|
212
|
+
display: flex;
|
|
213
|
+
flex: 0 0 100%;
|
|
214
|
+
max-inline-size: 800px;
|
|
215
|
+
transition: transform v-bind(transitionSpeedStr) ease;
|
|
216
|
+
transform: v-bind(itemTransform);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.controls-container {
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
justify-content: flex-end;
|
|
224
|
+
|
|
225
|
+
.markers-container {
|
|
226
|
+
.markers-list {
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-direction: row;
|
|
229
|
+
gap: 10px;
|
|
230
|
+
list-style-type: none;
|
|
231
|
+
margin: unset;
|
|
232
|
+
padding: unset;
|
|
233
|
+
|
|
234
|
+
.markers-item {
|
|
235
|
+
.btn-marker {
|
|
236
|
+
border: none;
|
|
237
|
+
outline: none;
|
|
238
|
+
box-shadow: none;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
transition: background-color v-bind(transitionSpeedStr) linear;
|
|
241
|
+
|
|
242
|
+
&.active {
|
|
243
|
+
background-color: red;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.buttons-container {
|
|
251
|
+
display: flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: end;
|
|
254
|
+
gap: 20px;
|
|
255
|
+
|
|
256
|
+
.btn-action {
|
|
257
|
+
cursor: pointer;
|
|
258
|
+
height: fit-content;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
</style>
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="carousel-flip" :class="[elementClasses, { 'controls-inside': controlsInside }]">
|
|
3
|
+
|
|
4
|
+
<div class="item-container" ref="carouselContent">
|
|
5
|
+
<div v-for="(item, index) in data?.items" :key="index" class="item" ref="carouselItems">
|
|
6
|
+
<h3>{{ index }}</h3>
|
|
7
|
+
<p>{{ item.alt }}</p>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="controls-container">
|
|
13
|
+
<div class="buttons-container">
|
|
14
|
+
<button type="submit" @click.prevent="actionPrevious()" class="btn-action"
|
|
15
|
+
:disabled="transitionRunning">Prev</button>
|
|
16
|
+
<button type="submit" @click.prevent="actionNext()" class="btn-action"
|
|
17
|
+
:disabled="transitionRunning">Next</button>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="thumbnail-container">
|
|
20
|
+
<ul class="thumbnail-list">
|
|
21
|
+
<li v-for="item, index in data?.items" class="thumbnail-item" ref="thumbnailItems">
|
|
22
|
+
<div class="thumbnail-item_inner">{{ index }}</div>
|
|
23
|
+
</li>
|
|
24
|
+
</ul>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import type { ICarouselBasic } from "@/types/types.carousel-basic";
|
|
32
|
+
import { useElementSize, useEventListener, useResizeObserver } from "@vueuse/core";
|
|
33
|
+
const props = defineProps({
|
|
34
|
+
propsData: {
|
|
35
|
+
type: Object as PropType<ICarouselBasic>,
|
|
36
|
+
default: <ICarouselBasic>{
|
|
37
|
+
items: [],
|
|
38
|
+
total: 0,
|
|
39
|
+
skip: 0,
|
|
40
|
+
limit: 10
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
data: {
|
|
44
|
+
type: Object,
|
|
45
|
+
default: <ICarouselBasic>{}
|
|
46
|
+
},
|
|
47
|
+
styleClassPassthrough: {
|
|
48
|
+
type: Array as PropType<string[]>,
|
|
49
|
+
default: () => [],
|
|
50
|
+
},
|
|
51
|
+
transitionSpeed: {
|
|
52
|
+
type: Number,
|
|
53
|
+
default: 1000
|
|
54
|
+
},
|
|
55
|
+
controlsInside: {
|
|
56
|
+
type: Boolean,
|
|
57
|
+
default: false
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
62
|
+
|
|
63
|
+
const carouselContentRef = useTemplateRef<HTMLDivElement>('carouselContent');
|
|
64
|
+
const carouselItems = useTemplateRef<HTMLDivElement[]>('carouselItems');
|
|
65
|
+
const thumbnailItems = useTemplateRef<HTMLLIElement[]>('thumbnailItems');
|
|
66
|
+
const carouselInitComplete = ref(false);
|
|
67
|
+
|
|
68
|
+
const currentIndex = ref(1);
|
|
69
|
+
const itemCount = ref(props.data.items.length);
|
|
70
|
+
const offset = ref(1);
|
|
71
|
+
const previousOffset = ref(1);
|
|
72
|
+
const transitionSpeedStr = props.transitionSpeed + 'ms';
|
|
73
|
+
const transitionRunning = ref(false);
|
|
74
|
+
|
|
75
|
+
const actionPrevious = () => {
|
|
76
|
+
// if (transitionRunning.value) return;
|
|
77
|
+
|
|
78
|
+
offset.value = -1;
|
|
79
|
+
onTransitionEnd();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const actionNext = () => {
|
|
83
|
+
// if (transitionRunning.value) return;
|
|
84
|
+
|
|
85
|
+
offset.value = 1;
|
|
86
|
+
onTransitionEnd();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const updateOrder = (index: number, order: number) => {
|
|
90
|
+
if (carouselItems.value !== null && thumbnailItems.value !== null) {
|
|
91
|
+
carouselItems.value[index - 1].style.order = order.toString();
|
|
92
|
+
thumbnailItems.value[index - 1].style.order = order.toString();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const initialSetup = () => {
|
|
97
|
+
const items = carouselItems.value;
|
|
98
|
+
const thumbs = thumbnailItems.value;
|
|
99
|
+
|
|
100
|
+
items?.forEach((item, index) => {
|
|
101
|
+
item.style.zIndex = index === 0 || index === itemCount.value - 1 ? '1' : '2';
|
|
102
|
+
item.style.order = String(index + 1);
|
|
103
|
+
// item.setAttribute('data-order', String(index + 1));
|
|
104
|
+
});
|
|
105
|
+
thumbs?.forEach((thumb, index) => {
|
|
106
|
+
thumb.style.zIndex = index === 0 || index === itemCount.value - 1 ? '1' : '2';
|
|
107
|
+
thumb.style.order = String(index + 1);
|
|
108
|
+
// thumb.setAttribute('data-order', String(index + 1));
|
|
109
|
+
});
|
|
110
|
+
carouselInitComplete.value = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
const onTransitionEnd = () => {
|
|
115
|
+
|
|
116
|
+
// transitionRunning.value = true;
|
|
117
|
+
const items = carouselItems.value;
|
|
118
|
+
const thumbs = thumbnailItems.value;
|
|
119
|
+
|
|
120
|
+
if (!items || !Array.isArray(items)) return;
|
|
121
|
+
if (!thumbs || !Array.isArray(thumbs)) return;
|
|
122
|
+
|
|
123
|
+
// 1. Capture initial positions for both main items and thumbnails
|
|
124
|
+
const firstRects = items.map(el => el.getBoundingClientRect());
|
|
125
|
+
const firstThumbRects = thumbs.map(el => el.getBoundingClientRect());
|
|
126
|
+
|
|
127
|
+
// 2. Update orders
|
|
128
|
+
let firstVisualElementIndex = currentIndex.value; // Track which element should be visually first
|
|
129
|
+
|
|
130
|
+
if (carouselInitComplete.value) {
|
|
131
|
+
if (offset.value === 1) {
|
|
132
|
+
const localOffset = offset.value === previousOffset.value ? offset.value : 2; // Ensure we have a valid offset
|
|
133
|
+
currentIndex.value = currentIndex.value === itemCount.value ? 1 : currentIndex.value + localOffset;
|
|
134
|
+
firstVisualElementIndex = currentIndex.value;
|
|
135
|
+
let order = 1;
|
|
136
|
+
|
|
137
|
+
for (let i = currentIndex.value; i <= itemCount.value; i++) updateOrder(i, order++);
|
|
138
|
+
for (let i = 1; i < currentIndex.value; i++) updateOrder(i, order++);
|
|
139
|
+
|
|
140
|
+
} else {
|
|
141
|
+
const localOffset = offset.value === previousOffset.value ? offset.value : -2; // Ensure we have a valid offset
|
|
142
|
+
currentIndex.value = currentIndex.value === 1 ? itemCount.value : currentIndex.value + localOffset;
|
|
143
|
+
firstVisualElementIndex = currentIndex.value;
|
|
144
|
+
let order = itemCount.value;
|
|
145
|
+
|
|
146
|
+
for (let i = currentIndex.value; i >= 1; i--) updateOrder(i, order--);
|
|
147
|
+
for (let i = itemCount.value; i > currentIndex.value; i--) updateOrder(i, order--);
|
|
148
|
+
}
|
|
149
|
+
previousOffset.value = offset.value; // Store the previous offset for next transition
|
|
150
|
+
|
|
151
|
+
// setTimeout(() => {
|
|
152
|
+
// transitionRunning.value = false;
|
|
153
|
+
// }, props.transitionSpeed);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Next tick: capture new positions & animate both main items and thumbnails
|
|
157
|
+
requestAnimationFrame(() => {
|
|
158
|
+
const lastRects = items.map(el => el.getBoundingClientRect());
|
|
159
|
+
const lastThumbRects = thumbs.map(el => el.getBoundingClientRect());
|
|
160
|
+
|
|
161
|
+
// Animate main carousel items
|
|
162
|
+
items.forEach((el, i) => {
|
|
163
|
+
const dx = firstRects[i].left - lastRects[i].left;
|
|
164
|
+
const dy = firstRects[i].top - lastRects[i].top;
|
|
165
|
+
|
|
166
|
+
el.style.transition = 'none';
|
|
167
|
+
el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
168
|
+
|
|
169
|
+
requestAnimationFrame(() => {
|
|
170
|
+
el.style.transition = `transform ${transitionSpeedStr} ease`;
|
|
171
|
+
el.style.transform = '';
|
|
172
|
+
|
|
173
|
+
// Set z-index after the transition actually completes
|
|
174
|
+
const elementIndex = i + 1; // Convert to 1-based index to match your logic
|
|
175
|
+
const isFirstVisual = elementIndex === firstVisualElementIndex;
|
|
176
|
+
|
|
177
|
+
// Listen for transition end to update z-index
|
|
178
|
+
const handleTransitionEnd = (event: TransitionEvent) => {
|
|
179
|
+
if (event.propertyName === 'transform') {
|
|
180
|
+
el.style.zIndex = isFirstVisual ? '1' : '2';
|
|
181
|
+
el.removeEventListener('transitionend', handleTransitionEnd);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
el.addEventListener('transitionend', handleTransitionEnd);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Animate thumbnail items
|
|
190
|
+
thumbs.forEach((thumb, i) => {
|
|
191
|
+
const dx = firstThumbRects[i].left - lastThumbRects[i].left;
|
|
192
|
+
const dy = firstThumbRects[i].top - lastThumbRects[i].top;
|
|
193
|
+
|
|
194
|
+
thumb.style.transition = 'none';
|
|
195
|
+
thumb.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
196
|
+
|
|
197
|
+
requestAnimationFrame(() => {
|
|
198
|
+
thumb.style.transition = `transform ${transitionSpeedStr} ease`;
|
|
199
|
+
thumb.style.transform = '';
|
|
200
|
+
|
|
201
|
+
// Set z-index after the transition actually completes
|
|
202
|
+
const thumbIndex = i + 1; // Convert to 1-based index
|
|
203
|
+
const isActiveThumbnail = thumbIndex === firstVisualElementIndex;
|
|
204
|
+
|
|
205
|
+
// Listen for transition end to update z-index
|
|
206
|
+
const handleThumbTransitionEnd = (event: TransitionEvent) => {
|
|
207
|
+
if (event.propertyName === 'transform') {
|
|
208
|
+
thumb.style.zIndex = isActiveThumbnail ? '1' : '2';
|
|
209
|
+
thumb.removeEventListener('transitionend', handleThumbTransitionEnd);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
thumb.addEventListener('transitionend', handleThumbTransitionEnd);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
carouselInitComplete.value = true;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
onMounted(() => {
|
|
222
|
+
initialSetup();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
</script>
|
|
226
|
+
|
|
227
|
+
<style lang="css">
|
|
228
|
+
.carousel-flip {
|
|
229
|
+
|
|
230
|
+
display: grid;
|
|
231
|
+
grid-template-columns: 1fr;
|
|
232
|
+
gap: 10px;
|
|
233
|
+
|
|
234
|
+
&.controls-inside {
|
|
235
|
+
grid-template-areas: "carousel-content";
|
|
236
|
+
isolation: isolate;
|
|
237
|
+
|
|
238
|
+
.item-container {
|
|
239
|
+
grid-area: carousel-content;
|
|
240
|
+
z-index: 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.controls-container {
|
|
244
|
+
grid-area: carousel-content;
|
|
245
|
+
z-index: 2;
|
|
246
|
+
height: fit-content;
|
|
247
|
+
align-self: flex-end;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.item-container {
|
|
252
|
+
display: flex;
|
|
253
|
+
gap: 10px;
|
|
254
|
+
overflow-x: auto;
|
|
255
|
+
padding-block: 10px;
|
|
256
|
+
padding-inline: 10px;
|
|
257
|
+
outline: 1px solid light-dark(#00000090, #f00ff090);
|
|
258
|
+
|
|
259
|
+
/* scroll-snap-type: x mandatory; */
|
|
260
|
+
|
|
261
|
+
/* isolation: isolate; */
|
|
262
|
+
position: relative;
|
|
263
|
+
|
|
264
|
+
.item {
|
|
265
|
+
display: flex;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
align-items: center;
|
|
268
|
+
justify-content: center;
|
|
269
|
+
|
|
270
|
+
/* transition: transform v-bind(transitionSpeedStr) ease; */
|
|
271
|
+
/* For FLIP smoothness */
|
|
272
|
+
|
|
273
|
+
aspect-ratio: 4 / 3;
|
|
274
|
+
|
|
275
|
+
min-inline-size: 600px;
|
|
276
|
+
color: light-dar(#aaa, #333);
|
|
277
|
+
padding-block: 10px;
|
|
278
|
+
padding-inline: 10px;
|
|
279
|
+
border-radius: 4px;
|
|
280
|
+
outline: 1px solid light-dark(#00000090, #f00ff090);
|
|
281
|
+
|
|
282
|
+
background-color: light-dark(#f00, #00f);
|
|
283
|
+
|
|
284
|
+
/* scroll-snap-align: none center; */
|
|
285
|
+
|
|
286
|
+
&:nth-child(odd) {
|
|
287
|
+
background-color: light-dark(#00f, #f00);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
.controls-container {
|
|
294
|
+
|
|
295
|
+
display: flex;
|
|
296
|
+
gap: 20px;
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
.buttons-container {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-grow: 1;
|
|
303
|
+
align-items: center;
|
|
304
|
+
justify-content: end;
|
|
305
|
+
gap: 20px;
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
.btn-action {
|
|
309
|
+
padding: 10px 20px;
|
|
310
|
+
border-radius: 4px;
|
|
311
|
+
background-color: light-dark(#000, #fff);
|
|
312
|
+
color: light-dark(#fff, #000);
|
|
313
|
+
border: none;
|
|
314
|
+
cursor: pointer;
|
|
315
|
+
height: fit-content;
|
|
316
|
+
|
|
317
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
318
|
+
|
|
319
|
+
&:hover {
|
|
320
|
+
background-color: light-dark(#0009, #fff9);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
&:active {
|
|
324
|
+
background-color: light-dark(#0009, #fff9);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
&:disabled {
|
|
328
|
+
background-color: light-dark(#0003, #fff3);
|
|
329
|
+
cursor: not-allowed;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.thumbnail-container {
|
|
335
|
+
padding-block: 10px;
|
|
336
|
+
padding-inline: 10px;
|
|
337
|
+
outline: 1px solid light-dark(#00000090, #f00ff090);
|
|
338
|
+
max-inline-size: 40%;
|
|
339
|
+
|
|
340
|
+
.thumbnail-list {
|
|
341
|
+
display: flex;
|
|
342
|
+
gap: 10px;
|
|
343
|
+
list-style-type: none;
|
|
344
|
+
padding-block: 8px;
|
|
345
|
+
padding-inline: 8px;
|
|
346
|
+
margin-block: 0;
|
|
347
|
+
margin-inline: 0;
|
|
348
|
+
|
|
349
|
+
outline: 1px solid light-dark(#00000090, #f00ff090);
|
|
350
|
+
overflow-x: auto;
|
|
351
|
+
|
|
352
|
+
.thumbnail-item {
|
|
353
|
+
|
|
354
|
+
display: flex;
|
|
355
|
+
align-items: center;
|
|
356
|
+
justify-content: center;
|
|
357
|
+
|
|
358
|
+
aspect-ratio: 3 / 4;
|
|
359
|
+
min-inline-size: 120px;
|
|
360
|
+
outline: 1px solid light-dark(#f00, #00f);
|
|
361
|
+
border-radius: 4px;
|
|
362
|
+
|
|
363
|
+
background-color: light-dark(#f00, #00f);
|
|
364
|
+
|
|
365
|
+
&:nth-child(odd) {
|
|
366
|
+
background-color: light-dark(#00f, #f00);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
.thumbnail-item_inner {}
|
|
371
|
+
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
}
|
|
378
|
+
</style>
|
package/nuxt.config.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export default defineNuxtConfig({
|
|
3
3
|
devtools: { enabled: true },
|
|
4
4
|
css: ['modern-normalize', './assets/styles/main.css'],
|
|
5
|
-
modules: ['@nuxt/icon', '@nuxt/image'
|
|
5
|
+
modules: ['@nuxt/icon', '@nuxt/image'],
|
|
6
6
|
app: {
|
|
7
7
|
head: {
|
|
8
8
|
htmlAttrs: {
|
|
@@ -34,7 +34,4 @@ export default defineNuxtConfig({
|
|
|
34
34
|
typescript: {
|
|
35
35
|
includeWorkspace: true,
|
|
36
36
|
},
|
|
37
|
-
eslint: {
|
|
38
|
-
// Add any custom options here
|
|
39
|
-
},
|
|
40
37
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "srcdev-nuxt-components",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.0.
|
|
4
|
+
"version": "4.0.2",
|
|
5
5
|
"main": "nuxt.config.ts",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"clean": "rm -rf .nuxt && rm -rf .output && rm -rf .playground/.nuxt && rm -rf .playground/.output",
|
|
@@ -27,18 +27,18 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@iconify-json/akar-icons": "1.2.2",
|
|
29
29
|
"@iconify-json/bitcoin-icons": "1.2.2",
|
|
30
|
-
"@nuxt/eslint": "1.
|
|
31
|
-
"@nuxt/
|
|
32
|
-
"@nuxt/icon": "1.12.0",
|
|
30
|
+
"@nuxt/eslint-config": "1.5.2",
|
|
31
|
+
"@nuxt/icon": "1.15.0",
|
|
33
32
|
"@nuxt/image": "1.10.0",
|
|
33
|
+
"@oddbird/css-anchor-positioning": "0.6.1",
|
|
34
|
+
"@vueuse/core": "13.5.0",
|
|
35
|
+
"eslint": "9.31.0",
|
|
34
36
|
"happy-dom": "16.8.1",
|
|
35
|
-
"nuxt": "3.17.
|
|
37
|
+
"nuxt": "3.17.6",
|
|
36
38
|
"release-it": "18.1.2",
|
|
37
39
|
"typescript": "5.8.3"
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
|
-
"@oddbird/css-anchor-positioning": "0.6.0",
|
|
41
|
-
"@vueuse/core": "13.1.0",
|
|
42
42
|
"focus-trap-vue": "4.0.3",
|
|
43
43
|
"modern-normalize": "3.0.1"
|
|
44
44
|
},
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface CarouselBasicItem {
|
|
2
|
+
id: number | string;
|
|
3
|
+
url: string;
|
|
4
|
+
alt: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CarouselModifiedItem {
|
|
8
|
+
id: number | string;
|
|
9
|
+
url: string;
|
|
10
|
+
alt: string;
|
|
11
|
+
order: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ICarouselBasic {
|
|
15
|
+
items: CarouselBasicItem[] | CarouselModifiedItem[];
|
|
16
|
+
total: number;
|
|
17
|
+
skip: number;
|
|
18
|
+
limit: number;
|
|
19
|
+
}
|