tide-design-system 2.5.2 → 2.5.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.
@@ -62,7 +62,7 @@ export const getFieldValidationResult = ({
62
62
 
63
63
  // custom validator prop errors from have second highest precedence
64
64
  if (validators) {
65
- const validation = validateProperty(value.value, validators);
65
+ const validation = validateProperty(value.value ?? '', validators);
66
66
 
67
67
  if (!validation.valid) {
68
68
  return validation;
package/docs/upgrading.md CHANGED
@@ -83,9 +83,46 @@ watch(active, doSomething);
83
83
 
84
84
  ✅ After these changes your project should be compatible with **TIDE 2.5**.
85
85
 
86
+ ## Upgrading from 2.4.5 → 2.4.6
87
+
88
+ ### Summary
89
+
90
+ Version **2.4.6** introduced a Nuxt SSR incompatibility. From **2.4.6**, TideModal uses Vue `<Teleport />` to place modal content in `#tide-top-layer`. This is not a problem in CSR apps, but [Nuxt SSR only provides support for one teleport target](https://nuxt.com/docs/4.x/api/components/teleports#:~:text=in%20the%20DOM.-,The,wrapper.,-Body%20Teleport). The default id on this teleport target is `#teleports`.
91
+
92
+ ### Migration steps
93
+
94
+ Update the `teleportId` value within the Nuxt config to `tide-top-layer`.
95
+
96
+ ``` ts
97
+ // nuxt.config.ts
98
+ export defineNuxtConfig({
99
+ app: {
100
+ teleportId: 'tide-top-layer',
101
+ }
102
+ });
103
+ ```
104
+
105
+ Alternatively you can use the `teleportAttrs` config value.
106
+
107
+ ``` ts
108
+ // nuxt.config.ts
109
+ export defineNuxtConfig({
110
+ app: {
111
+ teleportAttrs: {
112
+ id: 'tide-top-layer',
113
+ },
114
+ }
115
+ });
116
+ ```
117
+
118
+ See [Nuxt Configuration docs](https://nuxt.com/docs/4.x/api/nuxt-config#teleportid).
119
+
120
+ ✅ After these changes your project should be compatible with TIDE 2.4.6.
121
+
86
122
  ## Upgrading from 2.3 → 2.4
87
123
 
88
124
  ### Summary
125
+
89
126
  Version **2.4** introduces a new suite of form components. It remains largely backward-compatible with the previous versions of components, but requires an update to a number of component names. It also removes a number of form-related TS exports.
90
127
 
91
128
  ### Migration steps
package/package.json CHANGED
@@ -63,7 +63,7 @@
63
63
  "main": "dist/tide-design-system.cjs",
64
64
  "module": "dist/tide-design-system.esm.js",
65
65
  "types": "dist/tide-design-system.esm.d.ts",
66
- "version": "2.5.2",
66
+ "version": "2.5.4",
67
67
  "dependencies": {
68
68
  "@floating-ui/vue": "^1.1.6"
69
69
  }
@@ -11,8 +11,17 @@
11
11
  --tide-font-28: 1.75rem;
12
12
  --tide-font-32: 2rem;
13
13
 
14
- /* Animation */
15
- --tide-animate: 300ms ease-in-out;
14
+ /* Motion */
15
+ --tide-duration-75: 75ms;
16
+ --tide-duration-150: 150ms;
17
+ --tide-duration-300: 300ms;
18
+ --tide-duration-600: 600ms;
19
+
20
+ --tide-easing-ease: ease;
21
+ --tide-easing-ease-out: ease-out;
22
+ --tide-easing-linear: linear;
23
+
24
+ --tide-animate: var(--tide-duration-300) var(--tide-easing-ease);
16
25
 
17
26
  /* Spacing */
18
27
  --tide-spacing-0: 0rem;
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue';
3
+
4
+ import { CSS } from '@/types/Styles';
5
+
6
+ const isTouched = ref(false);
7
+ const isSaved = defineModel<boolean>({ required: true });
8
+
9
+ watch(
10
+ isSaved,
11
+ () => {
12
+ if (!isTouched.value) isTouched.value = true;
13
+ },
14
+ { immediate: false }
15
+ );
16
+ </script>
17
+
18
+ <template>
19
+ <button
20
+ :class="['tide-button-save', CSS.PADDING.FULL.QUARTER, isSaved && 'saved']"
21
+ @click="isSaved = !isSaved"
22
+ type="button"
23
+ >
24
+ <div :class="['heart-container', CSS.DISPLAY.FLEX, isSaved && isTouched && 'animate-heartbeat']">
25
+ <svg
26
+ :class="['tide-button-save-icon', CSS.FLEX.GROW.OFF, CSS.FLEX.SHRINK.OFF]"
27
+ height="32"
28
+ viewBox="0 0 24 24"
29
+ width="32"
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ >
32
+ <path
33
+ :class="['heart-filled', isSaved ? CSS.FONT.COLOR.GLOBAL.PRIMARY.RED : 'filled-transparent']"
34
+ d="m12 21-1.45-1.3c-1.6833-1.5167-3.075-2.825-4.175-3.925-1.1-1.1-1.975-2.0875-2.625-2.9625-.65-.875-1.1042-1.6792-1.3625-2.4125C2.1292 9.6667 2 8.9167 2 8.15c0-1.5667.525-2.875 1.575-3.925C4.625 3.175 5.9333 2.65 7.5 2.65c.8667 0 1.6917.1833 2.475.55.7833.3667 1.4583.8833 2.025 1.55.5667-.6667 1.2417-1.1833 2.025-1.55.7833-.3667 1.6083-.55 2.475-.55 1.5667 0 2.875.525 3.925 1.575C21.475 5.275 22 6.5833 22 8.15c0 .7667-.1292 1.5167-.3875 2.25-.2583.7333-.7125 1.5375-1.3625 2.4125s-1.525 1.8625-2.625 2.9625c-1.1 1.1-2.4917 2.4083-4.175 3.925L12 21Z"
35
+ />
36
+
37
+ <path
38
+ :class="['heart-outline', CSS.FONT.COLOR.SURFACE.INVERSE]"
39
+ d="m12 21-1.45-1.3c-1.6833-1.5167-3.075-2.825-4.175-3.925-1.1-1.1-1.975-2.0875-2.625-2.9625-.65-.875-1.1042-1.6792-1.3625-2.4125C2.1292 9.6667 2 8.9167 2 8.15c0-1.5667.525-2.875 1.575-3.925C4.625 3.175 5.9333 2.65 7.5 2.65c.8667 0 1.6917.1833 2.475.55.7833.3667 1.4583.8833 2.025 1.55.5667-.6667 1.2417-1.1833 2.025-1.55.7833-.3667 1.6083-.55 2.475-.55 1.5667 0 2.875.525 3.925 1.575C21.475 5.275 22 6.5833 22 8.15c0 .7667-.1292 1.5167-.3875 2.25-.2583.7333-.7125 1.5375-1.3625 2.4125s-1.525 1.8625-2.625 2.9625c-1.1 1.1-2.4917 2.4083-4.175 3.925L12 21Zm0-2.7c1.6-1.4333 2.9167-2.6625 3.95-3.6875s1.85-1.9167 2.45-2.675c.6-.7583 1.0167-1.4333 1.25-2.025.2333-.5917.35-1.1792.35-1.7625 0-1-.3333-1.8333-1-2.5-.6667-.6667-1.5-1-2.5-1-.7833 0-1.5083.2208-2.175.6625C13.6583 5.7542 13.2 6.3167 12.95 7h-1.9c-.25-.6833-.7083-1.2458-1.375-1.6875S8.2833 4.65 7.5 4.65c-1 0-1.8333.3333-2.5 1-.6667.6667-1 1.5-1 2.5 0 .5833.1167 1.1708.35 1.7625.2333.5917.65 1.2667 1.25 2.025s1.4167 1.65 2.45 2.675S10.4 16.8667 12 18.3Z"
40
+ />
41
+ </svg>
42
+ </div>
43
+ </button>
44
+ </template>
45
+
46
+ <style scoped>
47
+ .heart-container {
48
+ --rotate-during-heart-beat: 9deg;
49
+ filter: drop-shadow(0px 2px 8px rgba(0, 0, 0, 0.3));
50
+ transition: var(--tide-duration-300) var(--tide-easing-ease-out);
51
+ transition-property: transform, scale;
52
+ rotate: 0deg;
53
+ }
54
+
55
+ .heart-container.animate-heartbeat {
56
+ animation: heartBeat var(--tide-duration-150);
57
+ animation-direction: alternate;
58
+ animation-iteration-count: 4;
59
+ }
60
+
61
+ .heart-container:active {
62
+ scale: 0.9;
63
+ transition-duration: var(--tide-duration-75);
64
+ }
65
+
66
+ .tide-button-save-icon {
67
+ fill: currentColor;
68
+ }
69
+
70
+ @media (hover: hover) {
71
+ .heart-container {
72
+ --rotate-during-heart-beat: 0deg;
73
+ }
74
+
75
+ .tide-button-save:where(:hover, :focus-visible) .heart-container {
76
+ transform: rotate(9deg);
77
+ }
78
+ }
79
+
80
+ .heart-filled {
81
+ scale: 0.985; /* avoid hairline border render issue */
82
+ transform-origin: center;
83
+ transition: color var(--tide-duration-75) var(--tide-easing-linear);
84
+ }
85
+
86
+ .filled-transparent {
87
+ color: var(--tide-transparent-400);
88
+ }
89
+
90
+ @keyframes heartBeat {
91
+ from {
92
+ scale: 1;
93
+ rotate: 0deg;
94
+ }
95
+ to {
96
+ scale: 1.25;
97
+ rotate: var(--rotate-during-heart-beat);
98
+ }
99
+ }
100
+ </style>
@@ -34,6 +34,7 @@
34
34
 
35
35
  const emit = defineEmits<Emits>();
36
36
 
37
+ const carouselRef = ref<HTMLDivElement | null>(null);
37
38
  const containerRef = ref<HTMLDivElement | null>(null);
38
39
  const contentWidth = ref<number>(0);
39
40
  const currentPageIndex = ref<number>(0);
@@ -46,26 +47,25 @@
46
47
  const slides = ref<HTMLElement[]>([]);
47
48
  const slidesInView = ref<number[]>([]);
48
49
  const slotObserver = ref<MutationObserver | null>(null);
50
+ const touchStart = ref<Touch | undefined>(undefined);
51
+ const slidesInViewCount = ref<number>(1);
49
52
 
50
53
  const currentPage = computed(() => currentPageIndex.value + 1);
51
54
  const dotContainerWidth = computed(() => dotCountVisible.value * dotWidth + (dotCountVisible.value - 1) * dotGap);
52
55
  const dotCountVisible = computed(() => (props.maxDots > totalPages.value ? totalPages.value : props.maxDots));
53
- const lastPageIndex = computed(() => totalPages.value - 1);
54
56
  const totalPages = computed(() => {
55
57
  if (!slides.value.length) return 0;
56
58
  if (!props.isScrollByPage) return slides.value.length;
57
59
 
58
- const gapWidth = cardGap * (slides.value.length - 1);
59
- const contentNoGap = contentWidth.value - gapWidth;
60
- const quotient = Math.round(contentNoGap / frameWidth.value);
61
- const remainder = contentNoGap % frameWidth.value;
60
+ const quotient = Math.floor(slides.value.length / slidesInViewCount.value);
61
+ const remainder = slides.value.length % slidesInViewCount.value;
62
62
 
63
63
  return remainder ? quotient + 1 : quotient;
64
64
  });
65
65
 
66
- const cardGap: number = 16;
67
66
  const dotGap: number = 8;
68
67
  const dotWidth: number = 8;
68
+ let scrollTimeout: number | undefined;
69
69
 
70
70
  const getDotClass = (dotIndex: number) => {
71
71
  let className = '';
@@ -76,17 +76,64 @@
76
76
  return className;
77
77
  };
78
78
 
79
+ const getIsElementWithinContainer = (element: HTMLElement, container: HTMLElement) => {
80
+ const containerRect = container.getBoundingClientRect();
81
+ const containerRight = containerRect.left + containerRect.width;
82
+ const elementRect = element.getBoundingClientRect();
83
+ const elementRight = elementRect.left + elementRect.width;
84
+
85
+ return elementRight <= containerRight && elementRect.left >= containerRect.left;
86
+ };
87
+
88
+ const handleTouchEnd = (event: TouchEvent) => {
89
+ if (!touchStart.value) return;
90
+
91
+ const touchEnd = event.changedTouches[0];
92
+ const deltaX = touchStart.value.clientX - touchEnd.clientX;
93
+ const deltaY = touchEnd.clientY - touchStart.value.clientY;
94
+
95
+ if (Math.abs(deltaX) > Math.abs(deltaY)) scrollByDelta(deltaX);
96
+ };
97
+
98
+ const handleTouchStart = (event: TouchEvent) => {
99
+ touchStart.value = event.touches[0];
100
+ };
101
+
102
+ const handleWheel = (event: WheelEvent) => {
103
+ const isShiftKeyDown = event.shiftKey;
104
+ const isWheel = Math.abs(event.deltaY) >= 80;
105
+
106
+ if (isWheel && !isShiftKeyDown) return;
107
+ if (event.shiftKey) event.preventDefault();
108
+
109
+ clearTimeout(scrollTimeout);
110
+
111
+ scrollTimeout = window.setTimeout(() => {
112
+ if (isWheel) {
113
+ scrollByDelta(event.deltaY);
114
+ } else {
115
+ const offset = slides.value[slidesInView.value[0]]?.offsetLeft || 0;
116
+
117
+ scrollToOffset(offset);
118
+ }
119
+ }, 100);
120
+ };
121
+
79
122
  const measureDom = () => {
80
123
  if (!containerRef.value) return;
81
124
 
82
125
  contentWidth.value = containerRef.value.scrollWidth;
83
126
  frameWidth.value = containerRef.value.clientWidth;
84
127
  showButtons.value = contentWidth.value > frameWidth.value;
128
+
129
+ slidesInViewCount.value = slides.value.filter((slide) =>
130
+ getIsElementWithinContainer(slide, containerRef.value as HTMLElement)
131
+ ).length;
85
132
  };
86
133
 
87
134
  const observeSlides = () => {
88
135
  const options = {
89
- root: containerRef?.value,
136
+ root: carouselRef?.value,
90
137
  rootMargin: '0px 1px 0px 0px',
91
138
  threshold: 1,
92
139
  };
@@ -131,12 +178,20 @@
131
178
  if (containerRef.value) slotObserver.value.observe(containerRef.value, { childList: true });
132
179
  };
133
180
 
181
+ const scrollByDelta = (delta: number) => {
182
+ const isScrollingLeft = delta < 0;
183
+
184
+ if (isScrollingLeft) {
185
+ props.isScrollByPage ? showPreviousPage() : showPreviousSlide();
186
+ } else {
187
+ props.isScrollByPage ? showNextPage() : showNextSlide();
188
+ }
189
+ };
190
+
134
191
  const scrollToOffset = (target: number) => {
135
192
  if (containerRef.value === null) return;
136
193
 
137
- const lastOffset: number = slides.value[slides.value.length - 1].offsetLeft;
138
- const placement = (target / lastOffset) * lastPageIndex.value;
139
- const isScrollingLeft = placement <= currentPageIndex.value;
194
+ const isScrollingLeft = target <= containerRef.value.scrollLeft;
140
195
 
141
196
  currentPageIndex.value = isScrollingLeft ? currentPageIndex.value - 1 : currentPageIndex.value + 1;
142
197
 
@@ -148,19 +203,21 @@
148
203
  };
149
204
 
150
205
  const showNextPage = () => {
151
- if (slidesInView.value.length === 0) return;
206
+ if (slidesInView.value.length === 0 || isLastSlide.value) return;
152
207
 
153
208
  const nextSlide: number = slidesInView.value[slidesInView.value.length - 1] + 1;
209
+ const offset = slides.value[nextSlide]?.offsetLeft;
154
210
 
155
- scrollToOffset(slides.value[nextSlide].offsetLeft);
211
+ scrollToOffset(offset);
156
212
  };
157
213
 
158
214
  const showPreviousPage = () => {
159
- if (slidesInView.value.length === 0) return;
215
+ if (slidesInView.value.length === 0 || isFirstSlide.value) return;
160
216
 
161
217
  const previousSlide: number = slidesInView.value[0] - slidesInView.value.length;
218
+ const offset = slides.value[previousSlide]?.offsetLeft || 0;
162
219
 
163
- scrollToOffset(slides.value[previousSlide]?.offsetLeft || 0);
220
+ scrollToOffset(offset);
164
221
  };
165
222
 
166
223
  const showNextSlide = () => {
@@ -284,51 +341,58 @@
284
341
  CSS.SNAP.ON,
285
342
  ]"
286
343
  ref="containerRef"
344
+ @touchend="handleTouchEnd"
345
+ @touchstart.passive="handleTouchStart"
346
+ @wheel="handleWheel"
287
347
  >
288
348
  <slot />
289
349
  </ul>
290
350
 
291
351
  <div
292
352
  :class="[
293
- 'tide-carousel-dots-container',
294
353
  CSS.POSITION.ABSOLUTE,
295
354
  CSS.POSITIONING.BOTTOM,
296
355
  CSS.DISPLAY.FLEX,
297
356
  CSS.AXIS1.CENTER,
298
357
  CSS.MARGIN.BOTTOM.HALF,
299
358
  CSS.WIDTH.FULL,
359
+ CSS.POINTER_EVENTS.OFF,
300
360
  ]"
301
- :style="{
302
- width: `${dotContainerWidth}px`,
303
- }"
304
- v-if="isScrollByPage && hasDots && totalPages > 1"
361
+ v-if="hasDots && totalPages > 1"
305
362
  >
306
363
  <div
307
- :class="[
308
- 'tide-carousel-dots',
309
- CSS.DISPLAY.FLEX,
310
- CSS.AXIS1.START,
311
- CSS.AXIS2.CENTER,
312
- CSS.GAP.HALF,
313
- CSS.OVERFLOW.X.SCROLL,
314
- CSS.POINTER_EVENTS.OFF,
315
- CSS.SCROLLBAR.OFF,
316
- ]"
317
- ref="dotsRef"
364
+ :class="['tide-carousel-dots-container']"
365
+ :style="{
366
+ width: `${dotContainerWidth}px`,
367
+ }"
318
368
  >
319
369
  <div
320
370
  :class="[
321
- 'tide-carousel-dot',
322
- CSS.FLEX.SHRINK.OFF,
323
- CSS.HEIGHT.ZERO,
324
- CSS.WIDTH.ZERO,
325
- CSS.BORDER.RADIUS.FULL,
326
- CSS.BG.SURFACE.DEFAULT,
327
- getDotClass(index),
371
+ 'tide-carousel-dots',
372
+ CSS.DISPLAY.FLEX,
373
+ CSS.AXIS1.START,
374
+ CSS.AXIS2.CENTER,
375
+ CSS.GAP.HALF,
376
+ CSS.OVERFLOW.X.SCROLL,
377
+ CSS.POINTER_EVENTS.OFF,
378
+ CSS.SCROLLBAR.OFF,
328
379
  ]"
329
- :key="index"
330
- v-for="(_, index) in totalPages"
331
- />
380
+ ref="dotsRef"
381
+ >
382
+ <div
383
+ :class="[
384
+ 'tide-carousel-dot',
385
+ CSS.FLEX.SHRINK.OFF,
386
+ CSS.HEIGHT.ZERO,
387
+ CSS.WIDTH.ZERO,
388
+ CSS.BORDER.RADIUS.FULL,
389
+ CSS.BG.SURFACE.DEFAULT,
390
+ getDotClass(index),
391
+ ]"
392
+ :key="index"
393
+ v-for="(_, index) in totalPages"
394
+ />
395
+ </div>
332
396
  </div>
333
397
  </div>
334
398
 
@@ -4,16 +4,19 @@
4
4
  import InternalBaseLink from '@/components/InternalBaseLink.vue';
5
5
  import TideIcon from '@/components/TideIcon.vue';
6
6
  import { ELEMENT } from '@/types/Element';
7
+ import { SIZE } from '@/types/Size';
7
8
  import { CSS } from '@/types/Styles';
8
9
  import { TARGET } from '@/types/Target';
9
10
 
10
11
  import type { Element } from '@/types/Element';
11
12
  import type { Icon } from '@/types/Icon';
13
+ import type { Size } from '@/types/Size';
12
14
 
13
15
  type Props = {
14
16
  element?: Element;
15
17
  href?: string;
16
18
  iconLeading?: Icon;
19
+ iconSize?: Size;
17
20
  iconTrailing?: Icon;
18
21
  isNewTab?: boolean;
19
22
  label: string;
@@ -24,6 +27,7 @@
24
27
  element: ELEMENT.LINK,
25
28
  href: undefined,
26
29
  iconLeading: undefined,
30
+ iconSize: SIZE.SMALL,
27
31
  iconTrailing: undefined,
28
32
  isNewTab: false,
29
33
  label: undefined,
@@ -49,6 +53,7 @@
49
53
  <TideIcon
50
54
  :class="[CSS.DISPLAY.INLINE_BLOCK, CSS.ALIGN.Y.MIDDLE, CSS.MARGIN.RIGHT.QUARTER]"
51
55
  :icon="props.iconLeading"
56
+ :size="iconSize"
52
57
  v-if="props.iconLeading"
53
58
  />
54
59
 
@@ -59,6 +64,7 @@
59
64
  <TideIcon
60
65
  :class="[CSS.DISPLAY.INLINE_BLOCK, CSS.ALIGN.Y.MIDDLE, CSS.MARGIN.LEFT.QUARTER]"
61
66
  :icon="props.iconTrailing"
67
+ :size="iconSize"
62
68
  v-if="props.iconTrailing"
63
69
  />
64
70
  </component>
@@ -50,7 +50,7 @@
50
50
  CSS.BORDER.RADIUS.ZERO,
51
51
  CSS.DISPLAY.FLEX,
52
52
  CSS.ELLIPSIS,
53
- CSS.FONT.ROLE.LABEL_2,
53
+ CSS.FONT.ROLE.LABEL_1,
54
54
  CSS.GAP.QUARTER,
55
55
  CSS.PADDING.X.ONE,
56
56
  CSS.PADDING.Y.HALF,
@@ -0,0 +1,62 @@
1
+ import { ref, watch } from 'vue';
2
+
3
+ import TideButtonSave from '@/components/TideButtonSave.vue';
4
+ import { dataTrack, doSomething, parameters } from '@/utilities/storybook';
5
+
6
+ import type { StoryContext } from '@storybook/vue3';
7
+
8
+ type Args = InstanceType<typeof TideButtonSave>['$props'] & {
9
+ vModel: boolean;
10
+ };
11
+
12
+ const render = (args: Args, context: StoryContext) => ({
13
+ components: { TideButtonSave },
14
+ methods: {
15
+ doSomething,
16
+ handleIsSavedChange: (value: boolean) => {
17
+ context.updateArgs({ ...args, vModel: value });
18
+ },
19
+ },
20
+ setup: () => {
21
+ const isSaved = ref(args.vModel);
22
+
23
+ watch(
24
+ () => args.vModel,
25
+ (newValue) => {
26
+ isSaved.value = newValue;
27
+ }
28
+ );
29
+
30
+ return {
31
+ args,
32
+ isSaved,
33
+ };
34
+ },
35
+ template: `<TideButtonSave v-bind="args" v-model="isSaved" @update:modelValue="handleIsSavedChange" />`,
36
+ });
37
+
38
+ export default {
39
+ argTypes: {
40
+ dataTrack,
41
+ vModel: {
42
+ control: 'boolean',
43
+ description: 'Data binding to Vue ref',
44
+ table: {
45
+ category: 'Native',
46
+ defaultValue: { summary: 'None' },
47
+ type: { summary: 'Ref<boolean>' },
48
+ },
49
+ },
50
+ },
51
+ args: {
52
+ dataTrack: '',
53
+ vModel: false,
54
+ },
55
+ component: TideButtonSave,
56
+ parameters,
57
+ render,
58
+ tags: ['autodocs'],
59
+ title: 'Components/TideButtonSave',
60
+ };
61
+
62
+ export const Demo = {};
@@ -1,4 +1,5 @@
1
1
  import { action } from '@storybook/addon-actions';
2
+ import { computed } from 'vue';
2
3
 
3
4
  import TideCard from '@/components/TideCard.vue';
4
5
  import TideCarousel from '@/components/TideCarousel.vue';
@@ -8,6 +9,8 @@ import type { StoryContext } from '@storybook/vue3';
8
9
 
9
10
  type Args = InstanceType<typeof TideCarousel>['$props'] & {
10
11
  handleSlidesAddedToView: string;
12
+ isFullWidthCards: boolean | undefined;
13
+ maxDots: string | undefined;
11
14
  };
12
15
 
13
16
  const formatSnippet = (code: string, context: StoryContext) => {
@@ -19,7 +22,6 @@ const formatSnippet = (code: string, context: StoryContext) => {
19
22
  if (args.isFloating !== undefined) argsWithValues.push(`:is-floating="${args.isFloating}"`);
20
23
  if (args.isHideawayButtons !== undefined) argsWithValues.push(`:is-hideaway-buttons="${args.isHideawayButtons}"`);
21
24
  if (args.isScrollByPage !== undefined) argsWithValues.push(`:is-scroll-by-page="false"`);
22
- if (args.isTouchscreen !== undefined) argsWithValues.push(`:is-touchscreen="${args.isTouchscreen}"`);
23
25
  if (args.maxDots !== '') argsWithValues.push(`max-dots="${args.maxDots}"`);
24
26
  if (args.subtitle !== '') argsWithValues.push(`subtitle="${args.subtitle}"`);
25
27
  if (args.title !== '') argsWithValues.push(`title="${args.title}"`);
@@ -31,7 +33,7 @@ const formatSnippet = (code: string, context: StoryContext) => {
31
33
  // prettier-ignore
32
34
  `<TideCarousel ${argsWithValues.join(' ')}>` + lineBreak +
33
35
  slotContentMisc + tab +
34
- `<li class="tide-shrink-none" v-for="(_child, index) in new Array(12)">` + lineBreak + tab + tab +
36
+ `<li class="tide-shrink-none${args.isFullWidthCards ? ' tide-width-full' : ''}" v-for="(_child, index) in new Array(12)">` + lineBreak + tab + tab +
35
37
  args.default + lineBreak + tab +
36
38
  `</li>` + lineBreak +
37
39
  `</TideCarousel>`
@@ -66,19 +68,31 @@ const render = (args: Args) => ({
66
68
  }
67
69
  },
68
70
  },
69
- setup: () => ({ args }),
71
+ setup: () => {
72
+ const conditionalFullWidth = computed(() => (args.isFullWidthCards ? ' tide-width-full' : ''));
73
+ const argsFormatted = computed(() => ({
74
+ ...args,
75
+ maxDots: args.maxDots === '' ? undefined : args.maxDots,
76
+ }));
77
+
78
+ return {
79
+ argsFormatted,
80
+ conditionalFullWidth,
81
+ };
82
+ },
70
83
  // prettier-ignore
71
84
  template:
72
85
  `<TideCarousel
73
86
  @slides-added-to-view="handleSlidesAddedToView"
74
- v-bind="args"
87
+ v-bind="argsFormatted"
88
+ :key="argsFormatted.isFullWidthCards"
75
89
  >
76
- <template #misc>{{ args.misc }}</template>
90
+ <template #misc>{{ argsFormatted.misc }}</template>
77
91
  <li
78
- class="tide-shrink-none tide-border-1 sb-border-blue tide-padding-1 sb-bg-blue-light"
92
+ :class="['tide-shrink-none tide-border-1 sb-border-blue tide-padding-1 sb-bg-blue-light', conditionalFullWidth]"
79
93
  v-for="(_child, index) in new Array(12)"
80
94
  >
81
- {{ args.default.replace('#', index) }}
95
+ {{ argsFormatted.default.replace('#', index) }}
82
96
  </li>
83
97
  </TideCarousel>`,
84
98
  });
@@ -106,7 +120,7 @@ export default {
106
120
  },
107
121
  hasDots: {
108
122
  ...argTypeBooleanUnrequired,
109
- description: 'Determines whether to display the indicator dots overlay',
123
+ description: 'Determines whether to display the indicator dots overlay<br />(Only valid with full width cards)',
110
124
  table: {
111
125
  defaultValue: { summary: 'False' },
112
126
  },
@@ -118,6 +132,16 @@ export default {
118
132
  defaultValue: { summary: 'False' },
119
133
  },
120
134
  },
135
+ isFullWidthCards: {
136
+ ...argTypeBooleanUnrequired,
137
+ description: 'Preview 1 card per page implementation',
138
+ table: {
139
+ category: 'Demo',
140
+ defaultValue: { summary: 'None' },
141
+ disable: true,
142
+ type: { summary: 'boolean' },
143
+ },
144
+ },
121
145
  isHeadline1: {
122
146
  ...argTypeBooleanUnrequired,
123
147
  description: 'Determines font role used for title display',
@@ -133,23 +157,11 @@ export default {
133
157
  defaultValue: { summary: 'True' },
134
158
  },
135
159
  },
136
- isScrollByPage: {
137
- ...argTypeBooleanUnrequired,
138
- description: 'Determines pagination method',
139
- table: {
140
- defaultValue: { summary: 'None' },
141
- },
142
- },
143
- isTouchscreen: {
144
- ...argTypeBooleanUnrequired,
145
- description: 'Determines button and/or swipe control scheme',
146
- table: {
147
- defaultValue: { summary: 'None' },
148
- },
149
- },
160
+ isScrollByPage: disabledArgType,
150
161
  maxDots: {
151
162
  control: 'text',
152
163
  description: 'Determines the max number of indicator dots to display at a given time',
164
+ if: { arg: 'hasDots', eq: true },
153
165
  table: {
154
166
  defaultValue: { summary: 'None' },
155
167
  type: { summary: 'number' },
@@ -186,9 +198,8 @@ export default {
186
198
  handleSlidesAddedToView: 'doSomething',
187
199
  hasDots: undefined,
188
200
  isFloating: undefined,
201
+ isFullWidthCards: undefined,
189
202
  isHideawayButtons: undefined,
190
- isScrollByPage: undefined,
191
- isTouchscreen: undefined,
192
203
  maxDots: '',
193
204
  misc: '',
194
205
  subtitle: '',
@@ -201,4 +212,15 @@ export default {
201
212
  title: 'Components/TideCarousel',
202
213
  };
203
214
 
204
- export const Demo = {};
215
+ export const Demo = {
216
+ name: 'Default',
217
+ };
218
+
219
+ export const FullWidth = {
220
+ args: {
221
+ default: 'Full-width card #',
222
+ isFullWidthCards: true,
223
+ title: 'Demo',
224
+ },
225
+ name: 'Full-width cards ',
226
+ };