Compare commits

..

4 Commits

4 changed files with 209 additions and 108 deletions

View File

@@ -11,7 +11,6 @@ import {
} from '$shared/shadcn/ui/item'; } from '$shared/shadcn/ui/item';
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
ComboControl,
ComboControlV2, ComboControlV2,
Drawer, Drawer,
IconButton, IconButton,
@@ -91,6 +90,7 @@ $effect(() => {
<ComboControlV2 <ComboControlV2
control={control.instance} control={control.instance}
orientation="horizontal" orientation="horizontal"
reduced
/> />
{/each} {/each}
</div> </div>
@@ -105,12 +105,12 @@ $effect(() => {
<div class="sm:py-2 sm:px-10 flex flex-row items-center gap-2"> <div class="sm:py-2 sm:px-10 flex flex-row items-center gap-2">
<div class="flex flex-row gap-3"> <div class="flex flex-row gap-3">
{#each controlManager.controls as control (control.id)} {#each controlManager.controls as control (control.id)}
<ComboControl <ComboControlV2
control={control.instance} control={control.instance}
increaseLabel={control.increaseLabel} increaseLabel={control.increaseLabel}
decreaseLabel={control.decreaseLabel} decreaseLabel={control.decreaseLabel}
controlLabel={control.controlLabel} controlLabel={control.controlLabel}
reduced={responsive.isMobile} orientation="vertical"
/> />
{/each} {/each}
</div> </div>

View File

@@ -4,11 +4,29 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { TypographyControl } from '$shared/lib'; 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 { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Input } from '$shared/ui'; import { Input } from '$shared/ui';
import { Slider } 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 type { ChangeEventHandler } from 'svelte/elements';
import IconButton from '../IconButton/IconButton.svelte';
interface Props { interface Props {
/** /**
@@ -27,6 +45,28 @@ interface Props {
* CSS class * CSS class
*/ */
class?: string; 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 { let {
@@ -34,6 +74,11 @@ let {
orientation = 'vertical', orientation = 'vertical',
label, label,
class: className, class: className,
showScale = true,
reduced = false,
increaseLabel = 'Increase',
decreaseLabel = 'Decrease',
controlLabel,
}: Props = $props(); }: Props = $props();
let inputValue = $state(String(control.value)); let inputValue = $state(String(control.value));
@@ -49,68 +94,136 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
inputValue = String(parsedValue); 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> </script>
<div {#snippet ComboControl()}
class={cn( <div
'flex gap-4 sm:p-4 rounded-xl transition-all duration-300', class={cn(
'backdrop-blur-md', 'flex gap-4 sm:p-4 rounded-xl transition-all duration-300',
orientation === 'horizontal' ? 'flex-row items-end w-full' : 'flex-col items-center h-full', 'backdrop-blur-md',
className, 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" <div class={cn('relative', orientation === 'horizontal' ? 'w-full' : 'h-full')}>
value={inputValue} {#if showScale}
onchange={handleInputChange}
min={control.min}
max={control.max}
step={control.step}
/>
<div class={cn('relative', orientation === 'horizontal' ? 'w-full' : 'h-full')}>
<div
class={cn(
'absolute flex justify-between',
orientation === 'horizontal' ? 'flex-row w-full -top-5 px-0.5' : 'flex-col h-full -left-5 py-0.5',
)}
>
{#each Array(5) as _, i}
<div <div
class={cn( class={cn(
'flex items-center gap-1.5', 'absolute flex justify-between',
orientation === 'horizontal' ? 'flex-col' : 'flex-row', orientation === 'horizontal' ? 'flex-row w-full -top-5 px-0.5' : 'flex-col h-full -left-5 py-0.5',
)} )}
> >
<span class="font-mono text-[0.375rem] text-gray-400 tabular-nums"> {#each Array(5) as _, i}
{ <div
Number.isInteger(control.step) class={cn(
? Math.round(control.min + (i * (control.max - control.min) / 4)) 'flex items-center gap-1.5',
: (control.min + (i * (control.max - control.min) / 4)).toFixed(2) orientation === 'horizontal' ? 'flex-col' : 'flex-row',
} )}
</span> >
<div class={cn('bg-gray-300', orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1')}></div> <span class="font-mono text-[0.375rem] text-gray-400 tabular-nums">
{calculateScale(i)}
</span>
<div class={cn('bg-gray-300', orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1')}>
</div>
</div>
{/each}
</div> </div>
{/each} {/if}
<Slider
class={cn(orientation === 'horizontal' ? 'w-full' : 'h-full')}
bind:value={control.value}
min={control.min}
max={control.max}
step={control.step}
{orientation}
/>
</div> </div>
<Slider <Input
class={cn(orientation === 'horizontal' ? 'w-full' : 'h-full')} 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"
bind:value={control.value} value={inputValue}
onchange={handleInputChange}
min={control.min} min={control.min}
max={control.max} max={control.max}
step={control.step} step={control.step}
{orientation} pattern={REGEXP_ONLY_DIGITS}
variant="ghost"
/> />
</div>
{#if label} {#if label}
<div class="flex items-center gap-2 opacity-70"> <div class="flex items-center gap-2 opacity-70">
<div class="w-1 h-1 rounded-full bg-gray-900"></div> <div class="w-1 h-1 rounded-full bg-gray-900"></div>
<div class="w-px h-2 bg-gray-400/50"></div> <div class="w-px h-2 bg-gray-400/50"></div>
<span class="font-mono text-[8px] uppercase tracking-[0.2em] text-gray-500 font-medium"> <span class="font-mono text-[8px] uppercase tracking-[0.2em] text-gray-500 font-medium">
{label} {label}
</span> </span>
</div> </div>
{/if} {/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}

View File

@@ -16,22 +16,29 @@ type Props = ComponentProps<typeof Input> & {
* Additional CSS classes for the container * Additional CSS classes for the container
*/ */
class?: string; class?: string;
variant?: 'default' | 'ghost';
}; };
let { let {
value = $bindable(''), value = $bindable(''),
class: className, class: className,
variant = 'default',
...rest ...rest
}: Props = $props(); }: Props = $props();
const isGhost = $derived(variant === 'ghost');
</script> </script>
<Input <Input
bind:value={value} bind:value={value}
class={cn( class={cn(
'h-12 sm:h-14 md:h-16 w-full text-sm sm:text-base', 'h-12 sm:h-14 md:h-16 w-full text-sm sm:text-base',
'backdrop-blur-md bg-white/80', 'backdrop-blur-md',
isGhost ? 'bg-transparent' : 'bg-white/80',
'border border-gray-300/50', 'border border-gray-300/50',
'shadow-[0_1px_3px_rgba(0,0,0,0.04)]', isGhost ? 'border-transparent' : 'border-gray-300/50',
isGhost ? 'shadow-none' : 'shadow-[0_1px_3px_rgba(0,0,0,0.04)]',
'focus-visible:border-gray-400/60', 'focus-visible:border-gray-400/60',
'focus-visible:outline-none', 'focus-visible:outline-none',
'focus-visible:ring-1', 'focus-visible:ring-1',

View File

@@ -13,6 +13,7 @@ import {
} from '$shared/ui'; } from '$shared/ui';
import { comparisonStore } from '$widgets/ComparisonSlider/model'; import { comparisonStore } from '$widgets/ComparisonSlider/model';
import AArrowUP from '@lucide/svelte/icons/a-arrow-up'; import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
import { type Orientation } from 'bits-ui';
import { Spring } from 'svelte/motion'; import { Spring } from 'svelte/motion';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@@ -106,6 +107,26 @@ $effect(() => {
}); });
</script> </script>
{#snippet InputComponent(className: string)}
<Input
class={className}
bind:value={comparisonStore.text}
disabled={isDragging}
onfocusin={handleInputFocus}
placeholder="The quick brown fox..."
/>
{/snippet}
{#snippet Controls(className: string, orientation: Orientation)}
{#if typography.weightControl && typography.sizeControl && typography.heightControl}
<div class={className}>
<ComboControlV2 control={typography.weightControl} {orientation} reduced />
<ComboControlV2 control={typography.sizeControl} {orientation} reduced />
<ComboControlV2 control={typography.heightControl} {orientation} reduced />
</div>
{/if}
{/snippet}
<div <div
class="z-50 will-change-transform" class="z-50 will-change-transform"
style:transform=" style:transform="
@@ -117,20 +138,8 @@ $effect(() => {
> >
{#if staticPosition} {#if staticPosition}
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<Input {@render InputComponent?.('p-6')}
class="p-6" {@render Controls?.('flex flex-col justify-between items-center-safe gap-6', 'horizontal')}
bind:value={comparisonStore.text}
disabled={isDragging}
onfocusin={handleInputFocus}
placeholder="The quick brown fox..."
/>
{#if typography.weightControl && typography.sizeControl && typography.heightControl}
<div class="flex flex-col justify-between items-center-safe gap-6">
<ComboControlV2 control={typography.weightControl} orientation="horizontal" />
<ComboControlV2 control={typography.sizeControl} orientation="horizontal" />
<ComboControlV2 control={typography.heightControl} orientation="horizontal" />
</div>
{/if}
</div> </div>
{:else} {:else}
<ExpandableWrapper <ExpandableWrapper
@@ -159,46 +168,18 @@ $effect(() => {
{/snippet} {/snippet}
{#snippet visibleContent()} {#snippet visibleContent()}
<div class="relative"> {@render InputComponent(cn(
<!-- 'pl-1 sm:pl-3 pr-1 sm:pr-3',
<Input 'h-6 sm:h-8 md:h-10',
bind:value={comparisonStore.text} 'rounded-lg',
disabled={isDragging} isActive
onfocusin={handleInputFocus} ? 'h-7 sm:h-8 text-[0.825rem]'
class={cn( : 'bg-transparent shadow-none border-none p-0 h-auto text-sm sm:text-base font-medium focus-visible:ring-0 text-slate-900/50',
isActive ))}
? 'h-7 sm:h-8 text-[11px] sm:text-xs text-center bg-white/40 border-none rounded-lg focus-visible:ring-indigo-500/50 text-slate-900'
: 'bg-transparent shadow-none border-none p-0 h-auto text-sm sm:text-base font-medium focus-visible:ring-0 text-slate-900/50',
' placeholder:text-slate-400 selection:bg-indigo-100 flex-1 transition-all duration-350 w-44 sm:w-56',
)}
placeholder="The quick brown fox..."
/>
-->
<Input
class={cn(
'pl-1 sm:pl-3 pr-1 sm:pr-3',
'h-6 sm:h-8 md:h-10',
'rounded-lg',
isActive
? 'h-7 sm:h-8 text-[0.825rem]'
: 'bg-transparent shadow-none border-none p-0 h-auto text-sm sm:text-base font-medium focus-visible:ring-0 text-slate-900/50',
)}
bind:value={comparisonStore.text}
disabled={isDragging}
onfocusin={handleInputFocus}
placeholder="The quick brown fox..."
/>
</div>
{/snippet} {/snippet}
{#snippet hiddenContent()} {#snippet hiddenContent()}
{#if typography.weightControl && typography.sizeControl && typography.heightControl} {@render Controls?.('flex flex-row justify-between items-center-safe gap-2 sm:gap-0 h-64', 'vertical')}
<div class="flex flex-row justify-between items-center-safe gap-2 sm:gap-0">
<ComboControlV2 control={typography.weightControl} orientation="vertical" />
<ComboControlV2 control={typography.sizeControl} orientation="vertical" />
<ComboControlV2 control={typography.heightControl} orientation="vertical" />
</div>
{/if}
{/snippet} {/snippet}
</ExpandableWrapper> </ExpandableWrapper>
{/if} {/if}