190 lines
5.9 KiB
Svelte
190 lines
5.9 KiB
Svelte
<!--
|
||
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}
|