tide-design-system 2.4.3 → 2.4.4
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/dist/css/storybook.css +1 -0
- package/dist/css/variables.css +2 -2
- package/dist/style.css +1 -1
- package/dist/tide-design-system.cjs +2 -2
- package/dist/tide-design-system.esm.d.ts +7 -2
- package/dist/tide-design-system.esm.js +1765 -1705
- package/package.json +1 -1
- package/src/assets/css/storybook.css +1 -0
- package/src/assets/css/variables.css +2 -2
- package/src/components/TideCarousel.vue +131 -28
- package/src/stories/TideCarousel.stories.ts +21 -23
- package/src/types/Icon.ts +3 -0
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap');
|
|
3
3
|
|
|
4
4
|
.sb-border-black {border: 1px solid #000000;}
|
|
5
|
+
.sb-border-blue {border: 1px solid #0000FF;}
|
|
5
6
|
.sb-border-blue-light {border: 1px solid #CCDEF3;}
|
|
6
7
|
.sb-border-white {border: 1px solid #FFFFFF;}
|
|
7
8
|
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
|
|
58
58
|
/* Global tonal palette */
|
|
59
59
|
--tide-gray-100: #FFFFFF;
|
|
60
|
-
--tide-gray-200: #
|
|
61
|
-
--tide-gray-300: #
|
|
60
|
+
--tide-gray-200: #F2F2F2;
|
|
61
|
+
--tide-gray-300: #E4E4E5;
|
|
62
62
|
--tide-gray-400: #C9CACB;
|
|
63
63
|
--tide-gray-500: #AEAFB2;
|
|
64
64
|
--tide-gray-600: #939598;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { onMounted, onUnmounted, ref } from 'vue';
|
|
2
|
+
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|
3
3
|
|
|
4
4
|
import TideButtonIcon from '@/components/TideButtonIcon.vue';
|
|
5
5
|
import { ICON } from '@/types/Icon';
|
|
@@ -7,23 +7,25 @@
|
|
|
7
7
|
import { CSS } from '@/types/Styles';
|
|
8
8
|
|
|
9
9
|
type Props = {
|
|
10
|
-
|
|
10
|
+
hasDots?: boolean;
|
|
11
11
|
isFloating?: boolean;
|
|
12
12
|
isHeadline1?: boolean;
|
|
13
13
|
isHideawayButtons?: boolean;
|
|
14
14
|
isScrollByPage?: boolean;
|
|
15
15
|
isTouchscreen?: boolean;
|
|
16
|
+
maxDots?: number;
|
|
16
17
|
subtitle?: string;
|
|
17
18
|
title?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
-
|
|
22
|
+
hasDots: undefined,
|
|
22
23
|
isFloating: false,
|
|
23
24
|
isHeadline1: false,
|
|
24
25
|
isHideawayButtons: true,
|
|
25
26
|
isScrollByPage: true,
|
|
26
27
|
isTouchscreen: undefined,
|
|
28
|
+
maxDots: 5,
|
|
27
29
|
subtitle: undefined,
|
|
28
30
|
title: undefined,
|
|
29
31
|
});
|
|
@@ -35,18 +37,52 @@
|
|
|
35
37
|
const emit = defineEmits<Emits>();
|
|
36
38
|
|
|
37
39
|
const containerRef = ref<HTMLDivElement | null>(null);
|
|
40
|
+
const contentWidth = ref<number>(0);
|
|
41
|
+
const currentPageIndex = ref<number>(0);
|
|
42
|
+
const dotsRef = ref<HTMLDivElement | null>(null);
|
|
43
|
+
const frameWidth = ref<number>(0);
|
|
38
44
|
const isFirstSlide = ref<boolean>(true);
|
|
39
45
|
const isLastSlide = ref<boolean>(false);
|
|
40
46
|
const showButtons = ref<boolean>(true);
|
|
41
47
|
const slideObserver = ref<IntersectionObserver | null>(null);
|
|
48
|
+
const slides = ref<HTMLElement[]>([]);
|
|
42
49
|
const slidesInView = ref<number[]>([]);
|
|
43
50
|
const slotObserver = ref<MutationObserver | null>(null);
|
|
44
51
|
|
|
45
|
-
const
|
|
52
|
+
const currentPage = computed(() => currentPageIndex.value + 1);
|
|
53
|
+
const dotContainerWidth = computed(() => dotCountVisible.value * dotWidth + (dotCountVisible.value - 1) * dotGap);
|
|
54
|
+
const dotCountVisible = computed(() => (props.maxDots > totalPages.value ? totalPages.value : props.maxDots));
|
|
55
|
+
const lastPageIndex = computed(() => totalPages.value - 1);
|
|
56
|
+
const totalPages = computed(() => {
|
|
57
|
+
if (!slides.value.length) return 0;
|
|
58
|
+
if (!props.isScrollByPage) return slides.value.length;
|
|
59
|
+
|
|
60
|
+
const gapWidth = cardGap * (slides.value.length - 1);
|
|
61
|
+
const contentNoGap = contentWidth.value - gapWidth;
|
|
62
|
+
const quotient = Math.round(contentNoGap / frameWidth.value);
|
|
63
|
+
const remainder = contentNoGap % frameWidth.value;
|
|
64
|
+
|
|
65
|
+
return remainder ? quotient + 1 : quotient;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const cardGap: number = 16;
|
|
69
|
+
const dotGap: number = 8;
|
|
70
|
+
const dotWidth: number = 8;
|
|
71
|
+
|
|
72
|
+
const getDotClass = (dotIndex: number) => {
|
|
73
|
+
let className = '';
|
|
74
|
+
|
|
75
|
+
if (dotIndex === currentPageIndex.value) className = 'tide-carousel-dot-active';
|
|
76
|
+
if (Math.abs(currentPageIndex.value - dotIndex) === 1) className = 'tide-carousel-dot-neighbor';
|
|
77
|
+
|
|
78
|
+
return className;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const measureDom = () => {
|
|
82
|
+
if (!containerRef.value) return;
|
|
46
83
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
updateContainerBleed();
|
|
84
|
+
contentWidth.value = containerRef.value.scrollWidth;
|
|
85
|
+
frameWidth.value = containerRef.value.clientWidth;
|
|
50
86
|
};
|
|
51
87
|
|
|
52
88
|
const observeSlides = () => {
|
|
@@ -99,10 +135,17 @@
|
|
|
99
135
|
const scrollToOffset = (target: number) => {
|
|
100
136
|
if (containerRef.value === null) return;
|
|
101
137
|
|
|
138
|
+
const lastOffset: number = slides.value[slides.value.length - 1].offsetLeft;
|
|
139
|
+
const placement = (target / lastOffset) * lastPageIndex.value;
|
|
140
|
+
const isScrollingLeft = placement <= currentPageIndex.value;
|
|
141
|
+
|
|
142
|
+
currentPageIndex.value = isScrollingLeft ? currentPageIndex.value - 1 : currentPageIndex.value + 1;
|
|
143
|
+
|
|
102
144
|
containerRef.value.scrollTo({
|
|
103
145
|
behavior: 'smooth',
|
|
104
146
|
left: target,
|
|
105
147
|
});
|
|
148
|
+
updateDots(isScrollingLeft);
|
|
106
149
|
};
|
|
107
150
|
|
|
108
151
|
const showNextPage = () => {
|
|
@@ -137,19 +180,19 @@
|
|
|
137
180
|
scrollToOffset(slides.value[previousSlide].offsetLeft);
|
|
138
181
|
};
|
|
139
182
|
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
183
|
+
const updateDots = (isScrollingLeft: boolean) => {
|
|
184
|
+
if (!dotsRef.value || dotCountVisible.value >= totalPages.value) return;
|
|
185
|
+
|
|
186
|
+
const isOddDotCount = dotCountVisible.value % 2 !== 0;
|
|
187
|
+
const centerDot =
|
|
188
|
+
isOddDotCount || isScrollingLeft
|
|
189
|
+
? Math.floor(dotCountVisible.value / 2) + 1
|
|
190
|
+
: Math.floor(dotCountVisible.value / 2);
|
|
191
|
+
|
|
192
|
+
dotsRef.value.scrollTo({
|
|
193
|
+
behavior: 'smooth',
|
|
194
|
+
left: currentPage.value < centerDot ? 0 : (currentPage.value - centerDot) * (dotWidth + dotGap),
|
|
195
|
+
});
|
|
153
196
|
};
|
|
154
197
|
|
|
155
198
|
onMounted(() => {
|
|
@@ -157,14 +200,19 @@
|
|
|
157
200
|
|
|
158
201
|
slides.value = Array.from(containerRef.value.children) as HTMLElement[];
|
|
159
202
|
|
|
203
|
+
measureDom();
|
|
160
204
|
observeSlides();
|
|
161
205
|
observeSlot();
|
|
162
|
-
|
|
206
|
+
|
|
207
|
+
if (window) {
|
|
208
|
+
window.addEventListener('resize', measureDom);
|
|
209
|
+
}
|
|
163
210
|
});
|
|
164
211
|
|
|
165
212
|
onUnmounted(() => {
|
|
166
213
|
slideObserver.value?.disconnect();
|
|
167
214
|
slotObserver.value?.disconnect();
|
|
215
|
+
window.removeEventListener('resize', measureDom);
|
|
168
216
|
});
|
|
169
217
|
</script>
|
|
170
218
|
|
|
@@ -225,11 +273,10 @@
|
|
|
225
273
|
|
|
226
274
|
<slot name="misc" />
|
|
227
275
|
|
|
228
|
-
<div :class="[CSS.POSITION.RELATIVE]">
|
|
276
|
+
<div :class="[CSS.POSITION.RELATIVE, CSS.DISPLAY.FLEX, CSS.AXIS1.CENTER]">
|
|
229
277
|
<ul
|
|
230
278
|
:class="[
|
|
231
279
|
'tide-carousel-container',
|
|
232
|
-
props.bleed && 'bleed',
|
|
233
280
|
CSS.DISPLAY.FLEX,
|
|
234
281
|
CSS.GAP.ONE,
|
|
235
282
|
CSS.LIST_BULLETS.OFF,
|
|
@@ -238,11 +285,54 @@
|
|
|
238
285
|
CSS.SNAP.ON,
|
|
239
286
|
]"
|
|
240
287
|
ref="containerRef"
|
|
241
|
-
@scroll="handleScroll"
|
|
242
288
|
>
|
|
243
289
|
<slot />
|
|
244
290
|
</ul>
|
|
245
291
|
|
|
292
|
+
<div
|
|
293
|
+
:class="[
|
|
294
|
+
'tide-carousel-dots-container',
|
|
295
|
+
CSS.POSITION.ABSOLUTE,
|
|
296
|
+
CSS.POSITIONING.BOTTOM,
|
|
297
|
+
CSS.DISPLAY.FLEX,
|
|
298
|
+
CSS.AXIS1.CENTER,
|
|
299
|
+
CSS.MARGIN.BOTTOM.HALF,
|
|
300
|
+
CSS.WIDTH.FULL,
|
|
301
|
+
]"
|
|
302
|
+
:style="{
|
|
303
|
+
width: `${dotContainerWidth}px`,
|
|
304
|
+
}"
|
|
305
|
+
v-if="isScrollByPage && hasDots && totalPages > 1"
|
|
306
|
+
>
|
|
307
|
+
<div
|
|
308
|
+
:class="[
|
|
309
|
+
'tide-carousel-dots',
|
|
310
|
+
CSS.DISPLAY.FLEX,
|
|
311
|
+
CSS.AXIS1.START,
|
|
312
|
+
CSS.AXIS2.CENTER,
|
|
313
|
+
CSS.GAP.HALF,
|
|
314
|
+
CSS.OVERFLOW.X.SCROLL,
|
|
315
|
+
CSS.POINTER_EVENTS.OFF,
|
|
316
|
+
CSS.SCROLLBAR.OFF,
|
|
317
|
+
]"
|
|
318
|
+
ref="dotsRef"
|
|
319
|
+
>
|
|
320
|
+
<div
|
|
321
|
+
:class="[
|
|
322
|
+
'tide-carousel-dot',
|
|
323
|
+
CSS.FLEX.SHRINK.OFF,
|
|
324
|
+
CSS.HEIGHT.ZERO,
|
|
325
|
+
CSS.WIDTH.ZERO,
|
|
326
|
+
CSS.BORDER.RADIUS.FULL,
|
|
327
|
+
CSS.BG.SURFACE.DEFAULT,
|
|
328
|
+
getDotClass(index),
|
|
329
|
+
]"
|
|
330
|
+
:key="index"
|
|
331
|
+
v-for="(_, index) in totalPages"
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
246
336
|
<div
|
|
247
337
|
:class="[
|
|
248
338
|
'tide-carousel-button-overlay',
|
|
@@ -279,10 +369,6 @@
|
|
|
279
369
|
</template>
|
|
280
370
|
|
|
281
371
|
<style scoped>
|
|
282
|
-
.tide-carousel-container.bleed {
|
|
283
|
-
transition: margin var(--tide-animate), padding var(--tide-animate);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
372
|
.tide-carousel-button-overlay.hideaway {
|
|
287
373
|
opacity: 0;
|
|
288
374
|
}
|
|
@@ -290,4 +376,21 @@
|
|
|
290
376
|
.tide-carousel:hover .tide-carousel-button-overlay.hideaway {
|
|
291
377
|
opacity: 1;
|
|
292
378
|
}
|
|
379
|
+
|
|
380
|
+
.tide-carousel-dot {
|
|
381
|
+
width: 8px;
|
|
382
|
+
height: 8px;
|
|
383
|
+
opacity: 0.75;
|
|
384
|
+
transform: scale(0.5);
|
|
385
|
+
transition: opacity var(--tide-animate), transform var(--tide-animate);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.tide-carousel-dot-neighbor {
|
|
389
|
+
transform: scale(0.75);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.tide-carousel-dot-active {
|
|
393
|
+
transform: scale(1);
|
|
394
|
+
opacity: 1;
|
|
395
|
+
}
|
|
293
396
|
</style>
|
|
@@ -7,7 +7,6 @@ import { argTypeBooleanUnrequired, disabledArgType, doSomething, lineBreak, tab
|
|
|
7
7
|
import type { StoryContext } from '@storybook/vue3';
|
|
8
8
|
|
|
9
9
|
type Args = InstanceType<typeof TideCarousel>['$props'] & {
|
|
10
|
-
bleed: number | string;
|
|
11
10
|
handleSlidesAddedToView: string;
|
|
12
11
|
};
|
|
13
12
|
|
|
@@ -15,13 +14,13 @@ const formatSnippet = (code: string, context: StoryContext) => {
|
|
|
15
14
|
const { args } = context;
|
|
16
15
|
|
|
17
16
|
const argsWithValues: string[] = [];
|
|
18
|
-
const hasBleed = args.bleed !== '';
|
|
19
17
|
|
|
20
|
-
if (
|
|
18
|
+
if (args.hasDots !== undefined) argsWithValues.push(`has-dots="${args.hasDots}"`);
|
|
21
19
|
if (args.isFloating !== undefined) argsWithValues.push(`:is-floating="${args.isFloating}"`);
|
|
22
20
|
if (args.isHideawayButtons !== undefined) argsWithValues.push(`:is-hideaway-buttons="${args.isHideawayButtons}"`);
|
|
23
21
|
if (args.isScrollByPage !== undefined) argsWithValues.push(`:is-scroll-by-page="false"`);
|
|
24
22
|
if (args.isTouchscreen !== undefined) argsWithValues.push(`:is-touchscreen="${args.isTouchscreen}"`);
|
|
23
|
+
if (args.maxDots !== '') argsWithValues.push(`max-dots="${args.maxDots}"`);
|
|
25
24
|
if (args.subtitle !== '') argsWithValues.push(`subtitle="${args.subtitle}"`);
|
|
26
25
|
if (args.title !== '') argsWithValues.push(`title="${args.title}"`);
|
|
27
26
|
if (args.handleSlidesAddedToView) argsWithValues.push(`@slides-added-to-view="${args.handleSlidesAddedToView}"`);
|
|
@@ -32,7 +31,7 @@ const formatSnippet = (code: string, context: StoryContext) => {
|
|
|
32
31
|
// prettier-ignore
|
|
33
32
|
`<TideCarousel ${argsWithValues.join(' ')}>` + lineBreak +
|
|
34
33
|
slotContentMisc + tab +
|
|
35
|
-
`<li class="tide-shrink-none
|
|
34
|
+
`<li class="tide-shrink-none" v-for="(_child, index) in new Array(12)">` + lineBreak + tab + tab +
|
|
36
35
|
args.default + lineBreak + tab +
|
|
37
36
|
`</li>` + lineBreak +
|
|
38
37
|
`</TideCarousel>`
|
|
@@ -72,37 +71,20 @@ const render = (args: Args) => ({
|
|
|
72
71
|
template:
|
|
73
72
|
`<TideCarousel
|
|
74
73
|
@slides-added-to-view="handleSlidesAddedToView"
|
|
75
|
-
class="tide-margin-x-1"
|
|
76
74
|
v-bind="args"
|
|
77
75
|
>
|
|
78
76
|
<template #misc>{{ args.misc }}</template>
|
|
79
77
|
<li
|
|
80
|
-
|
|
81
|
-
class="tide-shrink-none tide-border-1 tide-border tide-padding-1"
|
|
78
|
+
class="tide-shrink-none tide-border-1 sb-border-blue tide-padding-1 sb-bg-blue-light"
|
|
82
79
|
v-for="(_child, index) in new Array(12)"
|
|
83
80
|
>
|
|
84
81
|
{{ args.default.replace('#', index) }}
|
|
85
82
|
</li>
|
|
86
83
|
</TideCarousel>`,
|
|
87
|
-
updated() {
|
|
88
|
-
if (typeof args.bleed === 'string' && args.bleed === '') {
|
|
89
|
-
args.bleed = 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return { args };
|
|
93
|
-
},
|
|
94
84
|
});
|
|
95
85
|
|
|
96
86
|
export default {
|
|
97
87
|
argTypes: {
|
|
98
|
-
bleed: {
|
|
99
|
-
control: 'text',
|
|
100
|
-
description: 'Sets an allowance for visible overflow at component boundaries.',
|
|
101
|
-
table: {
|
|
102
|
-
defaultValue: { summary: 'None' },
|
|
103
|
-
type: { summary: 'number' },
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
88
|
default: {
|
|
107
89
|
control: 'text',
|
|
108
90
|
description: 'Dynamic card content',
|
|
@@ -122,6 +104,13 @@ export default {
|
|
|
122
104
|
type: { summary: '(slides: number[]) => void' },
|
|
123
105
|
},
|
|
124
106
|
},
|
|
107
|
+
hasDots: {
|
|
108
|
+
...argTypeBooleanUnrequired,
|
|
109
|
+
description: 'Determines whether to display the indicator dots overlay',
|
|
110
|
+
table: {
|
|
111
|
+
defaultValue: { summary: 'False' },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
125
114
|
isFloating: {
|
|
126
115
|
...argTypeBooleanUnrequired,
|
|
127
116
|
description: 'Determines whether to display on-page or floating carousel',
|
|
@@ -158,6 +147,14 @@ export default {
|
|
|
158
147
|
defaultValue: { summary: 'None' },
|
|
159
148
|
},
|
|
160
149
|
},
|
|
150
|
+
maxDots: {
|
|
151
|
+
control: 'text',
|
|
152
|
+
description: 'Determines the max number of indicator dots to display at a given time',
|
|
153
|
+
table: {
|
|
154
|
+
defaultValue: { summary: 'None' },
|
|
155
|
+
type: { summary: 'number' },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
161
158
|
misc: {
|
|
162
159
|
control: 'text',
|
|
163
160
|
description: 'Content to display between the title/buttons and carousel content',
|
|
@@ -186,13 +183,14 @@ export default {
|
|
|
186
183
|
},
|
|
187
184
|
},
|
|
188
185
|
args: {
|
|
189
|
-
bleed: '',
|
|
190
186
|
default: 'Card #',
|
|
191
187
|
handleSlidesAddedToView: 'doSomething',
|
|
188
|
+
hasDots: undefined,
|
|
192
189
|
isFloating: undefined,
|
|
193
190
|
isHideawayButtons: undefined,
|
|
194
191
|
isScrollByPage: undefined,
|
|
195
192
|
isTouchscreen: undefined,
|
|
193
|
+
maxDots: '',
|
|
196
194
|
misc: '',
|
|
197
195
|
subtitle: '',
|
|
198
196
|
title: 'Demo',
|
package/src/types/Icon.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const ICON = {
|
|
|
11
11
|
ARROW_CYCLE: 'arrow-cycle',
|
|
12
12
|
ARROW_FORWARD: 'arrow-forward',
|
|
13
13
|
ARROW_RIGHT: 'arrow-right',
|
|
14
|
+
ARROW_UP: 'arrow-up',
|
|
14
15
|
ASSIGNMENT: 'assignment',
|
|
15
16
|
ATTACH_MONEY: 'attach-money',
|
|
16
17
|
AUTO_RENEW: 'auto-renew',
|
|
@@ -110,6 +111,8 @@ export const ICON = {
|
|
|
110
111
|
SMS: 'sms',
|
|
111
112
|
SNOWFLAKE: 'snowflake',
|
|
112
113
|
STAR: 'star',
|
|
114
|
+
STAR_FILLED: 'star-filled',
|
|
115
|
+
STAR_HALF: 'star-half',
|
|
113
116
|
SUMMARIZE: 'summarize',
|
|
114
117
|
SWAP_HORIZ: 'swap-horiz',
|
|
115
118
|
SWAP_VERT: 'swap-vert',
|