feature/ux-improvements #26
@@ -1,198 +1,51 @@
|
|||||||
<!--
|
<!--
|
||||||
Component: TypographyControls
|
Component: TypographyControls
|
||||||
Wrapper for the controls of the slider.
|
Controls for text input and typography settings (size, weight, height).
|
||||||
- Input to change the text
|
Simplified version for static positioning in settings mode.
|
||||||
- Three combo controls with inputs and sliders for font-weight, font-size, and line-height
|
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import {
|
import {
|
||||||
ComboControlV2,
|
ComboControlV2,
|
||||||
ExpandableWrapper,
|
|
||||||
Input,
|
Input,
|
||||||
} 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 { 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 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>
|
</script>
|
||||||
|
|
||||||
{#snippet InputComponent(className: string)}
|
<!-- Text input -->
|
||||||
<Input
|
<Input
|
||||||
class={className}
|
|
||||||
bind:value={comparisonStore.text}
|
bind:value={comparisonStore.text}
|
||||||
disabled={isDragging}
|
size="sm"
|
||||||
onfocusin={handleInputFocus}
|
label="Text"
|
||||||
placeholder="The quick brown fox..."
|
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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Typography controls -->
|
||||||
|
{#if typography.weightControl && typography.sizeControl && typography.heightControl}
|
||||||
|
<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
|
||||||
/>
|
/>
|
||||||
{/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>
|
</div>
|
||||||
{/if}
|
{/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>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user