Files
frontend-svelte/src/shared/ui/ComboControl/ComboControl.svelte
Ilia Mashkov 9b90080c57
All checks were successful
Workflow / build (pull_request) Successful in 3m29s
Workflow / publish (pull_request) Has been skipped
chore: change hex colors to tailwind bariables
2026-03-04 16:51:49 +03:00

190 lines
5.9 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
Component: ComboControl
Typography value control: surface +/ buttons flanking a two-line trigger
that opens a vertical slider popover.
-->
<script lang="ts">
import type { TypographyControl } from '$shared/lib';
import {
Content as PopoverContent,
Root as PopoverRoot,
Trigger as PopoverTrigger,
} from '$shared/shadcn/ui/popover';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Slider } from '$shared/ui';
import { Button } from '$shared/ui/Button';
import MinusIcon from '@lucide/svelte/icons/minus';
import PlusIcon from '@lucide/svelte/icons/plus';
import TechText from '../TechText/TechText.svelte';
interface Props {
/**
* Typography control
*/
control: TypographyControl;
/**
* Control label
*/
label?: string;
/**
* CSS classes
*/
class?: string;
/**
* Reduced layout
* @default false
*/
reduced?: boolean;
/**
* Increase button label
* @default 'Increase'
*/
increaseLabel?: string;
/**
* Decrease button label
* @default 'Decrease'
*/
decreaseLabel?: string;
/**
* Control aria label
*/
controlLabel?: string;
}
let {
control,
label,
class: className,
reduced = false,
increaseLabel = 'Increase',
decreaseLabel = 'Decrease',
controlLabel,
}: Props = $props();
let open = $state(false);
// Smart value formatting matching the Figma design
const formattedValue = $derived(() => {
const v = control.value;
if (Number.isInteger(v)) return String(v);
return control.step < 0.1 ? v.toFixed(2) : v.toFixed(1);
});
// Display label: prefer explicit prop, fall back to controlLabel
const displayLabel = $derived(label ?? controlLabel ?? '');
</script>
<!--
REDUCED MODE
Inline slider + value. No buttons, no popover.
-->
{#if reduced}
<div
class={cn(
'flex gap-4 items-end w-full',
className,
)}
>
<Slider
class="w-full"
bind:value={control.value}
min={control.min}
max={control.max}
step={control.step}
orientation="horizontal"
/>
<span
class="font-mono text-[0.6875rem] text-neutral-500 dark:text-neutral-400 tabular-nums w-10 text-right shrink-0"
>
{formattedValue()}
</span>
</div>
<!-- ── FULL MODE ──────────────────────────────────────────────────────────────── -->
{:else}
<div class={cn('flex items-center px-1 relative', className)}>
<!-- Decrease button -->
<Button
variant="icon"
size="sm"
onclick={control.decrease}
disabled={control.isAtMin}
aria-label={decreaseLabel}
>
{#snippet icon()}
<MinusIcon class="size-3.5 stroke-2" />
{/snippet}
</Button>
<!-- Trigger -->
<div class="relative mx-1">
<PopoverRoot bind:open>
<PopoverTrigger>
{#snippet child({ props })}
<button
{...props}
class={cn(
'flex flex-col items-center justify-center w-14 py-1',
'select-none rounded-none transition-all duration-150',
'border border-transparent',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30',
open
? 'bg-paper dark:bg-dark-card shadow-sm border-black/5 dark:border-white/10'
: 'hover:bg-paper/50 dark:hover:bg-dark-card/50',
)}
aria-label={controlLabel}
>
<!-- Label row -->
{#if displayLabel}
<span
class="
text-[0.5625rem] font-primary font-bold tracking-tight uppercase
text-neutral-900 dark:text-neutral-100
mb-0.5 leading-none
"
>
{displayLabel}
</span>
{/if}
<!-- Value row -->
<TechText variant="muted" size="md">
{formattedValue()}
</TechText>
</button>
{/snippet}
</PopoverTrigger>
<!-- Vertical slider popover -->
<PopoverContent
class="w-auto py-4 px-3 h-64 flex items-center justify-center rounded-none border border-black/5 dark:border-white/10 shadow-sm bg-paper dark:bg-dark-card"
align="center"
side="top"
>
<Slider
class="h-full"
bind:value={control.value}
min={control.min}
max={control.max}
step={control.step}
orientation="vertical"
/>
</PopoverContent>
</PopoverRoot>
</div>
<!-- Increase button -->
<Button
variant="icon"
size="sm"
onclick={control.increase}
disabled={control.isAtMax}
aria-label={increaseLabel}
>
{#snippet icon()}
<PlusIcon class="size-3.5 stroke-2" />
{/snippet}
</Button>
</div>
{/if}