2026-01-18 15:55:07 +03:00
|
|
|
|
<!--
|
|
|
|
|
|
Component: ComboControl
|
2026-02-25 09:55:46 +03:00
|
|
|
|
Typography value control: surface +/– buttons flanking a two-line trigger
|
|
|
|
|
|
that opens a vertical slider popover.
|
2026-01-18 15:55:07 +03:00
|
|
|
|
-->
|
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-21 21:50:30 +03:00
|
|
|
|
import {
|
|
|
|
|
|
Content as PopoverContent,
|
|
|
|
|
|
Root as PopoverRoot,
|
|
|
|
|
|
Trigger as PopoverTrigger,
|
|
|
|
|
|
} from '$shared/shadcn/ui/popover';
|
2026-02-25 09:55:46 +03:00
|
|
|
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
|
|
|
|
import { Slider } from '$shared/ui';
|
|
|
|
|
|
import { Button } from '$shared/ui/Button';
|
2026-01-05 09:03:31 +03:00
|
|
|
|
import MinusIcon from '@lucide/svelte/icons/minus';
|
|
|
|
|
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
2026-02-25 09:55:46 +03:00
|
|
|
|
import TechText from '../TechText/TechText.svelte';
|
2026-01-05 09:03:31 +03:00
|
|
|
|
|
2026-02-07 11:24:44 +03:00
|
|
|
|
interface Props {
|
2026-03-02 22:19:35 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* Typography control
|
|
|
|
|
|
*/
|
2026-02-25 09:55:46 +03:00
|
|
|
|
control: TypographyControl;
|
2026-03-02 22:19:35 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* Control label
|
|
|
|
|
|
*/
|
2026-02-25 09:55:46 +03:00
|
|
|
|
label?: string;
|
2026-03-02 22:19:35 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* CSS classes
|
|
|
|
|
|
*/
|
2026-02-25 09:55:46 +03:00
|
|
|
|
class?: string;
|
2026-03-02 22:19:35 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* Reduced layout
|
|
|
|
|
|
* @default false
|
|
|
|
|
|
*/
|
2026-02-25 09:55:46 +03:00
|
|
|
|
reduced?: boolean;
|
2026-03-02 22:19:35 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* Increase button label
|
|
|
|
|
|
* @default 'Increase'
|
|
|
|
|
|
*/
|
2026-01-05 09:03:31 +03:00
|
|
|
|
increaseLabel?: string;
|
2026-03-02 22:19:35 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* Decrease button label
|
|
|
|
|
|
* @default 'Decrease'
|
|
|
|
|
|
*/
|
2026-01-05 09:03:31 +03:00
|
|
|
|
decreaseLabel?: string;
|
2026-03-02 22:19:35 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* Control aria label
|
|
|
|
|
|
*/
|
2026-01-05 09:03:31 +03:00
|
|
|
|
controlLabel?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 09:55:46 +03:00
|
|
|
|
let {
|
2026-01-07 16:53:17 +03:00
|
|
|
|
control,
|
2026-02-25 09:55:46 +03:00
|
|
|
|
label,
|
|
|
|
|
|
class: className,
|
2026-02-07 11:24:44 +03:00
|
|
|
|
reduced = false,
|
2026-02-25 09:55:46 +03:00
|
|
|
|
increaseLabel = 'Increase',
|
|
|
|
|
|
decreaseLabel = 'Decrease',
|
|
|
|
|
|
controlLabel,
|
2026-02-07 11:24:44 +03:00
|
|
|
|
}: Props = $props();
|
2026-01-05 09:03:31 +03:00
|
|
|
|
|
2026-02-25 09:55:46 +03:00
|
|
|
|
let open = $state(false);
|
2026-01-05 09:03:31 +03:00
|
|
|
|
|
2026-02-25 09:55:46 +03:00
|
|
|
|
// 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);
|
|
|
|
|
|
});
|
2026-01-05 09:03:31 +03:00
|
|
|
|
|
2026-02-25 09:55:46 +03:00
|
|
|
|
// Display label: prefer explicit prop, fall back to controlLabel
|
|
|
|
|
|
const displayLabel = $derived(label ?? controlLabel ?? '');
|
2026-01-05 09:03:31 +03:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-02-25 09:55:46 +03:00
|
|
|
|
<!--
|
2026-03-02 22:19:35 +03:00
|
|
|
|
REDUCED MODE
|
2026-02-25 09:55:46 +03:00
|
|
|
|
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
|
2026-03-02 22:19:35 +03:00
|
|
|
|
variant="icon"
|
|
|
|
|
|
size="sm"
|
2026-02-25 09:55:46 +03:00
|
|
|
|
onclick={control.decrease}
|
|
|
|
|
|
disabled={control.isAtMin}
|
|
|
|
|
|
aria-label={decreaseLabel}
|
|
|
|
|
|
>
|
2026-03-02 22:19:35 +03:00
|
|
|
|
{#snippet icon()}
|
|
|
|
|
|
<MinusIcon class="size-3.5 stroke-2" />
|
|
|
|
|
|
{/snippet}
|
2026-02-25 09:55:46 +03:00
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Trigger -->
|
|
|
|
|
|
<div class="relative mx-1">
|
|
|
|
|
|
<PopoverRoot bind:open>
|
2026-01-24 15:37:06 +03:00
|
|
|
|
<PopoverTrigger>
|
|
|
|
|
|
{#snippet child({ props })}
|
2026-02-25 09:55:46 +03:00
|
|
|
|
<button
|
2026-01-24 15:37:06 +03:00
|
|
|
|
{...props}
|
2026-02-25 09:55:46 +03:00
|
|
|
|
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
|
2026-03-04 16:51:49 +03:00
|
|
|
|
? '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',
|
2026-02-25 09:55:46 +03:00
|
|
|
|
)}
|
2026-01-24 15:37:06 +03:00
|
|
|
|
aria-label={controlLabel}
|
|
|
|
|
|
>
|
2026-02-25 09:55:46 +03:00
|
|
|
|
<!-- 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>
|
2026-01-24 15:37:06 +03:00
|
|
|
|
{/snippet}
|
|
|
|
|
|
</PopoverTrigger>
|
2026-02-25 09:55:46 +03:00
|
|
|
|
|
|
|
|
|
|
<!-- Vertical slider popover -->
|
|
|
|
|
|
<PopoverContent
|
2026-03-04 16:51:49 +03:00
|
|
|
|
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"
|
2026-02-25 09:55:46 +03:00
|
|
|
|
align="center"
|
|
|
|
|
|
side="top"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Slider
|
|
|
|
|
|
class="h-full"
|
|
|
|
|
|
bind:value={control.value}
|
|
|
|
|
|
min={control.min}
|
|
|
|
|
|
max={control.max}
|
|
|
|
|
|
step={control.step}
|
|
|
|
|
|
orientation="vertical"
|
|
|
|
|
|
/>
|
2026-01-24 15:37:06 +03:00
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</PopoverRoot>
|
2026-02-25 09:55:46 +03:00
|
|
|
|
</div>
|
2026-01-24 15:37:06 +03:00
|
|
|
|
|
2026-02-25 09:55:46 +03:00
|
|
|
|
<!-- Increase button -->
|
|
|
|
|
|
<Button
|
2026-03-02 22:19:35 +03:00
|
|
|
|
variant="icon"
|
|
|
|
|
|
size="sm"
|
2026-02-25 09:55:46 +03:00
|
|
|
|
onclick={control.increase}
|
|
|
|
|
|
disabled={control.isAtMax}
|
|
|
|
|
|
aria-label={increaseLabel}
|
|
|
|
|
|
>
|
2026-03-02 22:19:35 +03:00
|
|
|
|
{#snippet icon()}
|
|
|
|
|
|
<PlusIcon class="size-3.5 stroke-2" />
|
|
|
|
|
|
{/snippet}
|
2026-02-25 09:55:46 +03:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/if}
|