feat(Slider): tweak styles for a knob and add slider label
This commit is contained in:
@@ -98,11 +98,11 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
|||||||
function calculateScale(index: number): number | string {
|
function calculateScale(index: number): number | string {
|
||||||
const calculate = () =>
|
const calculate = () =>
|
||||||
orientation === 'horizontal'
|
orientation === 'horizontal'
|
||||||
? (control.min + (index * (control.max - control.min) / 4))
|
? control.min + (index * (control.max - control.min)) / 4
|
||||||
: (control.max - (index * (control.max - control.min) / 4));
|
: control.max - (index * (control.max - control.min)) / 4;
|
||||||
return Number.isInteger(control.step)
|
return Number.isInteger(control.step)
|
||||||
? Math.round(calculate())
|
? Math.round(calculate())
|
||||||
: (calculate()).toFixed(2);
|
: calculate().toFixed(2);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -111,7 +111,9 @@ function calculateScale(index: number): number | string {
|
|||||||
class={cn(
|
class={cn(
|
||||||
'flex gap-4 sm:py-4 sm:px-1 rounded-xl transition-all duration-300',
|
'flex gap-4 sm:py-4 sm:px-1 rounded-xl transition-all duration-300',
|
||||||
'',
|
'',
|
||||||
orientation === 'horizontal' ? 'flex-row items-end w-full' : 'flex-col items-center h-full',
|
orientation === 'horizontal'
|
||||||
|
? 'flex-row items-end w-full'
|
||||||
|
: 'flex-col items-center h-full',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -120,7 +122,9 @@ function calculateScale(index: number): number | string {
|
|||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
'absolute flex justify-between',
|
'absolute flex justify-between',
|
||||||
orientation === 'horizontal' ? 'flex-row w-full -top-5 px-0.5' : 'flex-col h-full -left-5 py-0.5',
|
orientation === 'horizontal'
|
||||||
|
? 'flex-row w-full -top-8 px-0.5'
|
||||||
|
: 'flex-col h-full -left-5 py-0.5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{#each Array(5) as _, i}
|
{#each Array(5) as _, i}
|
||||||
@@ -133,7 +137,12 @@ function calculateScale(index: number): number | string {
|
|||||||
<span class="font-mono text-[0.375rem] text-text-muted tabular-nums">
|
<span class="font-mono text-[0.375rem] text-text-muted tabular-nums">
|
||||||
{calculateScale(i)}
|
{calculateScale(i)}
|
||||||
</span>
|
</span>
|
||||||
<div class={cn('bg-border-muted', orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1')}>
|
<div
|
||||||
|
class={cn(
|
||||||
|
'bg-border-muted',
|
||||||
|
orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1',
|
||||||
|
)}
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -146,6 +155,7 @@ function calculateScale(index: number): number | string {
|
|||||||
min={control.min}
|
min={control.min}
|
||||||
max={control.max}
|
max={control.max}
|
||||||
step={control.step}
|
step={control.step}
|
||||||
|
{label}
|
||||||
{orientation}
|
{orientation}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,16 +172,6 @@ function calculateScale(index: number): number | string {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if label}
|
|
||||||
<div class="flex items-center gap-2 opacity-70">
|
|
||||||
<div class="w-1 h-1 rounded-full bg-foreground"></div>
|
|
||||||
<div class="w-px h-2 bg-text-muted/50"></div>
|
|
||||||
<span class="font-mono text-[8px] uppercase tracking-[0.2em] text-text-subtle font-medium">
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,20 @@ import {
|
|||||||
type SliderRootProps,
|
type SliderRootProps,
|
||||||
} from 'bits-ui';
|
} from 'bits-ui';
|
||||||
|
|
||||||
type Props = Omit<SliderRootProps, 'type' | 'onValueChange' | 'onValueCommit'> & {
|
type Props =
|
||||||
|
& Omit<
|
||||||
|
SliderRootProps,
|
||||||
|
'type' | 'onValueChange' | 'onValueCommit'
|
||||||
|
>
|
||||||
|
& {
|
||||||
/**
|
/**
|
||||||
* Slider value, numeric.
|
* Slider value, numeric.
|
||||||
*/
|
*/
|
||||||
value: number;
|
value: number;
|
||||||
|
/**
|
||||||
|
* Optional label displayed inline on the track before the filled range.
|
||||||
|
*/
|
||||||
|
label?: string;
|
||||||
/**
|
/**
|
||||||
* A callback function called when the value changes.
|
* A callback function called when the value changes.
|
||||||
* @param newValue - number
|
* @param newValue - number
|
||||||
@@ -26,11 +35,17 @@ type Props = Omit<SliderRootProps, 'type' | 'onValueChange' | 'onValueCommit'> &
|
|||||||
onValueCommit?: (newValue: number) => void;
|
onValueCommit?: (newValue: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { value = $bindable(), orientation = 'horizontal', class: className, ...rest }: Props = $props();
|
let {
|
||||||
|
value = $bindable(),
|
||||||
|
orientation = 'horizontal',
|
||||||
|
class: className,
|
||||||
|
label,
|
||||||
|
...rest
|
||||||
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Slider.Root
|
<Slider.Root
|
||||||
bind:value={value}
|
bind:value
|
||||||
class={cn(
|
class={cn(
|
||||||
'relative flex h-full w-6 touch-none select-none items-center justify-center',
|
'relative flex h-full w-6 touch-none select-none items-center justify-center',
|
||||||
orientation === 'horizontal' ? 'w-48 h-6' : 'w-6 h-48',
|
orientation === 'horizontal' ? 'w-48 h-6' : 'w-6 h-48',
|
||||||
@@ -41,13 +56,23 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{#snippet children(props)}
|
{#snippet children(props)}
|
||||||
|
{#if label && orientation === 'horizontal'}
|
||||||
|
<span class="absolute top-0 left-0 -translate-y-1/2 text-[0.5rem] uppercase text-gray-400">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<span
|
<span
|
||||||
{...props}
|
{...props}
|
||||||
class={cn('relative bg-background-muted rounded-full', orientation === 'horizontal' ? 'w-full h-px' : 'h-full w-px')}
|
class={cn(
|
||||||
|
'relative bg-background-muted rounded-full',
|
||||||
|
orientation === 'horizontal' ? 'w-full h-px' : 'h-full w-px',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<!-- Filled range with NO transition -->
|
|
||||||
<Slider.Range
|
<Slider.Range
|
||||||
class={cn('absolute bg-foreground rounded-full', orientation === 'horizontal' ? 'h-full' : 'w-full')}
|
class={cn(
|
||||||
|
'absolute bg-foreground rounded-full',
|
||||||
|
orientation === 'horizontal' ? 'h-full' : 'w-full',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Slider.Thumb
|
<Slider.Thumb
|
||||||
@@ -56,27 +81,32 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
|
|||||||
'group/thumb relative block',
|
'group/thumb relative block',
|
||||||
'size-2',
|
'size-2',
|
||||||
orientation === 'horizontal' ? '-top-1' : '-left-1',
|
orientation === 'horizontal' ? '-top-1' : '-left-1',
|
||||||
'rounded-sm',
|
'rounded-full',
|
||||||
'bg-foreground',
|
'bg-foreground',
|
||||||
// Glow shadow
|
// Glow shadow
|
||||||
'shadow-[0_0_6px_rgba(0,0,0,0.4)]',
|
'shadow-[0_0_6px_rgba(0,0,0,0.4)]',
|
||||||
// Smooth transitions only for size/position
|
// Smooth transitions only for size/position
|
||||||
'duration-200 ease-out',
|
'duration-200 ease-out',
|
||||||
orientation === 'horizontal' ? 'transition-[height,top,left,box-shadow]' : 'transition-[width,top,left,box-shadow]',
|
orientation === 'horizontal'
|
||||||
|
? 'transition-[height,top,left,box-shadow]'
|
||||||
|
: 'transition-[width,top,left,box-shadow]',
|
||||||
// Hover: bigger glow
|
// Hover: bigger glow
|
||||||
'hover:shadow-[0_0_10px_rgba(0,0,0,0.5)]',
|
'hover:shadow-[0_0_10px_rgba(0,0,0,0.5)]',
|
||||||
orientation === 'horizontal' ? 'hover:size-3 hover:-top-[5.5px]' : 'hover:size-3 hover:-left-[5.5px]',
|
orientation === 'horizontal'
|
||||||
|
? 'hover:size-3 hover:-top-[5.5px]'
|
||||||
|
: 'hover:size-3 hover:-left-[5.5px]',
|
||||||
// Active: smaller glow
|
// Active: smaller glow
|
||||||
'active:shadow-[0_0_4px_rgba(0,0,0,0.3)]',
|
'active:shadow-[0_0_4px_rgba(0,0,0,0.3)]',
|
||||||
orientation === 'horizontal' ? 'active:h-2.5 active:-top-[4.5px]' : 'active:w-2.5 active:-left-[4.5px]',
|
orientation === 'horizontal'
|
||||||
|
? 'active:h-2.5 active:-top-[4.5px]'
|
||||||
|
: 'active:w-2.5 active:-left-[4.5px]',
|
||||||
'focus:outline-none',
|
'focus:outline-none',
|
||||||
'cursor-grab active:cursor-grabbing',
|
'cursor-grab active:cursor-grabbing',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<!-- Soft glow on hover -->
|
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
absolute inset-0 rounded-sm
|
absolute inset-0 rounded-full
|
||||||
bg-background-20
|
bg-background-20
|
||||||
opacity-0 group-hover/thumb:opacity-100
|
opacity-0 group-hover/thumb:opacity-100
|
||||||
transition-opacity duration-200
|
transition-opacity duration-200
|
||||||
@@ -84,11 +114,12 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Value label -->
|
|
||||||
<span
|
<span
|
||||||
class={cn(
|
class={cn(
|
||||||
'absolute',
|
'absolute',
|
||||||
orientation === 'horizontal' ? '-top-8 left-1/2 -translate-x-1/2' : 'left-5 top-1/2 -translate-y-1/2',
|
orientation === 'horizontal'
|
||||||
|
? '-top-8 left-1/2 -translate-x-1/2'
|
||||||
|
: 'left-5 top-1/2 -translate-y-1/2',
|
||||||
'px-1.5 py-0.5 rounded-md',
|
'px-1.5 py-0.5 rounded-md',
|
||||||
'bg-foreground/90 backdrop-blur-sm',
|
'bg-foreground/90 backdrop-blur-sm',
|
||||||
'font-mono text-[0.625rem] font-medium text-background',
|
'font-mono text-[0.625rem] font-medium text-background',
|
||||||
|
|||||||
Reference in New Issue
Block a user