diff --git a/packages/ui/src/components/base/Slider.vue b/packages/ui/src/components/base/Slider.vue
index 1188353c63..e4d8339da5 100644
--- a/packages/ui/src/components/base/Slider.vue
+++ b/packages/ui/src/components/base/Slider.vue
@@ -2,7 +2,8 @@
{{ minLabel ?? min }}
@@ -13,7 +14,6 @@
:class="[heightClass, disabled ? 'opacity-50' : '']"
>
+
+
@@ -45,24 +68,12 @@
>
-
-
-
-
{{ maxLabel ?? formatValue(max) }}
@@ -72,7 +83,8 @@
type="number"
:size="size"
wrapper-class="slider-value shrink-0"
- :style="{ width: currentValue === null ? '100%' : inputWidth }"
+ :class="currentValue === null ? 'w-full' : undefined"
+ :style="currentValue === null ? undefined : { '--value-chars': valueFieldChars }"
:input-class="currentValue === null ? undefined : 'text-center'"
:disabled="disabled"
:placeholder="placeholder"
@@ -132,15 +144,17 @@ const heightClass = computed(
large: 'h-12',
})[props.size],
)
-const currentValue = ref(props.modelValue === null ? null : normalizeValue(props.modelValue))
-const inputWidth = computed(() => {
- const digits = Math.max(String(props.min).length, String(props.max).length)
- const padding = props.size === 'small' || props.size === 'standard' ? 1.5 : 2
- return `max(65px, calc(${digits}ch + ${padding}rem + 2px))`
+const labelSizeClass = computed(() => (props.size === 'small' ? 'text-sm' : 'text-base'))
+const valueFieldChars = computed(() => {
+ const decimals = (String(props.step).split('.')[1] ?? '').length
+
+ return Math.max(props.min.toFixed(decimals).length, props.max.toFixed(decimals).length, 2)
})
+const currentValue = ref(props.modelValue === null ? null : normalizeValue(props.modelValue))
+const previousRawValue = ref(null)
const currentPercentage = computed(() => getPercentage(currentValue.value ?? props.min))
const visibleSnapPoints = computed(() =>
- props.snapPoints.filter((snapPoint) => snapPoint >= props.min && snapPoint <= props.max),
+ props.snapPoints.filter((snapPoint) => snapPoint > props.min && snapPoint < props.max),
)
watch(
@@ -180,14 +194,25 @@ function inputValueValid(inputValue: number) {
}
function onInputWithSnap(value: string) {
- let parsedValue = Number.parseFloat(value)
+ const parsedValue = Number.parseFloat(value)
+ const previousValue = previousRawValue.value ?? currentValue.value ?? props.min
+
+ let snappedValue = parsedValue
+ let closestDistance = props.snapRange
for (const snapPoint of props.snapPoints) {
const distance = Math.abs(snapPoint - parsedValue)
- if (distance < props.snapRange) parsedValue = snapPoint
+ const previousDistance = Math.abs(snapPoint - previousValue)
+
+ // Only pull toward a snap point while approaching it, not while moving away
+ if (distance < closestDistance && distance < previousDistance) {
+ closestDistance = distance
+ snappedValue = snapPoint
+ }
}
- inputValueValid(parsedValue)
+ previousRawValue.value = parsedValue
+ inputValueValid(snappedValue)
}
function onInput(event: Event) {
@@ -246,15 +271,15 @@ function onInput(event: Event) {
opacity: 1;
}
- &:focus-visible + .slider-track .slider-thumb {
+ &:focus-visible ~ .slider-track .slider-thumb {
outline: 3px solid var(--color-focus-ring);
outline-offset: 3px;
}
&:hover,
&:focus-visible {
- & + .slider-track .filled-slider-track,
- & ~ .snap-points .brightness-on-hover {
+ & ~ .slider-track .filled-slider-track,
+ & ~ .snap-points .bg-brand {
filter: brightness(var(--hover-brightness));
}
}
@@ -262,6 +287,8 @@ function onInput(event: Event) {
.slider-value :deep(input[type='number']) {
-moz-appearance: textfield;
+ flex: none;
+ width: calc(var(--value-chars) * 1ch);
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
@@ -276,4 +303,11 @@ function onInput(event: Event) {
transition: none;
}
}
+
+.snap-points-filled {
+ transition: clip-path 0.25s var(--ease-out-expo);
+ @media (prefers-reduced-motion) {
+ transition: none;
+ }
+}
diff --git a/packages/ui/src/stories/base/Slider.stories.ts b/packages/ui/src/stories/base/Slider.stories.ts
index 4be91681ce..bdc5036a28 100644
--- a/packages/ui/src/stories/base/Slider.stories.ts
+++ b/packages/ui/src/stories/base/Slider.stories.ts
@@ -37,6 +37,24 @@ export const WithSnapPoints: Story = {
},
}
+export const DecimalSteps: Story = {
+ args: {
+ modelValue: 0.5,
+ min: 0,
+ max: 1,
+ step: 0.05,
+ },
+}
+
+export const Small: Story = {
+ args: {
+ size: 'small',
+ modelValue: 50,
+ min: 0,
+ max: 100,
+ },
+}
+
export const Disabled: Story = {
args: {
modelValue: 50,