feat(ComboControlV2): rewrite controls to use custom bits-ui slider
This commit is contained in:
43
src/shared/ui/ComboControlV2/ComboControlV2.stories.svelte
Normal file
43
src/shared/ui/ComboControlV2/ComboControlV2.stories.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script module>
|
||||
import { createTypographyControl } from '$shared/lib';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import ComboControlV2 from './ComboControlV2.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Shared/ComboControlV2',
|
||||
component: ComboControlV2,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'ComboControl with input field and slider',
|
||||
},
|
||||
story: { inline: false }, // Render stories in iframe for state isolation
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
orientation: {
|
||||
control: 'select',
|
||||
options: ['horizontal', 'vertical'],
|
||||
description: 'Orientation of the ComboControl',
|
||||
defaultValue: 'vertical',
|
||||
},
|
||||
label: {
|
||||
control: 'text',
|
||||
description: 'Label for the ComboControl',
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const control = createTypographyControl({ min: 0, max: 100, step: 1, value: 50 });
|
||||
</script>
|
||||
|
||||
<Story name="Horizontal" args={{ orientation: 'horizontal', control }}>
|
||||
<ComboControlV2 control={control} orientation="horizontal" />
|
||||
</Story>
|
||||
|
||||
<Story name="Vertical" args={{ orientation: 'vertical', control, class: 'h-48' }}>
|
||||
<ComboControlV2 control={control} orientation="vertical" />
|
||||
</Story>
|
||||
@@ -4,62 +4,113 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { TypographyControl } from '$shared/lib';
|
||||
import { Input } from '$shared/shadcn/ui/input';
|
||||
import { Slider } from '$shared/shadcn/ui/slider';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import { Input } from '$shared/ui';
|
||||
import { Slider } from '$shared/ui';
|
||||
import type { Orientation } from 'bits-ui';
|
||||
import type { ChangeEventHandler } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Typography control instance
|
||||
* Control instance
|
||||
*/
|
||||
control: TypographyControl;
|
||||
/**
|
||||
* Orientation of the component
|
||||
* Orientation
|
||||
*/
|
||||
orientation?: Orientation;
|
||||
/**
|
||||
* Label text
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* CSS class
|
||||
*/
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
control,
|
||||
orientation = 'vertical',
|
||||
label,
|
||||
class: className,
|
||||
}: Props = $props();
|
||||
|
||||
let sliderValue = $state(Number(control.value));
|
||||
let inputValue = $state(String(control.value));
|
||||
|
||||
$effect(() => {
|
||||
sliderValue = Number(control?.value);
|
||||
inputValue = String(control.value);
|
||||
});
|
||||
|
||||
const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
||||
const parsedValue = parseFloat(event.currentTarget.value);
|
||||
if (!isNaN(parsedValue)) {
|
||||
control.value = parsedValue;
|
||||
inputValue = String(parsedValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSliderChange = (newValue: number) => {
|
||||
control.value = newValue;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class={cn('flex flex-col items-center gap-4 w-full', orientation === 'vertical' ? 'flex-col' : 'flex-row')}>
|
||||
<div
|
||||
class={cn(
|
||||
'flex gap-4 sm:p-4 rounded-xl transition-all duration-300',
|
||||
'backdrop-blur-md',
|
||||
orientation === 'horizontal' ? 'flex-row items-end w-full' : 'flex-col items-center h-full',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
value={control.value}
|
||||
class="h-10 rounded-lg w-12 pl-1 pr-1 sm:pr-1 md:pr-1 sm:pl-1 md:pl-1 text-center"
|
||||
value={inputValue}
|
||||
onchange={handleInputChange}
|
||||
min={control.min}
|
||||
max={control.max}
|
||||
class="w-14 h-8 text-xs text-center bg-white/40 border-none rounded-lg focus-visible:ring-indigo-500/50"
|
||||
step={control.step}
|
||||
/>
|
||||
|
||||
<div class={cn('relative', orientation === 'horizontal' ? 'w-full' : 'h-full')}>
|
||||
<div
|
||||
class={cn(
|
||||
'absolute flex justify-between',
|
||||
orientation === 'horizontal' ? 'flex-row w-full -top-5 px-0.5' : 'flex-col h-full -left-5 py-0.5',
|
||||
)}
|
||||
>
|
||||
{#each Array(5) as _, i}
|
||||
<div
|
||||
class={cn(
|
||||
'flex items-center gap-1.5',
|
||||
orientation === 'horizontal' ? 'flex-col' : 'flex-row',
|
||||
)}
|
||||
>
|
||||
<span class="font-mono text-[0.375rem] text-gray-400 tabular-nums">
|
||||
{
|
||||
Number.isInteger(control.step)
|
||||
? Math.round(control.min + (i * (control.max - control.min) / 4))
|
||||
: (control.min + (i * (control.max - control.min) / 4)).toFixed(2)
|
||||
}
|
||||
</span>
|
||||
<div class={cn('bg-gray-300', orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1')}></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Slider
|
||||
class={cn(orientation === 'horizontal' ? 'w-full' : 'h-full')}
|
||||
bind:value={control.value}
|
||||
min={control.min}
|
||||
max={control.max}
|
||||
step={control.step}
|
||||
value={sliderValue}
|
||||
onValueChange={handleSliderChange}
|
||||
type="single"
|
||||
orientation={orientation}
|
||||
class={cn(orientation === 'vertical' ? 'h-30' : 'w-full')}
|
||||
{orientation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if label}
|
||||
<div class="flex items-center gap-2 opacity-70">
|
||||
<div class="w-1 h-1 rounded-full bg-gray-900"></div>
|
||||
<div class="w-px h-2 bg-gray-400/50"></div>
|
||||
<span class="font-mono text-[8px] uppercase tracking-[0.2em] text-gray-500 font-medium">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user