feat(TypographyControls): drasticaly reduce animations, keep only the container functional

This commit is contained in:
Ilia Mashkov
2026-02-15 23:07:23 +03:00
parent 72334a3d05
commit 99966d2de9

View File

@@ -1,198 +1,51 @@
<!--
Component: TypographyControls
Wrapper for the controls of the slider.
- Input to change the text
- Three combo controls with inputs and sliders for font-weight, font-size, and line-height
Controls for text input and typography settings (size, weight, height).
Simplified version for static positioning in settings mode.
-->
<script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import {
ComboControlV2,
ExpandableWrapper,
Input,
} from '$shared/ui';
import { comparisonStore } from '$widgets/ComparisonSlider/model';
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
import { type Orientation } from 'bits-ui';
import { untrack } from 'svelte';
import { Spring } from 'svelte/motion';
import { fade } from 'svelte/transition';
interface Props {
/**
* Ref
*/
wrapper?: HTMLDivElement | null;
/**
* Slider position
*/
sliderPos: number;
/**
* Whether slider is being dragged
*/
isDragging: boolean;
/** */
isActive?: boolean;
/**
* Container width
*/
containerWidth: number;
/**
* Reduced animations flag
*/
staticPosition?: boolean;
}
let {
sliderPos,
isDragging,
isActive = $bindable(false),
wrapper = $bindable(null),
containerWidth = 0,
staticPosition = false,
}: Props = $props();
const typography = $derived(comparisonStore.typography);
const panelWidth = $derived(wrapper?.clientWidth ?? 0);
const margin = 24;
let side = $state<'left' | 'right'>('left');
// Unified active state for the entire wrapper
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
const xSpring = new Spring(0, {
stiffness: 0.14, // Lower is slower
damping: 0.5, // Settle
});
const rotateSpring = new Spring(0, {
stiffness: 0.12,
damping: 0.55,
});
function handleInputFocus() {
isActive = true;
}
// Movement Logic
$effect(() => {
if (containerWidth === 0 || panelWidth === 0 || staticPosition) {
return;
}
const sliderX = (sliderPos / 100) * containerWidth;
const buffer = 40;
const leftTrigger = margin + panelWidth + buffer;
const rightTrigger = containerWidth - (margin + panelWidth + buffer);
if (side === 'left' && sliderX < leftTrigger) {
side = 'right';
} else if (side === 'right' && sliderX > rightTrigger) {
side = 'left';
}
});
$effect(() => {
// Trigger only when side changes
const currentSide = side;
untrack(() => {
if (containerWidth > 0 && panelWidth > 0) {
const targetX = currentSide === 'right'
? containerWidth - panelWidth - margin * 2
: 0;
// On side change set the position and the rotation
xSpring.target = targetX;
rotateSpring.target = currentSide === 'right' ? 3.5 : -3.5;
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
rotateSpring.target = 0;
}, 600);
}
});
return () => {
if (timeoutId) clearTimeout(timeoutId);
};
});
</script>
{#snippet InputComponent(className: string)}
<!-- Text input -->
<Input
class={className}
bind:value={comparisonStore.text}
disabled={isDragging}
onfocusin={handleInputFocus}
size="sm"
label="Text"
placeholder="The quick brown fox..."
class="w-full px-3 py-2 h-10 rounded-lg border border-border-muted bg-background-60 backdrop-blur-sm mr-4"
/>
{/snippet}
{#snippet Controls(className: string, orientation: Orientation)}
<!-- Typography controls -->
{#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 class="flex flex-col gap-1.5 mt-1.5">
<ComboControlV2
control={typography.weightControl}
orientation="horizontal"
class="sm:py-0"
showScale={false}
reduced
/>
<ComboControlV2
control={typography.sizeControl}
orientation="horizontal"
class="sm:py-0"
showScale={false}
reduced
/>
<ComboControlV2
control={typography.heightControl}
orientation="horizontal"
class="sm:py-0"
showScale={false}
reduced
/>
</div>
{/if}
{/snippet}
<div
class="z-50 will-change-transform"
style:transform="
translateX({xSpring.current}px)
rotateZ({rotateSpring.current}deg)
"
in:fade={{ duration: 300, delay: 300 }}
out:fade={{ duration: 300, delay: 300 }}
>
{#if staticPosition}
<div class="flex flex-col gap-6">
{@render InputComponent?.('p-6')}
{@render Controls?.('flex flex-col justify-between items-center-safe gap-6', 'horizontal')}
</div>
{:else}
<ExpandableWrapper
bind:element={wrapper}
bind:expanded={isActive}
disabled={isDragging}
aria-label="Font controls"
rotation={side === 'right' ? 'counterclockwise' : 'clockwise'}
class={cn(
'transition-opacity flex items-top gap-1.5',
panelWidth === 0 ? 'opacity-0' : 'opacity-100',
)}
containerClassName={cn(!isActive && 'p-2 sm:p-0')}
>
{#snippet badge()}
<div
class={cn(
'animate-nudge relative transition-all',
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()}
{@render InputComponent(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',
))}
{/snippet}
{#snippet hiddenContent()}
{@render Controls?.('flex flex-row justify-between items-center-safe gap-2 sm:gap-0 h-64', 'vertical')}
{/snippet}
</ExpandableWrapper>
{/if}
</div>