@cnamts/synapse 0.0.8-alpha → 0.0.9-alpha

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 (111) hide show
  1. package/dist/design-system-v3.d.ts +584 -128
  2. package/dist/design-system-v3.js +4176 -2694
  3. package/dist/design-system-v3.umd.cjs +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/assets/settings.scss +1 -1
  7. package/src/components/ContextualMenu/Accessibilite.mdx +14 -0
  8. package/src/components/ContextualMenu/Accessibilite.stories.ts +191 -0
  9. package/src/components/ContextualMenu/AccessibiliteItems.ts +89 -0
  10. package/src/components/ContextualMenu/constants/ExpertiseLevelEnum.ts +4 -0
  11. package/src/components/Customs/SySelect/SySelect.stories.ts +7 -7
  12. package/src/components/Customs/SySelect/SySelect.vue +9 -4
  13. package/src/components/Customs/SySelect/tests/SySelect.spec.ts +2 -2
  14. package/src/components/Customs/SyTextField/SyTextField.stories.ts +187 -2
  15. package/src/components/Customs/SyTextField/SyTextField.vue +185 -16
  16. package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +2 -4
  17. package/src/components/Customs/SyTextField/tests/__snapshots__/SyTextField.spec.ts.snap +18 -16
  18. package/src/components/Customs/SyTextField/types.d.ts +2 -2
  19. package/src/components/DatePicker/DatePicker.mdx +191 -0
  20. package/src/components/DatePicker/DatePicker.stories.ts +787 -0
  21. package/src/components/DatePicker/DatePicker.vue +560 -0
  22. package/src/components/DatePicker/DateTextInput.vue +409 -0
  23. package/src/components/DatePicker/tests/DatePicker.spec.ts +266 -0
  24. package/src/components/DialogBox/DialogBox.stories.ts +1 -1
  25. package/src/components/ExternalLinks/Accessibilite.mdx +14 -0
  26. package/src/components/ExternalLinks/Accessibilite.stories.ts +191 -0
  27. package/src/components/ExternalLinks/AccessibiliteItems.ts +197 -0
  28. package/src/components/ExternalLinks/constants/ExpertiseLevelEnum.ts +4 -0
  29. package/src/components/ExternalLinks/tests/__snapshots__/ExternalLinks.spec.ts.snap +9 -9
  30. package/src/components/FileUpload/FileUpload.mdx +165 -0
  31. package/src/components/FileUpload/FileUpload.stories.ts +429 -0
  32. package/src/components/FileUpload/FileUpload.vue +195 -0
  33. package/src/components/FileUpload/FileUploadContent.vue +109 -0
  34. package/src/components/FileUpload/locales.ts +10 -0
  35. package/src/components/FileUpload/tests/FileUpload.spec.ts +332 -0
  36. package/src/components/FileUpload/tests/__snapshots__/FileUpload.spec.ts.snap +7 -0
  37. package/src/components/FileUpload/useFileDrop.ts +23 -0
  38. package/src/components/FileUpload/validateFiles.ts +39 -0
  39. package/src/components/NirField/NirField.stories.ts +1 -1
  40. package/src/components/NirField/NirField.vue +2 -1
  41. package/src/components/PasswordField/Accessibilite.mdx +14 -0
  42. package/src/components/PasswordField/Accessibilite.stories.ts +191 -0
  43. package/src/components/PasswordField/AccessibiliteItems.ts +184 -0
  44. package/src/components/PasswordField/PasswordField.vue +3 -3
  45. package/src/components/PasswordField/constants/ExpertiseLevelEnum.ts +4 -0
  46. package/src/components/PhoneField/PhoneField.vue +44 -60
  47. package/src/components/PhoneField/tests/PhoneField.spec.ts +0 -15
  48. package/src/components/RangeField/RangeField.mdx +54 -0
  49. package/src/components/RangeField/RangeField.stories.ts +189 -0
  50. package/src/components/RangeField/RangeField.vue +157 -0
  51. package/src/components/RangeField/RangeSlider/RangeSlider.vue +387 -0
  52. package/src/components/RangeField/RangeSlider/Tooltip/Tooltip.vue +64 -0
  53. package/src/components/RangeField/RangeSlider/tests/__snapshots__/rangeSlider.spec.ts.snap +27 -0
  54. package/src/components/RangeField/RangeSlider/tests/rangeSlider.spec.ts +100 -0
  55. package/src/components/RangeField/RangeSlider/tests/useDoubleSlider.spec.ts +246 -0
  56. package/src/components/RangeField/RangeSlider/tests/useMouseSlide.spec.ts +204 -0
  57. package/src/components/RangeField/RangeSlider/tests/useThumb.spec.ts +22 -0
  58. package/src/components/RangeField/RangeSlider/tests/useThumbKeyboard.spec.ts +233 -0
  59. package/src/components/RangeField/RangeSlider/tests/useTooltipsNudge.spec.ts +150 -0
  60. package/src/components/RangeField/RangeSlider/tests/useTrack.spec.ts +314 -0
  61. package/src/components/RangeField/RangeSlider/tests/vAnimateClick.spec.ts +32 -0
  62. package/src/components/RangeField/RangeSlider/types.ts +15 -0
  63. package/src/components/RangeField/RangeSlider/useMouseSlide.ts +109 -0
  64. package/src/components/RangeField/RangeSlider/useRangeSlider.ts +126 -0
  65. package/src/components/RangeField/RangeSlider/useThumb.ts +18 -0
  66. package/src/components/RangeField/RangeSlider/useThumbKeyboard.ts +84 -0
  67. package/src/components/RangeField/RangeSlider/useTooltipsNudge.ts +92 -0
  68. package/src/components/RangeField/RangeSlider/useTrack.ts +116 -0
  69. package/src/components/RangeField/RangeSlider/vAnimateClick.ts +19 -0
  70. package/src/components/RangeField/config.ts +7 -0
  71. package/src/components/RangeField/locales.ts +4 -0
  72. package/src/components/RangeField/tests/RangeField.spec.ts +224 -0
  73. package/src/components/RangeField/tests/__snapshots__/RangeField.spec.ts.snap +379 -0
  74. package/src/components/RatingPicker/EmotionPicker/EmotionPicker.vue +205 -0
  75. package/src/components/RatingPicker/EmotionPicker/locales.ts +3 -0
  76. package/src/components/RatingPicker/EmotionPicker/tests/EmotionPicker.spec.ts +104 -0
  77. package/src/components/RatingPicker/EmotionPicker/tests/__snapshots__/EmotionPicker.spec.ts.snap +66 -0
  78. package/src/components/RatingPicker/NumberPicker/NumberPicker.vue +159 -0
  79. package/src/components/RatingPicker/NumberPicker/locales.ts +4 -0
  80. package/src/components/RatingPicker/NumberPicker/tests/NumberPicker.spec.ts +73 -0
  81. package/src/components/RatingPicker/NumberPicker/tests/__snapshots__/NumberPicker.spec.ts.snap +105 -0
  82. package/src/components/RatingPicker/Rating.ts +45 -0
  83. package/src/components/RatingPicker/RatingPicker.mdx +56 -0
  84. package/src/components/RatingPicker/RatingPicker.stories.ts +515 -0
  85. package/src/components/RatingPicker/RatingPicker.vue +122 -0
  86. package/src/components/RatingPicker/StarsPicker/StarsPicker.vue +116 -0
  87. package/src/components/RatingPicker/StarsPicker/tests/StarsPicker.spec.ts +95 -0
  88. package/src/components/RatingPicker/StarsPicker/tests/__snapshots__/StarsPicker.spec.ts.snap +36 -0
  89. package/src/components/RatingPicker/locales.ts +3 -0
  90. package/src/components/RatingPicker/tests/Rating.spec.ts +104 -0
  91. package/src/components/RatingPicker/tests/RatingPicker.spec.ts +187 -0
  92. package/src/components/RatingPicker/tests/__snapshots__/RatingPicker.spec.ts.snap +108 -0
  93. package/src/components/SearchListField/SearchListField.mdx +74 -0
  94. package/src/components/SearchListField/SearchListField.stories.ts +126 -0
  95. package/src/components/SearchListField/SearchListField.vue +194 -0
  96. package/src/components/SearchListField/locales.ts +5 -0
  97. package/src/components/SearchListField/tests/SearchListField.spec.ts +323 -0
  98. package/src/components/SearchListField/types.d.ts +4 -0
  99. package/src/components/SelectBtnField/SelectBtnField.mdx +50 -0
  100. package/src/components/SelectBtnField/SelectBtnField.stories.ts +763 -0
  101. package/src/components/SelectBtnField/SelectBtnField.vue +283 -0
  102. package/src/components/SelectBtnField/config.ts +11 -0
  103. package/src/components/SelectBtnField/tests/SelectBtnField.spec.ts +327 -0
  104. package/src/components/SelectBtnField/tests/__snapshots__/SelectBtnField.spec.ts.snap +125 -0
  105. package/src/components/SelectBtnField/types.d.ts +11 -0
  106. package/src/components/index.ts +8 -1
  107. package/src/composables/rules/useFieldValidation.ts +172 -44
  108. package/src/designTokens/index.ts +3 -3
  109. package/src/stories/Fondamentaux/CustomisationEtThemes.mdx +52 -2
  110. package/src/utils/calcHumanFileSize/index.ts +12 -0
  111. package/src/utils/calcHumanFileSize/tests/calcHumanFileSize.spec.ts +21 -0
@@ -0,0 +1,189 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import RangeField from './RangeField.vue'
3
+
4
+ const meta = {
5
+ title: 'Composants/Formulaires/RangeField',
6
+ component: RangeField,
7
+ argTypes: {
8
+ 'min': {
9
+ control: 'number',
10
+ description: 'Valeur minimale du champ',
11
+ table: {
12
+ type: {
13
+ summary: 'number',
14
+ },
15
+ },
16
+ },
17
+ 'max': {
18
+ control: 'number',
19
+ description: 'Valeur maximale du champ',
20
+ table: {
21
+ type: {
22
+ summary: 'number',
23
+ },
24
+ },
25
+ },
26
+ 'step': {
27
+ control: 'number',
28
+ description: 'Le pas du slider',
29
+ table: {
30
+ type: {
31
+ summary: 'number',
32
+ },
33
+ },
34
+ },
35
+ 'modelValue': {
36
+ control: 'object',
37
+ description: 'Valeur du champ',
38
+ defaultValue: [0, 0],
39
+ table: {
40
+ category: 'props',
41
+ type: {
42
+ summary: '[number, number]',
43
+ },
44
+ },
45
+ },
46
+ 'onUpdate:modelValue': {
47
+ action: 'update:modelValue',
48
+ description: 'Événement émis lors de la modification de la valeur du champ',
49
+ table: {
50
+ category: 'events',
51
+ type: {
52
+ summary: '[number, number]',
53
+ },
54
+ },
55
+ },
56
+ 'vuetifyOptions': {
57
+ control: 'object',
58
+ description: 'Personnalisation des composants Vuetify internes',
59
+ table: {
60
+ category: 'props',
61
+ defaultValue: {
62
+ detail: `
63
+ {
64
+ textField: {
65
+ hideDetails: true,
66
+ class: 'ma-3',
67
+ variant: 'outlined',
68
+ },
69
+ }`,
70
+ },
71
+ type: {
72
+ summary: 'Record<string, any>',
73
+ detail: `
74
+ {
75
+ textField?: Record<string, any>,
76
+ rangeSlider?: Record<string, any>,
77
+ }
78
+ `,
79
+ },
80
+ },
81
+ },
82
+ },
83
+ } satisfies Meta<typeof RangeField>
84
+
85
+ export default meta
86
+
87
+ type Story = StoryObj<typeof meta>
88
+
89
+ export const Default: Story = {
90
+ parameters: {
91
+ sourceCode: [
92
+ {
93
+ name: 'Template',
94
+ code: `<template>
95
+ <div style="width: 300px;">
96
+ <RangeField v-model="range" />
97
+ </div>
98
+ </template>
99
+ `,
100
+ },
101
+ {
102
+ name: 'Script',
103
+ code: `<script setup lang="ts">
104
+ import { RangeField } from '@cnamts/synapse'
105
+ import { ref } from 'vue'
106
+
107
+ const range = ref([0, 100])
108
+ </script>
109
+ `,
110
+ },
111
+ ],
112
+ },
113
+ }
114
+
115
+ export const OtherRange: Story = {
116
+ args: {
117
+ min: -50,
118
+ max: 50,
119
+ },
120
+ parameters: {
121
+ sourceCode: [
122
+ {
123
+ name: 'Template',
124
+ code: `<template>
125
+ <div style="width: 300px;">
126
+ <RangeField v-model="modelValue" :min="-50" :max="50" />
127
+ </div>
128
+ </template>
129
+ `,
130
+ },
131
+ {
132
+ name: 'Script',
133
+ code: `<script setup lang="ts">
134
+ import { RangeField } from '@cnamts/synapse'
135
+ import { ref } from 'vue'
136
+ const modelValue = ref([50, 80])
137
+ </script>
138
+ `,
139
+ },
140
+ ],
141
+ },
142
+ }
143
+
144
+ export const Customization: Story = {
145
+ args: {
146
+ vuetifyOptions: {
147
+ textField: {
148
+ variant: 'plain',
149
+ },
150
+ rangeSlider: {
151
+ 'thumb-color': 'purple',
152
+ 'track-color': 'LightSteelBlue',
153
+ 'track-fill-color': 'purple',
154
+ },
155
+ },
156
+ },
157
+ parameters: {
158
+ sourceCode: [
159
+ {
160
+ name: 'Template',
161
+ code: `<template>
162
+ <div style="width: 300px;">
163
+ <RangeField vuetifyOptions="vuetifyOptions" v-model="range" />
164
+ </div>
165
+ </template>
166
+ `,
167
+ },
168
+ {
169
+ name: 'Script',
170
+ code: `<script setup lang="ts">
171
+ import { RangeField } from '@cnamts/synapse'
172
+ import { ref } from 'vue'
173
+ const vuetifyOptions = {
174
+ textField: {
175
+ variant: 'plain',
176
+ },
177
+ rangeSlider: {
178
+ 'thumb-color': 'purple',
179
+ 'track-color': 'LightSteelBlue',
180
+ 'track-fill-color': 'purple',
181
+ },
182
+ }
183
+ const range = ref([0, 100])
184
+ </script>
185
+ `,
186
+ },
187
+ ],
188
+ },
189
+ }
@@ -0,0 +1,157 @@
1
+ <script lang="ts" setup>
2
+ import useCustomizableOptions, { type CustomizableOptions } from '@/composables/useCustomizableOptions'
3
+ import { vMaska } from 'maska/vue'
4
+ import { computed, ref, watch } from 'vue'
5
+ import { useDisplay } from 'vuetify'
6
+ import RangeSlider from './RangeSlider/RangeSlider.vue'
7
+ import { config } from './config'
8
+ import { locales } from './locales'
9
+
10
+ const props = withDefaults(defineProps<CustomizableOptions & {
11
+ min?: number
12
+ max?: number
13
+ step?: number
14
+ }>(), {
15
+ min: 0,
16
+ max: 100,
17
+ step: 1,
18
+ })
19
+
20
+ const options = useCustomizableOptions(config, props)
21
+
22
+ const model = defineModel<
23
+ Array<number>
24
+ >()
25
+
26
+ const innerValue = computed(() => {
27
+ return model.value ? model.value : [props.min, props.max]
28
+ })
29
+
30
+ const fieldMin = ref<string | number>(innerValue.value[0])
31
+ const fieldMax = ref<string | number>(innerValue.value[1])
32
+
33
+ watch(model, (value) => {
34
+ if (!Array.isArray(value)) {
35
+ model.value = [
36
+ props.min,
37
+ props.max,
38
+ ]
39
+ return
40
+ }
41
+
42
+ const start = clamp(value[0]), end = clamp(value[1])
43
+ fieldMin.value = start
44
+ fieldMax.value = end
45
+ if (start !== value[0] || end !== value[1]) {
46
+ model.value = [start, end]
47
+ }
48
+ }, {
49
+ immediate: true,
50
+ deep: true,
51
+ })
52
+
53
+ const display = useDisplay()
54
+
55
+ const mask = {
56
+ tokens: {
57
+ 'N': { pattern: /-/, optional: true },
58
+ 'n': { pattern: /\d/, multiple: true, optional: true },
59
+ '.': { pattern: /\./, optional: true },
60
+ 'd': { pattern: /\d/, multiple: true, optional: true },
61
+ },
62
+ mask: 'Nn.d',
63
+ }
64
+
65
+ function clamp(value: number | string) {
66
+ value = Math.round(Number(value))
67
+ if (isNaN(value)) return props.min
68
+ return Math.min(Math.max(value, props.min), props.max)
69
+ }
70
+
71
+ function updateMin(value: string) {
72
+ if (value === '') {
73
+ return
74
+ }
75
+ const newValue = Number(value)
76
+ if (
77
+ !isNaN(newValue)
78
+ && isFinite(newValue)
79
+ && newValue >= props.min
80
+ && newValue <= model.value![1]
81
+ ) {
82
+ model.value = [newValue, innerValue.value[1]]
83
+ }
84
+ }
85
+
86
+ function updateMax(value: string) {
87
+ if (value === '') {
88
+ return
89
+ }
90
+ const newValue = Number(value)
91
+ if (
92
+ !isNaN(newValue)
93
+ && isFinite(newValue)
94
+ && newValue <= props.max
95
+ && newValue >= innerValue.value[0]
96
+ ) {
97
+ model.value = [innerValue.value[0], newValue]
98
+ }
99
+ }
100
+
101
+ function validateMin(focus: boolean) {
102
+ if (!focus) {
103
+ fieldMin.value = innerValue.value[0]
104
+ }
105
+ }
106
+
107
+ function validateMax(focus: boolean) {
108
+ if (!focus) {
109
+ fieldMax.value = innerValue.value[1]
110
+ }
111
+ }
112
+ </script>
113
+
114
+ <template>
115
+ <div class="vd-range-field">
116
+ <div class="mt-10 mb-2 mx-3">
117
+ <RangeSlider
118
+ :model-value="innerValue"
119
+ v-bind="options.rangeSlider"
120
+ :max="max"
121
+ :min="min"
122
+ :step="step"
123
+ class="mt-8 mb-2"
124
+ @update:model-value="model = $event"
125
+ />
126
+ </div>
127
+ <div
128
+ :class="{ 'flex-column': display.xs.value }"
129
+ class="d-flex flex-wrap max-width-none"
130
+ >
131
+ <VTextField
132
+ v-model="fieldMin"
133
+ v-maska="mask"
134
+ v-bind="options.textField"
135
+ :label="locales.minLabel"
136
+ :aria-label="locales.minLabel"
137
+ inputmode="numeric"
138
+ color="primary"
139
+ :title="locales.minLabel"
140
+ @update:model-value="updateMin"
141
+ @update:focused="validateMin"
142
+ />
143
+ <VTextField
144
+ v-model="fieldMax"
145
+ v-maska="mask"
146
+ v-bind="options.textField"
147
+ :label="locales.maxLabel"
148
+ :aria-label="locales.maxLabel"
149
+ inputmode="numeric"
150
+ color="primary"
151
+ :title="locales.maxLabel"
152
+ @update:model-value="updateMax"
153
+ @update:focused="validateMax"
154
+ />
155
+ </div>
156
+ </div>
157
+ </template>
@@ -0,0 +1,387 @@
1
+ // @see
2
+ // https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/examples/slider-multithumb/
3
+ // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/slider_role
4
+
5
+ <script setup lang="ts">
6
+ import { cnamColorsTokens } from '@/designTokens'
7
+ import { computed, ref, toRef, watch, type Ref } from 'vue'
8
+ import Tooltip from './Tooltip/Tooltip.vue'
9
+ import type { PropsStyle } from './types'
10
+ import useMouseSlide from './useMouseSlide'
11
+ import useDoubleSlider from './useRangeSlider'
12
+ import useThumb from './useThumb'
13
+ import useThumbKeyboard from './useThumbKeyboard'
14
+ import useTooltipsNudge from './useTooltipsNudge'
15
+ import useTrack from './useTrack'
16
+ import { vAnimateClick } from './vAnimateClick'
17
+
18
+ const props = withDefaults(
19
+ defineProps<
20
+ PropsStyle &
21
+ {
22
+ modelValue?: Array<number | string>
23
+ min?: number | string
24
+ max?: number | string
25
+ step?: number | string
26
+ minLabel?: string
27
+ maxLabel?: string
28
+ }>(),
29
+ {
30
+ 'modelValue': () => [],
31
+ 'min': 0,
32
+ 'max': 100,
33
+ 'step': 1,
34
+ 'minLabel': 'Minimum',
35
+ 'maxLabel': 'Maximum',
36
+ 'thumb-color': cnamColorsTokens.blue.base,
37
+ 'track-color': cnamColorsTokens.blue.lighten80,
38
+ 'track-fill-color': cnamColorsTokens.blue.lighten40,
39
+ },
40
+ )
41
+
42
+ const range = useDoubleSlider(
43
+ toRef(props, 'min'),
44
+ toRef(props, 'max'),
45
+ toRef(props, 'step'),
46
+ toRef(props, 'modelValue'),
47
+ )
48
+
49
+ const emits = defineEmits<{
50
+ (e: 'update:modelValue', value: number[]): void
51
+ }>()
52
+
53
+ const {
54
+ thumbStyle: thumbMinStyle,
55
+ } = useThumb(
56
+ range.selectedMin,
57
+ range.rangeMin,
58
+ range.rangeMax,
59
+ )
60
+
61
+ const {
62
+ thumbStyle: thumbMaxStyle,
63
+ } = useThumb(
64
+ range.selectedMax,
65
+ range.rangeMin,
66
+ range.rangeMax,
67
+ )
68
+
69
+ const filledTrackStyle = computed(() => {
70
+ const rangeMin = range.rangeMin.value
71
+ const rangeMax = range.rangeMax.value
72
+ const selectedMin = range.selectedMin.value
73
+ const selectedMax = range.selectedMax.value
74
+
75
+ const rangeWidth = rangeMax - rangeMin
76
+ const left = (selectedMin - rangeMin) * 100 / rangeWidth
77
+ const width = (selectedMax - rangeMin) * 100 / rangeWidth - left
78
+
79
+ return {
80
+ left: `${left}%`,
81
+ width: `${width}%`,
82
+ }
83
+ })
84
+
85
+ const track = ref<HTMLElement | null>(null)
86
+ const thumbMin = ref<HTMLElement | null>(null)
87
+ const thumbMax = ref<HTMLElement | null>(null)
88
+
89
+ const { inProgress: minThumbDrag } = useMouseSlide(
90
+ thumbMin as Ref<HTMLElement>,
91
+ track as Ref<HTMLElement>,
92
+ range.selectedMin,
93
+ range.rangeMin,
94
+ range.rangeMax,
95
+ range.step,
96
+ range.rangeMin,
97
+ range.selectedMax,
98
+ (value: number) => range.selectedMin.value = value,
99
+ )
100
+
101
+ const { inProgress: maxThumbDrag } = useMouseSlide(
102
+ thumbMax as Ref<HTMLElement>,
103
+ track as Ref<HTMLElement>,
104
+ range.selectedMax,
105
+ range.rangeMin,
106
+ range.rangeMax,
107
+ range.step,
108
+ range.selectedMin,
109
+ range.rangeMax,
110
+ (value: number) => range.selectedMax.value = value,
111
+ )
112
+
113
+ const dragInProgress = computed(() => minThumbDrag.value || maxThumbDrag.value)
114
+
115
+ useTrack(
116
+ track as Ref<HTMLElement>,
117
+ range,
118
+ (value: number) => range.selectedMin.value = value,
119
+ (value: number) => range.selectedMax.value = value,
120
+ dragInProgress,
121
+ )
122
+
123
+ useThumbKeyboard(
124
+ thumbMin as Ref<HTMLElement>,
125
+ range.selectedMin,
126
+ range.rangeMin,
127
+ range.selectedMax,
128
+ range.step,
129
+ (value: number) => range.selectedMin.value = value,
130
+ )
131
+
132
+ useThumbKeyboard(
133
+ thumbMax as Ref<HTMLElement>,
134
+ range.selectedMax,
135
+ range.selectedMin,
136
+ range.rangeMax,
137
+ range.step,
138
+ (value: number) => range.selectedMax.value = value,
139
+ )
140
+
141
+ const hiddenTooltipMin = ref<typeof Tooltip | null>(null)
142
+ const hiddenTooltipMax = ref<typeof Tooltip | null>(null)
143
+ const tooltipMin = ref<typeof Tooltip | null>(null)
144
+ const tooltipMax = ref<typeof Tooltip | null>(null)
145
+
146
+ const {
147
+ nudgeMinThumb,
148
+ nudgeMaxThumb,
149
+ } = useTooltipsNudge(
150
+ tooltipMin,
151
+ tooltipMax,
152
+ hiddenTooltipMin,
153
+ hiddenTooltipMax,
154
+ range,
155
+ )
156
+
157
+ watch(() => [range.selectedMin.value, range.selectedMax.value], (value) => {
158
+ if (
159
+ value[0] !== Number(props.modelValue[0])
160
+ || value[1] !== Number(props.modelValue[1])
161
+ ) {
162
+ emits('update:modelValue', value)
163
+ }
164
+ })
165
+
166
+ </script>
167
+
168
+ <template>
169
+ <div class="wrapper">
170
+ <div
171
+ ref="track"
172
+ class="track"
173
+ >
174
+ <button
175
+ ref="thumbMin"
176
+ v-animate-click
177
+ class="thumb-min"
178
+ :style="thumbMinStyle"
179
+ role="slider"
180
+ :aria-valuenow="range.selectedMin.value"
181
+ tabindex="0"
182
+ :aria-valuemin="range.rangeMin.value"
183
+ :aria-valuemax="range.selectedMax.value"
184
+ aria-orientation="horizontal"
185
+ :aria-label="minLabel"
186
+ :title="minLabel"
187
+ >
188
+ <Tooltip
189
+ ref="tooltipMin"
190
+ :nudge-right="nudgeMinThumb"
191
+ >
192
+ {{ range.selectedMin.value }}
193
+ </Tooltip>
194
+ <span class="inner-thumb" />
195
+ </button>
196
+ <button
197
+ ref="thumbMax"
198
+ v-animate-click
199
+ role="slider"
200
+ class="thumb-max"
201
+ :style="thumbMaxStyle"
202
+ :aria-valuenow="range.selectedMax.value"
203
+ tabindex="0"
204
+ :aria-valuemin="range.selectedMin.value"
205
+ :aria-valuemax="range.rangeMax.value"
206
+ aria-orientation="horizontal"
207
+ :aria-label="maxLabel"
208
+ :title="maxLabel"
209
+ >
210
+ <Tooltip
211
+ ref="tooltipMax"
212
+ :nudge-left="nudgeMaxThumb"
213
+ >
214
+ {{ range.selectedMax.value }}
215
+ </Tooltip>
216
+ <span class="inner-thumb" />
217
+ </button>
218
+ <div
219
+ class="fake-thumb thumb-min"
220
+ aria-hidden="true"
221
+ :style="thumbMinStyle"
222
+ >
223
+ <Tooltip
224
+ ref="hiddenTooltipMin"
225
+ >
226
+ {{ range.selectedMin.value }}
227
+ </Tooltip>
228
+ </div>
229
+ <div
230
+ class="fake-thumb thumb-max"
231
+ aria-hidden="true"
232
+ :style="thumbMaxStyle"
233
+ >
234
+ <Tooltip
235
+ ref="hiddenTooltipMax"
236
+ >
237
+ {{ range.selectedMax.value }}
238
+ </Tooltip>
239
+ </div>
240
+ <div
241
+ class="filled-track"
242
+ :style="filledTrackStyle"
243
+ />
244
+ </div>
245
+ </div>
246
+ </template>
247
+
248
+ <style lang="scss" scoped>
249
+ @use '@/assets/tokens';
250
+
251
+ $virtual-thumb-size: 40px;
252
+
253
+ .wrapper {
254
+ --sy-track-height: 4px;
255
+ --sy-thumb-size: 20px;
256
+ --sy-thumb-color: v-bind(props.thumbColor);
257
+ --sy-track-color: v-bind(props.trackColor);
258
+ --sy-track-fill-color: v-bind(props.trackFillColor);
259
+
260
+ position: relative;
261
+ margin-inline: var(--sy-thumb-size);
262
+ width: calc(100% - var(--sy-thumb-size) * 2);
263
+ }
264
+
265
+ .track {
266
+ height: 32px;
267
+ cursor: pointer;
268
+
269
+ &::before {
270
+ content: '';
271
+ position: absolute;
272
+ top: 50%;
273
+ left: 0;
274
+ transform: translate(0, -50%);
275
+ width: 100%;
276
+ height: var(--sy-track-height);
277
+ background-color: var(--sy-track-color);
278
+ }
279
+ }
280
+
281
+ .thumb-min,
282
+ .thumb-max {
283
+ cursor: pointer;
284
+ position: absolute;
285
+ top: 50%;
286
+ left: 0;
287
+ z-index: 2;
288
+ width: $virtual-thumb-size;
289
+ height: $virtual-thumb-size;
290
+ border-radius: 50%;
291
+ transition: left 0.1s;
292
+ will-change: left;
293
+
294
+ &::before {
295
+ content: '';
296
+ position: absolute;
297
+ top: 50%;
298
+ left: 50%;
299
+ transform: translate(-50%, -50%);
300
+ width: var(--sy-thumb-size);
301
+ height: var(--sy-thumb-size);
302
+ background-color: var(--sy-thumb-color);
303
+ border-radius: 50%;
304
+ transform-origin: bottom right;
305
+ transition: transform 0.1s;
306
+ opacity: 0.4;
307
+ }
308
+
309
+ &::after {
310
+ content: '';
311
+ position: absolute;
312
+ top: 50%;
313
+ left: 50%;
314
+ transform: translate(-50%, -50%);
315
+ width: var(--sy-thumb-size);
316
+ height: var(--sy-thumb-size);
317
+ background-color: var(--sy-thumb-color);
318
+ opacity: 0.4;
319
+ border-radius: 50%;
320
+ transform-origin: bottom right;
321
+ transition: transform 0.1s ease-in, opacity 0.1s ease-in;
322
+ }
323
+
324
+ &:focus,
325
+ &:hover {
326
+ &::before {
327
+ transform: scale(2);
328
+ }
329
+ }
330
+ }
331
+
332
+ .inner-thumb {
333
+ position: absolute;
334
+ top: 50%;
335
+ left: 50%;
336
+ z-index: 2;
337
+ transform: translate(-50%, -50%);
338
+ width: var(--sy-thumb-size);
339
+ height: var(--sy-thumb-size);
340
+ background-color: var(--sy-thumb-color);
341
+ border-radius: 50%;
342
+ box-shadow: 0 1px 5px 0 #0000001f, 0 2px 2px 0 #00000024, 0 3px 1px -2px #0003;
343
+ }
344
+
345
+ .thumb-min {
346
+ transform:
347
+ translate(
348
+ calc(
349
+ ($virtual-thumb-size / 2 + var(--sy-thumb-size) / 2) * -1
350
+ ),
351
+ -50%
352
+ );
353
+ }
354
+
355
+ .thumb-max {
356
+ transform:
357
+ translate(
358
+ calc((var(--sy-thumb-size) - $virtual-thumb-size) / 2),
359
+ -50%
360
+ );
361
+ }
362
+
363
+ .filled-track {
364
+ position: absolute;
365
+ top: 50%;
366
+ left: 0;
367
+ transform: translate(0, -50%);
368
+ width: 100%;
369
+ height: var(--sy-track-height);
370
+ background-color: var(--sy-track-fill-color);
371
+ transition: all 0.1s;
372
+ }
373
+
374
+ .animate-click::after {
375
+ transform: scale(2);
376
+ opacity: 0.4;
377
+ transition: transform 0.25s ease-out, opacity 0.25s ease-out;
378
+ }
379
+
380
+ .fake-thumb {
381
+ visibility: hidden;
382
+ transition: none !important;
383
+ cursor: default;
384
+ z-index: -1;
385
+ user-select: none;
386
+ }
387
+ </style>