2026-01-18 15:55:07 +03:00
|
|
|
<!--
|
|
|
|
|
Component: ComboControl
|
|
|
|
|
Provides multiple ways to change certain value
|
|
|
|
|
- via Increase/Decrease buttons
|
|
|
|
|
- via Slider
|
|
|
|
|
- via Input
|
|
|
|
|
-->
|
2026-01-05 09:03:31 +03:00
|
|
|
<script lang="ts">
|
2026-01-07 16:53:17 +03:00
|
|
|
import type { TypographyControl } from '$shared/lib';
|
2026-01-05 09:03:31 +03:00
|
|
|
import { Button } from '$shared/shadcn/ui/button';
|
2026-01-21 21:50:30 +03:00
|
|
|
import { Root as ButtonGroupRoot } from '$shared/shadcn/ui/button-group';
|
2026-01-05 09:03:31 +03:00
|
|
|
import { Input } from '$shared/shadcn/ui/input';
|
2026-01-21 21:50:30 +03:00
|
|
|
import {
|
|
|
|
|
Content as PopoverContent,
|
|
|
|
|
Root as PopoverRoot,
|
|
|
|
|
Trigger as PopoverTrigger,
|
|
|
|
|
} from '$shared/shadcn/ui/popover';
|
2026-01-05 09:03:31 +03:00
|
|
|
import { Slider } from '$shared/shadcn/ui/slider';
|
2026-01-24 15:37:06 +03:00
|
|
|
import {
|
|
|
|
|
Content as TooltipContent,
|
|
|
|
|
Root as TooltipRoot,
|
|
|
|
|
Trigger as TooltipTrigger,
|
|
|
|
|
} from '$shared/shadcn/ui/tooltip';
|
2026-01-05 09:03:31 +03:00
|
|
|
import MinusIcon from '@lucide/svelte/icons/minus';
|
|
|
|
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
|
|
|
|
import type { ChangeEventHandler } from 'svelte/elements';
|
|
|
|
|
|
|
|
|
|
interface ComboControlProps {
|
|
|
|
|
/**
|
|
|
|
|
* Text for increase button aria-label
|
|
|
|
|
*/
|
|
|
|
|
increaseLabel?: string;
|
|
|
|
|
/**
|
|
|
|
|
* Text for decrease button aria-label
|
|
|
|
|
*/
|
|
|
|
|
decreaseLabel?: string;
|
|
|
|
|
/**
|
|
|
|
|
* Text for control button aria-label
|
|
|
|
|
*/
|
|
|
|
|
controlLabel?: string;
|
|
|
|
|
/**
|
2026-01-07 16:53:17 +03:00
|
|
|
* Control instance
|
2026-01-05 09:03:31 +03:00
|
|
|
*/
|
2026-01-07 16:53:17 +03:00
|
|
|
control: TypographyControl;
|
2026-01-05 09:03:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {
|
2026-01-07 16:53:17 +03:00
|
|
|
control,
|
2026-01-05 09:03:31 +03:00
|
|
|
decreaseLabel,
|
2026-01-07 16:53:17 +03:00
|
|
|
increaseLabel,
|
2026-01-05 09:03:31 +03:00
|
|
|
controlLabel,
|
|
|
|
|
}: ComboControlProps = $props();
|
|
|
|
|
|
|
|
|
|
// Local state for the slider to prevent infinite loops
|
2026-01-08 13:14:04 +03:00
|
|
|
// svelte-ignore state_referenced_locally - $state captures initial value, $effect syncs updates
|
2026-01-07 16:53:17 +03:00
|
|
|
let sliderValue = $state(Number(control.value));
|
2026-01-05 09:03:31 +03:00
|
|
|
|
|
|
|
|
// Sync sliderValue when external value changes
|
|
|
|
|
$effect(() => {
|
2026-01-07 16:53:17 +03:00
|
|
|
sliderValue = Number(control.value);
|
2026-01-05 09:03:31 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
|
|
|
|
const parsedValue = parseFloat(event.currentTarget.value);
|
|
|
|
|
if (!isNaN(parsedValue)) {
|
2026-01-07 16:53:17 +03:00
|
|
|
control.value = parsedValue;
|
2026-01-05 09:03:31 +03:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle slider value change.
|
|
|
|
|
* The Slider component passes the value as a number directly.
|
|
|
|
|
*/
|
2026-01-07 16:53:17 +03:00
|
|
|
const handleSliderChange = (newValue: number) => {
|
|
|
|
|
control.value = newValue;
|
2026-01-05 09:03:31 +03:00
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
2026-01-24 15:37:06 +03:00
|
|
|
<TooltipRoot>
|
|
|
|
|
<ButtonGroupRoot class="bg-transparent border-none shadow-none">
|
|
|
|
|
<TooltipTrigger class="flex items-center">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
class="
|
|
|
|
|
group relative border-none size-9
|
|
|
|
|
bg-white/20 hover:bg-white/60
|
2026-01-26 12:49:15 +03:00
|
|
|
backdrop-blur-3xl
|
2026-01-24 15:37:06 +03:00
|
|
|
transition-all duration-200 ease-out
|
|
|
|
|
will-change-transform
|
|
|
|
|
hover:-translate-y-0.5
|
|
|
|
|
active:translate-y-0 active:scale-95 active:shadow-none
|
|
|
|
|
cursor-pointer
|
|
|
|
|
disabled:opacity-50 disabled:pointer-events-none
|
|
|
|
|
"
|
|
|
|
|
size="icon"
|
|
|
|
|
onclick={control.decrease}
|
|
|
|
|
disabled={control.isAtMin}
|
|
|
|
|
aria-label={decreaseLabel}
|
|
|
|
|
>
|
|
|
|
|
<MinusIcon
|
|
|
|
|
class="
|
|
|
|
|
size-4 transition-all duration-200
|
|
|
|
|
stroke-slate-600/50
|
|
|
|
|
group-hover:stroke-indigo-500 group-hover:scale-110 group-hover:stroke-3
|
|
|
|
|
group-active:scale-90 group-active:-rotate-6
|
|
|
|
|
group-disabled:stroke-transparent
|
|
|
|
|
"
|
2026-01-05 09:03:31 +03:00
|
|
|
/>
|
2026-01-24 15:37:06 +03:00
|
|
|
</Button>
|
|
|
|
|
<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 p-4">
|
|
|
|
|
<div class="flex flex-col items-center gap-3">
|
|
|
|
|
<Slider
|
|
|
|
|
min={control.min}
|
|
|
|
|
max={control.max}
|
|
|
|
|
step={control.step}
|
|
|
|
|
value={sliderValue}
|
|
|
|
|
onValueChange={handleSliderChange}
|
|
|
|
|
type="single"
|
|
|
|
|
orientation="vertical"
|
|
|
|
|
class="h-48"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
value={control.value}
|
|
|
|
|
onchange={handleInputChange}
|
|
|
|
|
min={control.min}
|
|
|
|
|
max={control.max}
|
|
|
|
|
class="w-16 text-center"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</PopoverRoot>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
class="
|
|
|
|
|
group relative border-none size-9
|
|
|
|
|
bg-white/20 hover:bg-white/60
|
|
|
|
|
transition-all duration-200 ease-out
|
|
|
|
|
will-change-transform
|
|
|
|
|
hover:-translate-y-0.5
|
|
|
|
|
active:translate-y-0 active:scale-95 active:shadow-none
|
|
|
|
|
disabled:opacity-50 disabled:pointer-events-none
|
|
|
|
|
cursor-pointer
|
|
|
|
|
"
|
|
|
|
|
size="icon"
|
|
|
|
|
aria-label={increaseLabel}
|
|
|
|
|
onclick={control.increase}
|
|
|
|
|
disabled={control.isAtMax}
|
|
|
|
|
>
|
|
|
|
|
<PlusIcon
|
|
|
|
|
class="
|
|
|
|
|
size-4 transition-all duration-200
|
|
|
|
|
stroke-slate-600/50
|
|
|
|
|
group-hover:stroke-indigo-500 group-hover:scale-110 group-hover:stroke-3
|
|
|
|
|
group-active:scale-90 group-active:rotate-6
|
|
|
|
|
group-disabled:stroke-transparent
|
|
|
|
|
"
|
2026-01-05 09:03:31 +03:00
|
|
|
/>
|
2026-01-24 15:37:06 +03:00
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
</ButtonGroupRoot>
|
|
|
|
|
{#if controlLabel}
|
|
|
|
|
<TooltipContent>
|
|
|
|
|
{controlLabel}
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
{/if}
|
|
|
|
|
</TooltipRoot>
|