feature/responsive #22

Merged
ilia merged 49 commits from feature/responsive into main 2026-02-09 06:49:25 +00:00
Showing only changes of commit ce9665a842 - Show all commits

View File

@@ -4,11 +4,29 @@
-->
<script lang="ts">
import type { TypographyControl } from '$shared/lib';
import { Button } from '$shared/shadcn/ui/button';
import { Root as ButtonGroupRoot } from '$shared/shadcn/ui/button-group';
import {
Content as PopoverContent,
Root as PopoverRoot,
Trigger as PopoverTrigger,
} from '$shared/shadcn/ui/popover';
import {
Content as TooltipContent,
Root as TooltipRoot,
Trigger as TooltipTrigger,
} from '$shared/shadcn/ui/tooltip';
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 MinusIcon from '@lucide/svelte/icons/minus';
import PlusIcon from '@lucide/svelte/icons/plus';
import {
type Orientation,
REGEXP_ONLY_DIGITS,
} from 'bits-ui';
import type { ChangeEventHandler } from 'svelte/elements';
import IconButton from '../IconButton/IconButton.svelte';
interface Props {
/**
@@ -27,6 +45,28 @@ interface Props {
* CSS class
*/
class?: string;
/**
* Show scale flag
*/
showScale?: boolean;
/**
* Flag that change component appearance
* from the one with increase/decrease buttons and popover with input + slider
* to just input + slider
*/
reduced?: boolean;
/**
* Text for increase button aria-label
*/
increaseLabel?: string;
/**
* Text for decrease button aria-label
*/
decreaseLabel?: string;
/**
* Text for control button aria-label
*/
controlLabel?: string;
}
let {
@@ -34,6 +74,11 @@ let {
orientation = 'vertical',
label,
class: className,
showScale = true,
reduced = false,
increaseLabel = 'Increase',
decreaseLabel = 'Decrease',
controlLabel,
}: Props = $props();
let inputValue = $state(String(control.value));
@@ -49,26 +94,29 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
inputValue = String(parsedValue);
}
};
function calculateScale(index: number): number | string {
const calculate = () =>
orientation === 'horizontal'
? (control.min + (index * (control.max - control.min) / 4))
: (control.max - (index * (control.max - control.min) / 4));
return Number.isInteger(control.step)
? Math.round(calculate())
: (calculate()).toFixed(2);
}
</script>
<div
{#snippet ComboControl()}
<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
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}
step={control.step}
/>
>
<div class={cn('relative', orientation === 'horizontal' ? 'w-full' : 'h-full')}>
{#if showScale}
<div
class={cn(
'absolute flex justify-between',
@@ -83,16 +131,14 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
)}
>
<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)
}
{calculateScale(i)}
</span>
<div class={cn('bg-gray-300', orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1')}></div>
<div class={cn('bg-gray-300', orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1')}>
</div>
</div>
{/each}
</div>
{/if}
<Slider
class={cn(orientation === 'horizontal' ? 'w-full' : 'h-full')}
@@ -104,6 +150,17 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
/>
</div>
<Input
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}
step={control.step}
pattern={REGEXP_ONLY_DIGITS}
variant="ghost"
/>
{#if label}
<div class="flex items-center gap-2 opacity-70">
<div class="w-1 h-1 rounded-full bg-gray-900"></div>
@@ -113,4 +170,60 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
</span>
</div>
{/if}
</div>
</div>
{/snippet}
{#if reduced}
{@render ComboControl()}
{:else}
<TooltipRoot>
<ButtonGroupRoot class="bg-transparent border-none shadow-none">
<TooltipTrigger class="flex items-center">
<IconButton
onclick={control.decrease}
disabled={control.isAtMin}
aria-label={decreaseLabel}
rotation="counterclockwise"
>
{#snippet icon({ className })}
<MinusIcon class={className} />
{/snippet}
</IconButton>
<PopoverRoot>
<PopoverTrigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
class="hover:bg-white/50 hover:font-bold bg-white/20 border-none duration-150 will-change-transform active:scale-95 cursor-pointer"
size="icon"
aria-label={controlLabel}
>
{control.value}
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="w-auto h-64 sm:px-1 py-0">
{@render ComboControl()}
</PopoverContent>
</PopoverRoot>
<IconButton
aria-label={increaseLabel}
onclick={control.increase}
disabled={control.isAtMax}
rotation="clockwise"
>
{#snippet icon({ className })}
<PlusIcon class={className} />
{/snippet}
</IconButton>
</TooltipTrigger>
</ButtonGroupRoot>
{#if controlLabel}
<TooltipContent>
{controlLabel}
</TooltipContent>
{/if}
</TooltipRoot>
{/if}