Compare commits
4 Commits
b4e97da3a0
...
6f840fbad8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f840fbad8 | ||
|
|
a7d08a9329 | ||
|
|
df2d6bae3b | ||
|
|
ce9665a842 |
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user