tide-design-system 2.4.7 → 2.5.0

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/docs/upgrading.md CHANGED
@@ -4,6 +4,85 @@ This document provides step-by-step instructions for upgrading from previous ver
4
4
 
5
5
  ---
6
6
 
7
+ ## Upgrading from 2.4 → 2.5
8
+
9
+ ### What changed?
10
+
11
+ TIDE **2.5** updates several *stateful* components to use Vue’s `v-model` for two-way binding. This replaces each component’s old “value prop + change emit” pattern.
12
+
13
+ Affected components:
14
+
15
+ - `TideAccordionItem`
16
+ - `TideButtonSegmented`
17
+ - `TideChipFilter`
18
+ - `TideModal`
19
+ - `TideSheet`
20
+ - `TideSwitch`
21
+
22
+ ### How to migrate
23
+
24
+ #### 1. Replace old props with `v-model`
25
+
26
+ | Component | v2.4 prop | v2.5+ |
27
+ |---|---|---|
28
+ | TideAccordionItem | `:is-expanded` | `v-model` |
29
+ | TideButtonSegmented | `:active-tab` | `v-model` |
30
+ | TideChipFilter | `:is-active` | `v-model` |
31
+ | TideModal | `:is-open` | `v-model` |
32
+ | TideSheet | `:is-open` | `v-model` |
33
+ | TideSwitch | `:is-active` | `v-model` |
34
+
35
+ ##### 2.4 example
36
+
37
+ ```
38
+ <TideButtonSegmented
39
+ :active-tab="active"
40
+ @change="active = $event"
41
+ />
42
+ ```
43
+
44
+ ##### 2.5+
45
+
46
+ ``` vue
47
+ <TideButtonSegmented v-model="active" />
48
+ ```
49
+
50
+ #### 2. Replace old emits with `@update:modelValue`
51
+
52
+ | Component | Old emit | New emit |
53
+ |---|---|---|
54
+ | TideAccordionItem | `@toggle` | `@update:modelValue` |
55
+ | TideButtonSegmented | `@change` | `@update:modelValue` |
56
+ | TideChipFilter | `@click` | `@update:modelValue` |
57
+ | TideModal | `@close` | `@update:modelValue` |
58
+ | TideSheet | `@close` | `@update:modelValue` |
59
+ | TideSwitch | `@change` | `@update:modelValue` |
60
+
61
+ Use this only for **side effects**, not basic value updates.
62
+
63
+ Or use Vue’s `watch()`:
64
+
65
+ ``` ts
66
+ watch(active, doSomething);
67
+ ```
68
+
69
+ ### Handling cancellable modals (new API)
70
+
71
+ **2.4** used `@close` + manual logic.
72
+
73
+ **2.5+** introduces `:before-close`, which cancels dismissal if it returns `false`.
74
+
75
+ ```
76
+ <TideModal
77
+ v-model="isOpen"
78
+ :before-close="() => {
79
+ if (!confirm('Close modal?')) return false;
80
+ }"
81
+ >
82
+ ```
83
+
84
+ ✅ After these changes your project should be compatible with **TIDE 2.5**.
85
+
7
86
  ## Upgrading from 2.3 → 2.4
8
87
 
9
88
  ### Summary
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.4.7",
66
+ "version": "2.5.0",
67
67
  "dependencies": {
68
68
  "@floating-ui/vue": "^1.1.6"
69
69
  }
@@ -5,12 +5,9 @@
5
5
  import { SIZE } from '@/types/Size';
6
6
  import { CSS } from '@/types/Styles';
7
7
 
8
- const emit = defineEmits(['toggle']);
9
-
10
8
  type Props = {
11
9
  hasBottomDivider?: boolean;
12
10
  hasTopDivider?: boolean;
13
- isExpanded?: boolean;
14
11
  isOptional?: boolean;
15
12
  label: string;
16
13
  };
@@ -18,22 +15,29 @@
18
15
  const props = withDefaults(defineProps<Props>(), {
19
16
  hasBottomDivider: false,
20
17
  hasTopDivider: false,
21
- isExpanded: false,
22
18
  isOptional: false,
23
- label: '',
24
19
  });
25
20
 
21
+ const isExpanded = defineModel<boolean>({ required: true });
22
+
26
23
  const handleToggleClick = () => {
27
- emit('toggle');
24
+ isExpanded.value = !isExpanded.value;
28
25
  };
29
26
  </script>
30
27
 
31
28
  <template>
32
- <div class="tide-accordion-item">
29
+ <div :class="['tide-accordion-item']">
33
30
  <TideDivider v-if="props.hasTopDivider" />
34
31
 
35
- <div
36
- :class="['tide-accordion-head', CSS.DISPLAY.FLEX, CSS.AXIS1.BETWEEN, CSS.PADDING.Y.HALF, CSS.CURSOR.POINTER]"
32
+ <button
33
+ :class="[
34
+ 'tide-accordion-head',
35
+ CSS.ALIGN.X.LEFT,
36
+ CSS.AXIS1.BETWEEN,
37
+ CSS.DISPLAY.FLEX,
38
+ CSS.PADDING.Y.HALF,
39
+ CSS.WIDTH.FULL,
40
+ ]"
37
41
  @click="handleToggleClick"
38
42
  >
39
43
  <div :class="[CSS.DISPLAY.FLEX, CSS.AXIS2.CENTER, CSS.GAP.HALF, CSS.FONT.ROLE.LABEL_1_SEMIBOLD]">
@@ -51,9 +55,9 @@
51
55
  :icon="isExpanded ? ICON.EXPAND_LESS : ICON.EXPAND_MORE"
52
56
  :size="SIZE.LARGE"
53
57
  />
54
- </div>
58
+ </button>
55
59
 
56
- <div :class="['tide-accordion-body', props.isExpanded ? 'expanded' : 'collapsed', CSS.DISPLAY.GRID]">
60
+ <div :class="['tide-accordion-body', isExpanded ? 'expanded' : 'collapsed', CSS.DISPLAY.GRID]">
57
61
  <div :class="[CSS.OVERFLOW.Y.HIDDEN, CSS.MARGIN.Y.ONE, CSS.FONT.ROLE.BODY_1, CSS.FONT.COLOR.SURFACE.DEFAULT]">
58
62
  <slot />
59
63
  </div>
@@ -64,15 +68,19 @@
64
68
  </template>
65
69
 
66
70
  <style scoped>
71
+ .tide-accordion-item {
72
+ align-self: stretch;
73
+ }
74
+
67
75
  .tide-accordion-body {
68
76
  transition: grid-template-rows var(--tide-animate);
69
77
  }
70
78
 
71
79
  .tide-accordion-body.expanded {
72
- grid-template-rows: 1fr;
80
+ grid-template-rows: minmax(0px, 1fr);
73
81
  }
74
82
 
75
83
  .tide-accordion-body.collapsed {
76
- grid-template-rows: 0fr;
84
+ grid-template-rows: minmax(0px, 0fr);
77
85
  }
78
86
  </style>
@@ -4,18 +4,15 @@
4
4
  import type { Tab } from '@/types/Tab';
5
5
 
6
6
  type Props = {
7
- activeTab: number;
8
- tabs: Tab[];
7
+ tabs: Tab[] | readonly Tab[];
9
8
  };
10
9
 
11
- const props = withDefaults(defineProps<Props>(), {
12
- activeTab: 0,
13
- });
10
+ defineProps<Props>();
14
11
 
15
- const emit = defineEmits(['change']);
12
+ const currentIndex = defineModel<number>({ required: true });
16
13
 
17
14
  const handleClick = (index: number) => {
18
- emit('change', index);
15
+ currentIndex.value = index;
19
16
  };
20
17
  </script>
21
18
 
@@ -24,33 +21,34 @@
24
21
  :class="[
25
22
  'tide-button-segmented',
26
23
  CSS.BG.SURFACE.VARIANT,
24
+ CSS.BORDER.RADIUS.FULL,
27
25
  CSS.DISPLAY.FLEX,
26
+ CSS.FLEX.SHRINK.OFF,
28
27
  CSS.GAP.QUARTER,
29
- CSS.BORDER.RADIUS.FULL,
28
+ CSS.OVERFLOW.XY.AUTO,
30
29
  CSS.PADDING.FULL.QUARTER,
31
- CSS.OVERFLOW.XY.HIDDEN,
32
- CSS.WIDTH.FULL,
33
30
  ]"
34
31
  >
35
32
  <button
36
33
  :class="[
37
34
  'tide-button-segmented-tab',
38
- index === activeTab
35
+ index === currentIndex
39
36
  ? [CSS.BG.SURFACE.DEFAULT, CSS.FONT.COLOR.SURFACE.DEFAULT, CSS.SHADOW.BOTTOM]
40
37
  : ['inactive', CSS.FONT.COLOR.SURFACE.VARIANT],
41
- CSS.FLEX.GROW.ON,
42
38
  CSS.BORDER.FULL.TWO,
43
39
  CSS.BORDER.RADIUS.FULL,
44
- CSS.PADDING.Y.QUARTER,
45
- CSS.WIDTH.FULL,
40
+ CSS.FLEX.GROW.ON,
46
41
  CSS.FONT.ROLE.BUTTON_1,
42
+ CSS.PADDING.X.HALF,
43
+ CSS.PADDING.Y.QUARTER,
47
44
  CSS.WHITESPACE_WRAP.OFF,
45
+ CSS.WIDTH.FULL,
48
46
  ]"
49
47
  :data-track="tab.dataTrack || undefined"
50
48
  :key="tab.label"
51
49
  @click="handleClick(index)"
52
50
  type="button"
53
- v-for="(tab, index) in props.tabs"
51
+ v-for="(tab, index) in tabs"
54
52
  >
55
53
  <span :class="[CSS.FONT.ROLE.LABEL_2_SEMIBOLD]">
56
54
  {{ tab.label }}
@@ -69,6 +67,7 @@
69
67
  <style scoped>
70
68
  .tide-button-segmented {
71
69
  max-width: 23.3rem;
70
+ align-self: stretch;
72
71
  }
73
72
 
74
73
  .tide-button-segmented-tab {
@@ -2,19 +2,24 @@
2
2
  import { CSS } from '@/types/Styles';
3
3
 
4
4
  type Props = {
5
- isActive: boolean;
6
5
  label: string;
7
6
  };
8
7
 
9
- const props = defineProps<Props>();
8
+ defineProps<Props>();
9
+
10
+ const isActive = defineModel<boolean>({ required: true });
11
+
12
+ const handleClick = () => {
13
+ isActive.value = !isActive.value;
14
+ };
10
15
  </script>
11
16
 
12
17
  <template>
13
18
  <button
14
19
  :class="[
15
20
  'tide-chip-filter',
16
- props.isActive
17
- ? [CSS.BG.SECONDARY, CSS.FONT.COLOR.SECONDARY]
21
+ isActive
22
+ ? ['active', CSS.BG.SECONDARY, CSS.FONT.COLOR.SECONDARY]
18
23
  : [CSS.BG.SURFACE.VARIANT, CSS.FONT.COLOR.SURFACE.DEFAULT],
19
24
  CSS.DISPLAY.FLEX,
20
25
  CSS.AXIS2.CENTER,
@@ -24,14 +29,15 @@
24
29
  CSS.PADDING.X.ONE,
25
30
  CSS.PADDING.Y.HALF,
26
31
  CSS.FONT.ROLE.LABEL_2,
27
- props.isActive ? 'active' : '',
28
32
  ]"
33
+ @click="handleClick"
29
34
  type="button"
30
35
  >
31
36
  <slot />
37
+
32
38
  <div :class="[CSS.DISPLAY.FLEX, CSS.AXIS1.CENTER, CSS.AXIS2.CENTER, CSS.GAP.HALF]">
33
- <span :class="[props.isActive ? '' : 'icon-spacing', CSS.FONT.ROLE.LABEL_1, CSS.WHITESPACE_WRAP.OFF]">
34
- {{ props.label }}
39
+ <span :class="[CSS.FONT.ROLE.LABEL_1, CSS.WHITESPACE_WRAP.OFF]">
40
+ {{ label }}
35
41
  </span>
36
42
  </div>
37
43
  </button>
@@ -107,7 +107,6 @@
107
107
  showError ? CSS.FONT.COLOR.GLOBAL.ERROR : CSS.FONT.COLOR.SURFACE.DEFAULT,
108
108
  disabled ? [CSS.OPACITY.DISABLED, CSS.CURSOR.NOT_ALLOWED] : [CSS.CURSOR.POINTER],
109
109
  ]"
110
- :for="inputId"
111
110
  >
112
111
  <input
113
112
  :checked="isChecked"
@@ -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>
@@ -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>
@@ -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
 
@@ -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>