feature/responsive #22

Merged
ilia merged 49 commits from feature/responsive into main 2026-02-09 06:49:25 +00:00
2 changed files with 116 additions and 22 deletions
Showing only changes of commit f4875d7324 - Show all commits

View 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>

View File

@@ -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"
/>
<Slider
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')}
/>
<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}
{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>