tide-design-system 2.4.7 → 2.5.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.
Files changed (50) hide show
  1. package/.storybook/main.ts +2 -0
  2. package/README.md +3 -1
  3. package/dist/css/reset.css +5 -1
  4. package/dist/css/utilities-base.css +6 -6
  5. package/dist/css/utilities-responsive.css +24 -24
  6. package/dist/css/variables.css +3 -0
  7. package/dist/style.css +1 -1
  8. package/dist/tide-design-system.cjs +2 -2
  9. package/dist/tide-design-system.esm.d.ts +152 -70
  10. package/dist/tide-design-system.esm.js +1375 -1243
  11. package/docs/assets/full-bleed.gif +0 -0
  12. package/docs/assets/layout-grid-default.webp +0 -0
  13. package/docs/assets/layout-grid-fluid.webp +0 -0
  14. package/docs/assets/layout-grid.webp +0 -0
  15. package/docs/configuation.md +47 -0
  16. package/docs/grid-layout.md +83 -0
  17. package/docs/upgrading.md +79 -0
  18. package/index.ts +4 -0
  19. package/package.json +1 -1
  20. package/src/assets/css/reset.css +5 -1
  21. package/src/assets/css/utilities-base.css +6 -6
  22. package/src/assets/css/utilities-responsive.css +24 -24
  23. package/src/assets/css/variables.css +3 -0
  24. package/src/components/TideAccordionItem.vue +21 -13
  25. package/src/components/TideAlert.vue +1 -1
  26. package/src/components/TideButtonSegmented.vue +14 -15
  27. package/src/components/TideChipFilter.vue +13 -7
  28. package/src/components/TideInputCheckbox.vue +0 -1
  29. package/src/components/TideInputSelect.vue +1 -1
  30. package/src/components/TideInputSelectDeprecated.vue +1 -1
  31. package/src/components/TideInputText.vue +2 -2
  32. package/src/components/TideInputTextDeprecated.vue +2 -2
  33. package/src/components/TideInputTextarea.vue +2 -2
  34. package/src/components/TideInputTextareaDeprecated.vue +2 -2
  35. package/src/components/TideModal.vue +33 -20
  36. package/src/components/TidePagination.vue +17 -19
  37. package/src/components/TideRating.vue +93 -0
  38. package/src/components/TideSheet.vue +24 -21
  39. package/src/components/TideSwitch.vue +23 -20
  40. package/src/components/TideTabs.vue +58 -0
  41. package/src/stories/TideAccordionItem.stories.ts +33 -34
  42. package/src/stories/TideButtonSegmented.stories.ts +33 -25
  43. package/src/stories/TideChipFilter.stories.ts +33 -23
  44. package/src/stories/TideInputCheckbox.stories.ts +18 -10
  45. package/src/stories/TideModal.stories.ts +33 -37
  46. package/src/stories/TidePagination.stories.ts +31 -20
  47. package/src/stories/TideRating.stories.ts +120 -0
  48. package/src/stories/TideSheet.stories.ts +33 -28
  49. package/src/stories/TideSwitch.stories.ts +33 -34
  50. package/src/stories/TideTabs.stories.ts +115 -0
@@ -189,7 +189,7 @@
189
189
 
190
190
  .tide-input-textarea:focus-within .tide-input-textarea-field {
191
191
  --tide-input-outline-width: var(--tide-border-width-2);
192
- outline-color: var(--tide-surface-border-high);
192
+ outline-color: var(--tide-border-high);
193
193
  }
194
194
 
195
195
  .tide-input-textarea textarea {
@@ -200,6 +200,6 @@
200
200
  --tide-input-outline-width: var(--tide-border-width-1);
201
201
  outline: var(--tide-input-outline-width) solid var(--tide-border);
202
202
  outline-offset: calc(var(--tide-input-outline-width) * -1);
203
- color: var(--tide-surface-foreground);
203
+ color: var(--tide-on-surface);
204
204
  }
205
205
  </style>
@@ -182,7 +182,7 @@
182
182
 
183
183
  .tide-input-textarea:focus-within .tide-input-textarea-field {
184
184
  --tide-input-outline-width: var(--tide-border-width-2);
185
- outline-color: var(--tide-surface-border-high);
185
+ outline-color: var(--tide-border-high);
186
186
  }
187
187
 
188
188
  .tide-input-textarea textarea {
@@ -193,6 +193,6 @@
193
193
  --tide-input-outline-width: var(--tide-border-width-1);
194
194
  outline: var(--tide-input-outline-width) solid var(--tide-border);
195
195
  outline-offset: calc(var(--tide-input-outline-width) * -1);
196
- color: var(--tide-surface-foreground);
196
+ color: var(--tide-on-surface);
197
197
  }
198
198
  </style>
@@ -11,14 +11,20 @@
11
11
  import type { Ref } from 'vue';
12
12
 
13
13
  type Props = {
14
+ /**
15
+ * Called before the modal closes.
16
+ *
17
+ * Return `false` to cancel the close event.
18
+ */
19
+ beforeClose?: () => void | boolean | Promise<void | boolean>;
14
20
  isBackButton?: boolean;
15
21
  isDismissible?: boolean;
16
- isOpen: boolean;
17
22
  title?: string;
18
23
  width?: string;
19
24
  };
20
25
 
21
26
  const props = withDefaults(defineProps<Props>(), {
27
+ beforeClose: undefined,
22
28
  isBackButton: false,
23
29
  isDismissible: true,
24
30
  title: undefined,
@@ -26,7 +32,6 @@
26
32
  });
27
33
 
28
34
  type Emits = {
29
- (e: 'close'): void;
30
35
  (e: 'back'): void;
31
36
  };
32
37
 
@@ -35,6 +40,8 @@
35
40
  const modalContent: Ref<HTMLDivElement | undefined> = ref();
36
41
  const modalDialog: Ref<HTMLDialogElement | undefined> = ref();
37
42
 
43
+ const isOpen = defineModel<boolean>({ required: true });
44
+
38
45
  const triggerNativeDialogOpen = () => {
39
46
  modalDialog.value?.showModal();
40
47
  };
@@ -50,35 +57,41 @@
50
57
  });
51
58
  };
52
59
 
60
+ const close = async () => {
61
+ if (props.beforeClose) {
62
+ const result = await props.beforeClose();
63
+ if (result === false) return;
64
+ }
65
+
66
+ isOpen.value = false;
67
+ };
68
+
53
69
  const handleBackdropClick = () => {
54
- if (props.isDismissible) emit('close');
70
+ if (props.isDismissible) close();
55
71
  };
56
72
 
57
73
  const handleEscapeKeydown = (e: Event) => {
58
74
  e.preventDefault();
59
- if (props.isDismissible) emit('close');
75
+ if (props.isDismissible) close();
60
76
  };
61
77
 
62
- watch(
63
- () => props.isOpen,
64
- (newValue) => {
65
- if (!modalDialog.value) return;
66
- if (newValue) {
67
- triggerNativeDialogOpen();
68
- scrollContentToTop();
69
- } else {
70
- triggerNativeDialogClose();
71
- }
72
- setScrollLock(newValue);
78
+ watch(isOpen, () => {
79
+ if (!modalDialog.value) return;
80
+ if (isOpen.value) {
81
+ triggerNativeDialogOpen();
82
+ scrollContentToTop();
83
+ } else {
84
+ triggerNativeDialogClose();
73
85
  }
74
- );
86
+ setScrollLock(isOpen.value);
87
+ });
75
88
 
76
89
  onBeforeMount(() => {
77
90
  initFauxTopLayer();
78
91
  });
79
92
 
80
93
  onMounted(() => {
81
- if (props.isOpen) {
94
+ if (isOpen.value) {
82
95
  triggerNativeDialogOpen();
83
96
  }
84
97
  });
@@ -89,7 +102,7 @@
89
102
  <dialog
90
103
  :class="['tide-modal', CSS.BG.INITIAL, CSS.HEIGHT.FULL, CSS.WIDTH.FULL, CSS.OVERFLOW.XY.HIDDEN]"
91
104
  ref="modalDialog"
92
- :style="{ '--modal-width': props.width }"
105
+ :style="{ '--modal-width': width }"
93
106
  @click.self="handleBackdropClick"
94
107
  @close.prevent
95
108
  @keydown.escape="handleEscapeKeydown"
@@ -141,7 +154,7 @@
141
154
  :class="[CSS.FLEX.GROW.OFF, CSS.FLEX.SHRINK.OFF, CSS.MARGIN.LEFT.AUTO]"
142
155
  :icon="ICON.CLOSE"
143
156
  :priority="PRIORITY.QUATERNARY"
144
- @click="emit('close')"
157
+ @click="close"
145
158
  v-if="isDismissible"
146
159
  />
147
160
  </header>
@@ -223,7 +236,7 @@
223
236
  padding-inline: var(--modal-padding-x);
224
237
  }
225
238
  .tide-modal-content {
226
- grid-template-columns: var(--modal-padding-x) 1fr var(--modal-padding-x);
239
+ grid-template-columns: var(--modal-padding-x) var(--tide-safe-fr) var(--modal-padding-x);
227
240
  }
228
241
  :where(.tide-modal-content):deep(> :where(*)) {
229
242
  grid-column: 2;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { computed, ref } from 'vue';
2
+ import { computed } from 'vue';
3
3
 
4
4
  import TideButtonIcon from '@/components/TideButtonIcon.vue';
5
5
  import TideButtonPagination from '@/components/TideButtonPagination.vue';
@@ -8,25 +8,25 @@
8
8
  import { CSS } from '@/types/Styles';
9
9
 
10
10
  type Props = {
11
- pageCurrent?: number;
12
- pageTotal?: number;
11
+ pageTotal: number;
13
12
  };
14
13
 
15
- const props = withDefaults(defineProps<Props>(), {
16
- pageCurrent: 1,
17
- pageTotal: 1,
18
- });
14
+ const props = defineProps<Props>();
19
15
 
20
- const emit = defineEmits(['change']);
21
-
22
- const pageCurrent = ref(props.pageCurrent);
16
+ const currentIndex = defineModel<number>({ required: true });
23
17
 
24
18
  const paginationButtons = computed(() => new Array(props.pageTotal).fill('').map((empty, index) => index + 1));
25
19
 
26
20
  const handleClick = (index: number) => {
27
- pageCurrent.value = index;
21
+ currentIndex.value = index;
22
+ };
28
23
 
29
- emit('change', event, index);
24
+ const handlePreviousClick = () => {
25
+ currentIndex.value--;
26
+ };
27
+
28
+ const handleNextClick = () => {
29
+ currentIndex.value++;
30
30
  };
31
31
  </script>
32
32
 
@@ -35,10 +35,10 @@
35
35
  :class="['tide-pagination', CSS.DISPLAY.FLEX, CSS.AXIS1.CENTER, CSS.AXIS2.CENTER, CSS.GAP.QUARTER, CSS.WIDTH.FULL]"
36
36
  >
37
37
  <TideButtonIcon
38
- :disabled="pageCurrent === 1"
38
+ :disabled="currentIndex <= 1"
39
39
  :icon="ICON.CHEVRON_LEFT"
40
40
  :priority="PRIORITY.QUATERNARY"
41
- @click="handleClick(pageCurrent - 1)"
41
+ @click="handlePreviousClick"
42
42
  />
43
43
 
44
44
  <ul :class="[CSS.DISPLAY.FLEX, CSS.AXIS2.CENTER, CSS.GAP.QUARTER, CSS.LIST_BULLETS.OFF]">
@@ -47,7 +47,7 @@
47
47
  v-for="paginationButton in paginationButtons"
48
48
  >
49
49
  <TideButtonPagination
50
- :disabled="pageCurrent === paginationButton"
50
+ :disabled="currentIndex === paginationButton"
51
51
  :label="paginationButton"
52
52
  :priority="PRIORITY.QUATERNARY"
53
53
  @click="handleClick(paginationButton)"
@@ -56,12 +56,10 @@
56
56
  </ul>
57
57
 
58
58
  <TideButtonIcon
59
- :disabled="pageCurrent === paginationButtons[paginationButtons.length - 1]"
59
+ :disabled="currentIndex >= paginationButtons[paginationButtons.length - 1]"
60
60
  :icon="ICON.CHEVRON_RIGHT"
61
61
  :priority="PRIORITY.QUATERNARY"
62
- @click="handleClick(pageCurrent + 1)"
62
+ @click="handleNextClick"
63
63
  />
64
64
  </section>
65
65
  </template>
66
-
67
- <style scoped></style>
@@ -0,0 +1,93 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue';
3
+
4
+ import { CSS } from '@/types/Styles';
5
+
6
+ type Props = {
7
+ description?: string;
8
+ maxRating?: number;
9
+ showRating?: boolean;
10
+ title?: string;
11
+ };
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ description: undefined,
15
+ maxRating: 10,
16
+ showRating: false,
17
+ title: undefined,
18
+ });
19
+
20
+ const ratingValue = defineModel<number>({ required: true });
21
+
22
+ const segments = computed(() => Array.from({ length: props.maxRating }, (_, index) => index + 1));
23
+
24
+ const handleClick = (segment: number) => {
25
+ ratingValue.value = segment;
26
+ };
27
+ </script>
28
+
29
+ <template>
30
+ <div :class="['tide-rating', CSS.WIDTH.FULL]">
31
+ <section :class="[CSS.DISPLAY.FLEX, CSS.AXIS2.CENTER, CSS.AXIS1.BETWEEN, CSS.PADDING.BOTTOM.HALF]">
32
+ <div :class="[CSS.FONT.ROLE.LABEL_2]">
33
+ {{ props.title }}
34
+ </div>
35
+ <div
36
+ :class="[CSS.FONT.ROLE.LABEL_3, CSS.FONT.COLOR.SURFACE.VARIANT]"
37
+ v-if="props.showRating"
38
+ >
39
+ {{ ratingValue }}/{{ props.maxRating }}
40
+ </div>
41
+ </section>
42
+ <section
43
+ :class="[CSS.FONT.ROLE.BODY_2, CSS.PADDING.BOTTOM.ONE, CSS.OVERFLOW.XY.HIDDEN, CSS.ELLIPSIS]"
44
+ v-if="props.description"
45
+ >
46
+ {{ props.description }}
47
+ </section>
48
+
49
+ <section :class="['tide-rating-bar', CSS.DISPLAY.FLEX, CSS.GAP.QUARTER, CSS.FLEX.GROW.ON, CSS.FLEX.SHRINK.ON]">
50
+ <button
51
+ :class="[
52
+ 'tide-rating-segment',
53
+ CSS.BG.SURFACE.VARIANT,
54
+ CSS.POSITION.RELATIVE,
55
+ CSS.OVERFLOW.XY.HIDDEN,
56
+ CSS.FLEX.GROW.ON,
57
+ CSS.FLEX.SHRINK.ON,
58
+ CSS.FLEX.BASIS.ZERO,
59
+ CSS.BORDER.FULL.ZERO,
60
+ segment === 1 && ['tide-rating-segment-first'],
61
+ segment === segments.length && ['tide-rating-segment-last'],
62
+ segment <= ratingValue && [CSS.BG.SURFACE.GRADIENT],
63
+ ]"
64
+ :key="segment"
65
+ @click="handleClick(segment)"
66
+ type="button"
67
+ v-for="segment in segments"
68
+ />
69
+ </section>
70
+ </div>
71
+ </template>
72
+
73
+ <style scoped>
74
+ .tide-rating-bar {
75
+ height: 28px;
76
+ }
77
+
78
+ .tide-rating-segment {
79
+ clip-path: polygon(12% 0, 100% 0, 88% 100%, 0 100%);
80
+ }
81
+
82
+ .tide-rating-segment-first {
83
+ border-top-left-radius: 9999px;
84
+ border-bottom-left-radius: 9999px;
85
+ clip-path: polygon(0 0, 100% 0, 88% 100%, 0 100%);
86
+ }
87
+
88
+ .tide-rating-segment-last {
89
+ border-top-right-radius: 9999px;
90
+ border-bottom-right-radius: 9999px;
91
+ clip-path: polygon(12% 0, 100% 0, 100% 100%, 0 100%);
92
+ }
93
+ </style>
@@ -2,30 +2,32 @@
2
2
  import { onBeforeMount, onMounted, ref, watch } from 'vue';
3
3
 
4
4
  import TideButtonIcon from '@/components/TideButtonIcon.vue';
5
+ import TideDivider from '@/components/TideDivider.vue';
5
6
  import { ICON } from '@/types/Icon';
6
7
  import { PRIORITY } from '@/types/Priority';
7
8
  import { CSS } from '@/types/Styles';
8
9
  import { TOP_LAYER_ID, initFauxTopLayer, setScrollLock } from '@/utilities/viewport';
9
10
 
10
- import TideDivider from './TideDivider.vue';
11
-
12
11
  import type { Ref } from 'vue';
13
12
 
14
13
  type Props = {
15
- isOpen: boolean;
16
14
  isBackButton?: boolean;
17
15
  };
18
- const props = defineProps<Props>();
16
+
17
+ withDefaults(defineProps<Props>(), {
18
+ isBackButton: false,
19
+ });
19
20
 
20
21
  type Emits = {
21
22
  (e: 'back'): void;
22
- (e: 'close'): void;
23
23
  };
24
24
 
25
25
  const emit = defineEmits<Emits>();
26
26
 
27
27
  const dialogElement: Ref<HTMLDialogElement | null> = ref(null);
28
28
 
29
+ const isOpen = defineModel<boolean>({ required: true });
30
+
29
31
  const triggerNativeDialogOpen = () => {
30
32
  dialogElement.value?.showModal();
31
33
  };
@@ -34,34 +36,35 @@
34
36
  dialogElement.value?.close();
35
37
  };
36
38
 
39
+ const close = () => {
40
+ isOpen.value = false;
41
+ };
42
+
37
43
  const handleBackdropClick = () => {
38
- emit('close');
44
+ close();
39
45
  };
40
46
 
41
47
  const handleEscapeKeydown = (e: Event) => {
42
48
  e.preventDefault();
43
- emit('close');
49
+ close();
44
50
  };
45
51
 
46
- watch(
47
- () => props.isOpen,
48
- (newValue) => {
49
- if (!dialogElement.value) return;
50
- if (newValue) {
51
- triggerNativeDialogOpen();
52
- } else {
53
- triggerNativeDialogClose();
54
- }
55
- setScrollLock(newValue);
52
+ watch(isOpen, () => {
53
+ if (!dialogElement.value) return;
54
+ if (isOpen.value) {
55
+ triggerNativeDialogOpen();
56
+ } else {
57
+ triggerNativeDialogClose();
56
58
  }
57
- );
59
+ setScrollLock(isOpen.value);
60
+ });
58
61
 
59
62
  onBeforeMount(() => {
60
63
  initFauxTopLayer();
61
64
  });
62
65
 
63
66
  onMounted(() => {
64
- if (props.isOpen) {
67
+ if (isOpen.value) {
65
68
  triggerNativeDialogOpen();
66
69
  }
67
70
  });
@@ -109,7 +112,7 @@
109
112
  :class="[CSS.FLEX.GROW.OFF, CSS.FLEX.SHRINK.OFF, CSS.MARGIN.LEFT.AUTO]"
110
113
  :icon="ICON.CLOSE"
111
114
  :priority="PRIORITY.QUATERNARY"
112
- @click="emit('close')"
115
+ @click="close"
113
116
  />
114
117
  </header>
115
118
 
@@ -167,7 +170,7 @@
167
170
  }
168
171
 
169
172
  .tide-sheet-content {
170
- grid-template-columns: var(--sheet-padding-x) 1fr var(--sheet-padding-x);
173
+ grid-template-columns: var(--sheet-padding-x) var(--tide-safe-fr) var(--sheet-padding-x);
171
174
  }
172
175
 
173
176
  :where(.tide-sheet-content):deep(> :where(*)) {
@@ -6,19 +6,17 @@
6
6
 
7
7
  type Props = {
8
8
  disabled?: boolean;
9
- isActive?: boolean;
10
9
  };
11
10
 
12
11
  const props = withDefaults(defineProps<Props>(), {
13
12
  disabled: false,
14
- isActive: false,
15
13
  });
16
14
 
17
- const emit = defineEmits(['change']);
15
+ const isActive = defineModel<boolean>({ required: true });
18
16
 
19
17
  const handleClick = () => {
20
18
  if (!props.disabled) {
21
- emit('change');
19
+ isActive.value = !isActive.value;
22
20
  }
23
21
  };
24
22
  </script>
@@ -27,14 +25,15 @@
27
25
  <button
28
26
  :class="[
29
27
  'tide-switch',
30
- props.isActive ? 'active' : '',
31
28
  CSS.DISPLAY.FLEX,
32
29
  CSS.AXIS2.CENTER,
33
30
  CSS.BORDER.FULL.ONE,
34
31
  CSS.BORDER.RADIUS.FULL,
35
- props.isActive ? '' : CSS.BORDER.COLOR.DEFAULT,
36
32
  CSS.OVERFLOW.XY.HIDDEN,
37
- props.isActive ? CSS.BG.SECONDARY : CSS.BG.SURFACE.DEFAULT,
33
+ CSS.FLEX.SHRINK.OFF,
34
+ CSS.FLEX.GROW.OFF,
35
+ isActive ? ['active', CSS.BG.SECONDARY] : [CSS.BG.SURFACE.DEFAULT, CSS.BORDER.COLOR.DEFAULT],
36
+ disabled && [CSS.OPACITY.DISABLED],
38
37
  ]"
39
38
  :disabled="props.disabled"
40
39
  @click="handleClick"
@@ -49,7 +48,7 @@
49
48
  CSS.AXIS1.CENTER,
50
49
  CSS.AXIS2.CENTER,
51
50
  CSS.BORDER.RADIUS.FULL,
52
- props.isActive ? CSS.BG.SURFACE.DEFAULT : CSS.BG.SECONDARY,
51
+ isActive ? ['active', CSS.BG.SURFACE.DEFAULT] : [CSS.BG.SECONDARY],
53
52
  ]"
54
53
  >
55
54
  <TideIcon
@@ -62,27 +61,31 @@
62
61
 
63
62
  <style scoped>
64
63
  .tide-switch {
65
- width: 64px;
66
- height: 32px;
67
- transition: var(--tide-animate);
68
- transition-property: background-color;
69
- }
64
+ --switch-indicator-size: 24px;
65
+ --switch-indicator-margin: var(--tide-spacing-1\/4);
66
+ --switch-border-width: var(--tide-border-width-1);
67
+ --switch-width: calc(var(--switch-indicator-size) * 2 + var(--switch-indicator-margin) * 4);
68
+ --switch-height: calc(var(--switch-indicator-size) + var(--switch-indicator-margin) * 2);
70
69
 
71
- .tide-switch:disabled {
72
- opacity: var(--tide-opacity);
70
+ width: var(--switch-width);
71
+ height: var(--switch-height);
72
+ transition: var(--tide-animate);
73
+ transition-property: background-color, border-color;
73
74
  }
74
75
 
75
76
  .tide-switch.active {
76
77
  border-color: var(--tide-secondary);
77
78
  }
78
79
 
79
- .tide-switch.active .tide-switch-indicator {
80
- left: 36px;
81
- }
82
-
83
80
  .tide-switch-indicator {
84
- left: 4px;
81
+ left: var(--switch-indicator-margin);
85
82
  transition: var(--tide-animate);
86
83
  transition-property: left, border-color, background-color;
87
84
  }
85
+
86
+ .tide-switch-indicator.active {
87
+ left: calc(
88
+ var(--switch-width) - var(--switch-indicator-size) - var(--switch-indicator-margin) - var(--switch-border-width)
89
+ );
90
+ }
88
91
  </style>
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import TideCarousel from '@/components/TideCarousel.vue';
3
+ import TideLink from '@/components/TideLink.vue';
4
+ import { ELEMENT } from '@/types/Element';
5
+ import { CSS } from '@/types/Styles';
6
+
7
+ import type { Tab } from '@/types/Tab';
8
+
9
+ type Props = {
10
+ tabs: Tab[];
11
+ };
12
+
13
+ defineProps<Props>();
14
+
15
+ const currentTab = defineModel<number>({ required: true });
16
+
17
+ const handleClick = (index: number) => {
18
+ currentTab.value = index;
19
+ };
20
+ </script>
21
+
22
+ <template>
23
+ <div :class="['tide-tabs', CSS.DISPLAY.FLEX, CSS.BORDER.BOTTOM.TWO, CSS.BORDER.COLOR.LOW]">
24
+ <TideCarousel :is-floating="true">
25
+ <li
26
+ :key="tab.label"
27
+ v-for="(tab, index) in tabs"
28
+ >
29
+ <TideLink
30
+ :class="[
31
+ index === currentTab ? CSS.FONT.COLOR.SURFACE.BRAND : CSS.FONT.COLOR.SURFACE.VARIANT,
32
+ CSS.PADDING.BOTTOM.ONE,
33
+ CSS.FONT.ROLE.LABEL_1,
34
+ CSS.WHITESPACE_WRAP.OFF,
35
+ ]"
36
+ :element="ELEMENT.BUTTON"
37
+ :label="tab.label"
38
+ :subtle="true"
39
+ @click="handleClick(index)"
40
+ />
41
+ <div :class="['rounded-border', index === currentTab && 'active']" />
42
+ </li>
43
+ </TideCarousel>
44
+ </div>
45
+ </template>
46
+
47
+ <style scoped>
48
+ .rounded-border {
49
+ border-radius: var(--tide-radius-full) var(--tide-radius-full) 0 0;
50
+ border-top-width: 4px;
51
+ border-top-style: solid;
52
+ border-color: transparent;
53
+ }
54
+
55
+ .active {
56
+ border-color: var(--tide-on-surface-brand);
57
+ }
58
+ </style>