feature/comparison-slider #19
@@ -3,9 +3,9 @@ import type { TypographyControl } from '$shared/lib';
|
|||||||
import { Input } from '$shared/shadcn/ui/input';
|
import { Input } from '$shared/shadcn/ui/input';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import { ComboControlV2 } from '$shared/ui';
|
import { ComboControlV2 } from '$shared/ui';
|
||||||
|
import { ExpandableWrapper } from '$shared/ui';
|
||||||
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
|
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
|
||||||
import { Spring } from 'svelte/motion';
|
import { Spring } from 'svelte/motion';
|
||||||
import { slide } from 'svelte/transition';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
wrapper?: HTMLDivElement | null;
|
wrapper?: HTMLDivElement | null;
|
||||||
@@ -29,40 +29,27 @@ let {
|
|||||||
heightControl,
|
heightControl,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let panelWidth = $state(0);
|
let panelWidth = $derived(wrapper?.clientWidth ?? 0);
|
||||||
const margin = 24;
|
const margin = 24;
|
||||||
let side = $state<'left' | 'right'>('left');
|
let side = $state<'left' | 'right'>('left');
|
||||||
|
|
||||||
// Unified active state for the entire wrapper
|
// Unified active state for the entire wrapper
|
||||||
let isActive = $state(false);
|
let isActive = $state(false);
|
||||||
|
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
function handleWrapperClick() {
|
const xSpring = new Spring(0, {
|
||||||
if (!isDragging) {
|
stiffness: 0.14, // Lower is slower
|
||||||
isActive = true;
|
damping: 0.5, // Settle
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function handleClickOutside(e: MouseEvent) {
|
const rotateSpring = new Spring(0, {
|
||||||
if (wrapper && !wrapper.contains(e.target as Node)) {
|
stiffness: 0.12,
|
||||||
isActive = false;
|
damping: 0.55,
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function handleInputFocus() {
|
function handleInputFocus() {
|
||||||
isActive = true;
|
isActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleWrapperClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActive && e.key === 'Escape') {
|
|
||||||
isActive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Movement Logic
|
// Movement Logic
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (containerWidth === 0 || panelWidth === 0) return;
|
if (containerWidth === 0 || panelWidth === 0) return;
|
||||||
@@ -78,144 +65,80 @@ $effect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// The "Dodge"
|
|
||||||
const xSpring = new Spring(0, {
|
|
||||||
stiffness: 0.14, // Lower is slower
|
|
||||||
damping: 0.5, // Settle
|
|
||||||
});
|
|
||||||
|
|
||||||
// The "Focus"
|
|
||||||
const ySpring = new Spring(0, {
|
|
||||||
stiffness: 0.32,
|
|
||||||
damping: 0.65,
|
|
||||||
});
|
|
||||||
|
|
||||||
// The "Rise"
|
|
||||||
const scaleSpring = new Spring(1, {
|
|
||||||
stiffness: 0.32,
|
|
||||||
damping: 0.65,
|
|
||||||
});
|
|
||||||
|
|
||||||
// The "Lean"
|
|
||||||
const rotateSpring = new Spring(0, {
|
|
||||||
stiffness: 0.12,
|
|
||||||
damping: 0.55,
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const targetX = side === 'right' ? containerWidth - panelWidth - margin * 2 : 0;
|
const targetX = side === 'right' ? containerWidth - panelWidth - margin * 2 : 0;
|
||||||
if (containerWidth > 0 && panelWidth > 0 && !isActive) {
|
if (containerWidth > 0 && panelWidth > 0) {
|
||||||
// On side change set the position and the rotation
|
// On side change set the position and the rotation
|
||||||
xSpring.target = targetX;
|
xSpring.target = targetX;
|
||||||
rotateSpring.target = side === 'right' ? 3.5 : -3.5;
|
rotateSpring.target = side === 'right' ? 3.5 : -3.5;
|
||||||
|
|
||||||
setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
rotateSpring.target = 0;
|
rotateSpring.target = 0;
|
||||||
}, 600);
|
}, 600);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Elevation and scale on focus and mouse over
|
return () => {
|
||||||
$effect(() => {
|
if (timeoutId) {
|
||||||
if (isActive && !isDragging) {
|
clearTimeout(timeoutId);
|
||||||
// Lift up
|
}
|
||||||
ySpring.target = 8;
|
};
|
||||||
// Slightly bigger
|
|
||||||
scaleSpring.target = 1.1;
|
|
||||||
|
|
||||||
rotateSpring.target = side === 'right' ? -1.1 : 1.1;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
rotateSpring.target = 0;
|
|
||||||
scaleSpring.target = 1.05;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
ySpring.target = 0;
|
|
||||||
scaleSpring.target = 1;
|
|
||||||
rotateSpring.target = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (isDragging) {
|
|
||||||
isActive = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click outside handler
|
|
||||||
$effect(() => {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
document.addEventListener('click', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('click', handleClickOutside);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onclick={handleWrapperClick}
|
class="absolute top-6 left-6 z-50 will-change-transform"
|
||||||
bind:this={wrapper}
|
|
||||||
bind:clientWidth={panelWidth}
|
|
||||||
class={cn(
|
|
||||||
'absolute top-6 left-6 z-50 will-change-transform transition-opacity duration-300 flex items-top gap-1.5',
|
|
||||||
panelWidth === 0 ? 'opacity-0' : 'opacity-100',
|
|
||||||
)}
|
|
||||||
style:pointer-events={isDragging ? 'none' : 'auto'}
|
|
||||||
style:transform="
|
style:transform="
|
||||||
translate({xSpring.current}px, {ySpring.current}px)
|
translateX({xSpring.current}px)
|
||||||
scale({scaleSpring.current})
|
|
||||||
rotateZ({rotateSpring.current}deg)
|
rotateZ({rotateSpring.current}deg)
|
||||||
"
|
"
|
||||||
role="button"
|
|
||||||
tabindex={0}
|
|
||||||
onkeydown={handleKeyDown}
|
|
||||||
aria-label="Font controls"
|
|
||||||
>
|
>
|
||||||
<div
|
<ExpandableWrapper
|
||||||
|
bind:element={wrapper}
|
||||||
|
bind:expanded={isActive}
|
||||||
|
disabled={isDragging}
|
||||||
|
aria-label="Font controls"
|
||||||
|
rotation={side === 'right' ? 'counterclockwise' : 'clockwise'}
|
||||||
class={cn(
|
class={cn(
|
||||||
'animate-nudge relative transition-all',
|
'transition-opacity flex items-top gap-1.5',
|
||||||
side === 'left' ? 'order-2' : 'order-0',
|
panelWidth === 0 ? 'opacity-0' : 'opacity-100',
|
||||||
isActive ? 'opacity-0' : 'opacity-100',
|
|
||||||
isDragging && 'opacity-80 grayscale-[0.2]',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<AArrowUP class={cn('size-3', 'stroke-indigo-600')} />
|
{#snippet badge()}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={cn(
|
|
||||||
'relative p-2 rounded-2xl border transition-all duration-250 ease-out flex flex-col gap-1.5',
|
|
||||||
isActive
|
|
||||||
? 'bg-white/95 border-indigo-400/40 shadow-[0_30px_70px_-10px_rgba(99,102,241,0.25)]'
|
|
||||||
: ' bg-white/25 border-white/40 shadow-[0_12px_40px_-12px_rgba(0,0,0,0.12)]',
|
|
||||||
isDragging && 'opacity-80 grayscale-[0.2]',
|
|
||||||
)}
|
|
||||||
style:backdrop-filter="blur(24px)"
|
|
||||||
>
|
|
||||||
<div class="relative px-2 py-1">
|
|
||||||
<Input
|
|
||||||
bind:value={text}
|
|
||||||
disabled={isDragging}
|
|
||||||
onfocusin={handleInputFocus}
|
|
||||||
class={cn(
|
|
||||||
isActive
|
|
||||||
? 'h-8 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 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-56',
|
|
||||||
)}
|
|
||||||
placeholder="Edit label..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if isActive}
|
|
||||||
<div
|
<div
|
||||||
in:slide={{ duration: 250, delay: 50 }}
|
class={cn(
|
||||||
out:slide={{ duration: 250 }}
|
'animate-nudge relative transition-all',
|
||||||
class="flex justify-between items-center-safe"
|
side === 'left' ? 'order-2' : 'order-0',
|
||||||
|
isActive ? 'opacity-0' : 'opacity-100',
|
||||||
|
isDragging && 'opacity-80 grayscale-[0.2]',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
|
<AArrowUP class={cn('size-3', 'stroke-indigo-600')} />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet visibleContent()}
|
||||||
|
<div class="relative px-2 py-1">
|
||||||
|
<Input
|
||||||
|
bind:value={text}
|
||||||
|
disabled={isDragging}
|
||||||
|
onfocusin={handleInputFocus}
|
||||||
|
class={cn(
|
||||||
|
isActive
|
||||||
|
? 'h-8 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 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-56',
|
||||||
|
)}
|
||||||
|
placeholder="The quick brown fox..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet hiddenContent()}
|
||||||
|
<div class="flex justify-between items-center-safe">
|
||||||
<ComboControlV2 control={weightControl} />
|
<ComboControlV2 control={weightControl} />
|
||||||
<ComboControlV2 control={sizeControl} />
|
<ComboControlV2 control={sizeControl} />
|
||||||
<ComboControlV2 control={heightControl} />
|
<ComboControlV2 control={heightControl} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/snippet}
|
||||||
</div>
|
</ExpandableWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user