feat(ComparisonSlider): Massively improve the slider and move it to the widgets layer
This commit is contained in:
@@ -10,35 +10,64 @@
|
|||||||
- Performance optimized using offscreen canvas for measurements and transform-based animations.
|
- Performance optimized using offscreen canvas for measurements and transform-based animations.
|
||||||
-->
|
-->
|
||||||
<script lang="ts" generics="T extends { name: string; id: string }">
|
<script lang="ts" generics="T extends { name: string; id: string }">
|
||||||
import { createCharacterComparison } from '$shared/lib';
|
import {
|
||||||
|
createCharacterComparison,
|
||||||
|
createTypographyControl,
|
||||||
|
} from '$shared/lib';
|
||||||
import type { LineData } from '$shared/lib';
|
import type { LineData } from '$shared/lib';
|
||||||
import { Spring } from 'svelte/motion';
|
import { Spring } from 'svelte/motion';
|
||||||
import CharacterSlot from './components/CharacterSlot.svelte';
|
import CharacterSlot from './components/CharacterSlot.svelte';
|
||||||
|
import ControlsWrapper from './components/ControlsWrapper.svelte';
|
||||||
import Labels from './components/Labels.svelte';
|
import Labels from './components/Labels.svelte';
|
||||||
import SliderLine from './components/SliderLine.svelte';
|
import SliderLine from './components/SliderLine.svelte';
|
||||||
|
|
||||||
interface Props<T extends { name: string; id: string }> {
|
interface Props<T extends { name: string; id: string }> {
|
||||||
/** First font definition ({name, id}) */
|
/**
|
||||||
|
* First font definition ({name, id})
|
||||||
|
*/
|
||||||
fontA: T;
|
fontA: T;
|
||||||
/** Second font definition ({name, id}) */
|
/**
|
||||||
|
* Second font definition ({name, id})
|
||||||
|
*/
|
||||||
fontB: T;
|
fontB: T;
|
||||||
/** Text to display and compare */
|
/**
|
||||||
|
* Text to display and compare
|
||||||
|
*/
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|
||||||
weight?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
fontA,
|
fontA,
|
||||||
fontB,
|
fontB,
|
||||||
text = 'The quick brown fox jumps over the lazy dog',
|
text = $bindable('The quick brown fox jumps over the lazy dog'),
|
||||||
weight = 400,
|
|
||||||
}: Props<T> = $props();
|
}: Props<T> = $props();
|
||||||
|
|
||||||
let container: HTMLElement | undefined = $state();
|
let container: HTMLElement | undefined = $state();
|
||||||
|
let controlsWrapperElement = $state<HTMLDivElement | null>(null);
|
||||||
let measureCanvas: HTMLCanvasElement | undefined = $state();
|
let measureCanvas: HTMLCanvasElement | undefined = $state();
|
||||||
let isDragging = $state(false);
|
let isDragging = $state(false);
|
||||||
|
|
||||||
|
const weightControl = createTypographyControl({
|
||||||
|
min: 100,
|
||||||
|
max: 700,
|
||||||
|
step: 100,
|
||||||
|
value: 400,
|
||||||
|
});
|
||||||
|
|
||||||
|
const heightControl = createTypographyControl({
|
||||||
|
min: 1,
|
||||||
|
max: 2,
|
||||||
|
step: 0.05,
|
||||||
|
value: 1.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sizeControl = createTypographyControl({
|
||||||
|
min: 1,
|
||||||
|
max: 112,
|
||||||
|
step: 1,
|
||||||
|
value: 64,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulated helper for text splitting, measuring, and character proximity calculations.
|
* Encapsulated helper for text splitting, measuring, and character proximity calculations.
|
||||||
* Manages line breaking and character state based on fonts and container dimensions.
|
* Manages line breaking and character state based on fonts and container dimensions.
|
||||||
@@ -47,7 +76,8 @@ const charComparison = createCharacterComparison(
|
|||||||
() => text,
|
() => text,
|
||||||
() => fontA,
|
() => fontA,
|
||||||
() => fontB,
|
() => fontB,
|
||||||
() => weight,
|
() => weightControl.value,
|
||||||
|
() => sizeControl.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Physics-based spring for smooth handle movement */
|
/** Physics-based spring for smooth handle movement */
|
||||||
@@ -67,6 +97,11 @@ function handleMove(e: PointerEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startDragging(e: PointerEvent) {
|
function startDragging(e: PointerEvent) {
|
||||||
|
if (e.target === controlsWrapperElement || controlsWrapperElement?.contains(e.target as Node)) {
|
||||||
|
console.log('Pointer down on controls wrapper');
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
handleMove(e);
|
handleMove(e);
|
||||||
@@ -86,7 +121,13 @@ $effect(() => {
|
|||||||
|
|
||||||
// Re-run line breaking when container resizes or dependencies change
|
// Re-run line breaking when container resizes or dependencies change
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (container && measureCanvas && weight && fontA && fontB) {
|
// React on text and typography settings changes
|
||||||
|
const _text = text;
|
||||||
|
const _weight = weightControl.value;
|
||||||
|
const _size = sizeControl.value;
|
||||||
|
const _height = heightControl.value;
|
||||||
|
|
||||||
|
if (container && measureCanvas && fontA && fontB) {
|
||||||
// Using rAF to ensure DOM is ready/stabilized
|
// Using rAF to ensure DOM is ready/stabilized
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
charComparison.breakIntoLines(container, measureCanvas);
|
charComparison.breakIntoLines(container, measureCanvas);
|
||||||
@@ -97,7 +138,9 @@ $effect(() => {
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (container && measureCanvas && weight) {
|
if (
|
||||||
|
container && measureCanvas
|
||||||
|
) {
|
||||||
charComparison.breakIntoLines(container, measureCanvas);
|
charComparison.breakIntoLines(container, measureCanvas);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -109,7 +152,8 @@ $effect(() => {
|
|||||||
{#snippet renderLine(line: LineData, lineIndex: number)}
|
{#snippet renderLine(line: LineData, lineIndex: number)}
|
||||||
<div
|
<div
|
||||||
class="relative flex w-full justify-center items-center whitespace-nowrap"
|
class="relative flex w-full justify-center items-center whitespace-nowrap"
|
||||||
style:height="1.2em"
|
style:height={`${heightControl.value}em`}
|
||||||
|
style:line-height={`${heightControl.value}em`}
|
||||||
>
|
>
|
||||||
{#each line.text.split('') as char, charIndex}
|
{#each line.text.split('') as char, charIndex}
|
||||||
{@const { proximity, isPast } = charComparison.getCharState(lineIndex, charIndex, line, sliderPos)}
|
{@const { proximity, isPast } = charComparison.getCharState(lineIndex, charIndex, line, sliderPos)}
|
||||||
@@ -122,7 +166,8 @@ $effect(() => {
|
|||||||
{char}
|
{char}
|
||||||
{proximity}
|
{proximity}
|
||||||
{isPast}
|
{isPast}
|
||||||
{weight}
|
weight={weightControl.value}
|
||||||
|
size={sizeControl.value}
|
||||||
fontAName={fontA.name}
|
fontAName={fontA.name}
|
||||||
fontBName={fontB.name}
|
fontBName={fontB.name}
|
||||||
/>
|
/>
|
||||||
@@ -133,52 +178,66 @@ $effect(() => {
|
|||||||
<!-- Hidden canvas used for text measurement by the helper -->
|
<!-- Hidden canvas used for text measurement by the helper -->
|
||||||
<canvas bind:this={measureCanvas} class="hidden" width="1" height="1"></canvas>
|
<canvas bind:this={measureCanvas} class="hidden" width="1" height="1"></canvas>
|
||||||
|
|
||||||
<div
|
<div class="relative">
|
||||||
bind:this={container}
|
|
||||||
role="slider"
|
|
||||||
tabindex="0"
|
|
||||||
aria-valuenow={Math.round(sliderPos)}
|
|
||||||
aria-label="Font comparison slider"
|
|
||||||
onpointerdown={startDragging}
|
|
||||||
class="
|
|
||||||
group relative w-full py-16 px-6 sm:py-24 sm:px-12 overflow-hidden
|
|
||||||
bg-white rounded-[2.5rem] border border-slate-100 shadow-2xl
|
|
||||||
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<!-- Background Gradient Accent -->
|
|
||||||
<div
|
<div
|
||||||
|
bind:this={container}
|
||||||
|
role="slider"
|
||||||
|
tabindex="0"
|
||||||
|
aria-valuenow={Math.round(sliderPos)}
|
||||||
|
aria-label="Font comparison slider"
|
||||||
|
onpointerdown={startDragging}
|
||||||
class="
|
class="
|
||||||
absolute inset-0 bg-linear-to-br
|
group relative w-full py-16 px-6 sm:py-24 sm:px-12 overflow-hidden
|
||||||
from-slate-50/50 via-white to-slate-100/50
|
bg-indigo-50 rounded-[2.5rem] border border-slate-100 shadow-2xl
|
||||||
opacity-50 pointer-events-none
|
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center
|
||||||
"
|
"
|
||||||
|
class:box-shadow={'-20px 20px 60px #bebebe, 20px -20px 60px #ffffff;'}
|
||||||
>
|
>
|
||||||
</div>
|
<!-- Background Gradient Accent -->
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
absolute inset-0 bg-linear-to-br
|
||||||
|
from-slate-50/50 via-white to-slate-100/50
|
||||||
|
opacity-50 pointer-events-none
|
||||||
|
"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Text Rendering Container -->
|
<!-- Text Rendering Container -->
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
relative flex flex-col items-center gap-4
|
relative flex flex-col items-center gap-4
|
||||||
text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.15]
|
text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.15]
|
||||||
z-10 pointer-events-none text-center
|
z-10 pointer-events-none text-center
|
||||||
"
|
"
|
||||||
style:perspective="1000px"
|
style:perspective="1000px"
|
||||||
>
|
>
|
||||||
{#each charComparison.lines as line, lineIndex}
|
{#each charComparison.lines as line, lineIndex}
|
||||||
<div
|
<div
|
||||||
class="relative w-full whitespace-nowrap"
|
class="relative w-full whitespace-nowrap"
|
||||||
style:height="1.2em"
|
style:height={`${heightControl.value}em`}
|
||||||
style:display="flex"
|
style:display="flex"
|
||||||
style:align-items="center"
|
style:align-items="center"
|
||||||
style:justify-content="center"
|
style:justify-content="center"
|
||||||
>
|
>
|
||||||
{@render renderLine(line, lineIndex)}
|
{@render renderLine(line, lineIndex)}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Visual Components -->
|
<!-- Visual Components -->
|
||||||
<SliderLine {sliderPos} isDragging={isDragging} />
|
<SliderLine {sliderPos} {isDragging} />
|
||||||
<Labels {fontA} {fontB} {sliderPos} />
|
<Labels {fontA} {fontB} {sliderPos} />
|
||||||
|
</div>
|
||||||
|
<!-- Since there're slider controls inside we put them outside the main one -->
|
||||||
|
<ControlsWrapper
|
||||||
|
bind:wrapper={controlsWrapperElement}
|
||||||
|
{sliderPos}
|
||||||
|
{isDragging}
|
||||||
|
bind:text={text}
|
||||||
|
containerWidth={container?.clientWidth}
|
||||||
|
weightControl={weightControl}
|
||||||
|
sizeControl={sizeControl}
|
||||||
|
heightControl={heightControl}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -6,15 +6,37 @@
|
|||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/**
|
||||||
|
* Displayed character
|
||||||
|
*/
|
||||||
char: string;
|
char: string;
|
||||||
|
/**
|
||||||
|
* Proximity of the character to the center of the slider
|
||||||
|
*/
|
||||||
proximity: number;
|
proximity: number;
|
||||||
|
/**
|
||||||
|
* Flag indicating whether character needed to be changed
|
||||||
|
*/
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
|
/**
|
||||||
|
* Font weight of the character
|
||||||
|
*/
|
||||||
weight: number;
|
weight: number;
|
||||||
|
/**
|
||||||
|
* Font size of the character
|
||||||
|
*/
|
||||||
|
size: number;
|
||||||
|
/**
|
||||||
|
* Name of the font for the character after the change
|
||||||
|
*/
|
||||||
fontAName: string;
|
fontAName: string;
|
||||||
|
/**
|
||||||
|
* Name of the font for the character before the change
|
||||||
|
*/
|
||||||
fontBName: string;
|
fontBName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { char, proximity, isPast, weight, fontAName, fontBName }: Props = $props();
|
let { char, proximity, isPast, weight, size, fontAName, fontBName }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
@@ -24,6 +46,7 @@ let { char, proximity, isPast, weight, fontAName, fontBName }: Props = $props();
|
|||||||
)}
|
)}
|
||||||
style:font-family={isPast ? fontBName : fontAName}
|
style:font-family={isPast ? fontBName : fontAName}
|
||||||
style:font-weight={weight}
|
style:font-weight={weight}
|
||||||
|
style:font-size={`${size}px`}
|
||||||
style:transform="
|
style:transform="
|
||||||
scale({1 + proximity * 0.2})
|
scale({1 + proximity * 0.2})
|
||||||
translateY({-proximity * 12}px)
|
translateY({-proximity * 12}px)
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { TypographyControl } from '$shared/lib';
|
||||||
|
import { Input } from '$shared/shadcn/ui/input';
|
||||||
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
|
import { ComboControlV2 } from '$shared/ui';
|
||||||
|
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
|
||||||
|
import { Spring } from 'svelte/motion';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
wrapper?: HTMLDivElement | null;
|
||||||
|
sliderPos: number;
|
||||||
|
isDragging: boolean;
|
||||||
|
text: string;
|
||||||
|
containerWidth: number;
|
||||||
|
weightControl: TypographyControl;
|
||||||
|
sizeControl: TypographyControl;
|
||||||
|
heightControl: TypographyControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
sliderPos,
|
||||||
|
isDragging,
|
||||||
|
wrapper = $bindable(null),
|
||||||
|
text = $bindable(),
|
||||||
|
containerWidth = 0,
|
||||||
|
weightControl,
|
||||||
|
sizeControl,
|
||||||
|
heightControl,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let panelWidth = $state(0);
|
||||||
|
const margin = 24;
|
||||||
|
let side = $state<'left' | 'right'>('left');
|
||||||
|
|
||||||
|
// Unified active state for the entire wrapper
|
||||||
|
let isActive = $state(false);
|
||||||
|
|
||||||
|
function handleWrapperClick() {
|
||||||
|
if (!isDragging) {
|
||||||
|
isActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
if (wrapper && !wrapper.contains(e.target as Node)) {
|
||||||
|
isActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputFocus() {
|
||||||
|
isActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleWrapperClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movement Logic
|
||||||
|
$effect(() => {
|
||||||
|
if (containerWidth === 0 || panelWidth === 0) 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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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(() => {
|
||||||
|
const targetX = side === 'right' ? containerWidth - panelWidth - margin * 2 : 0;
|
||||||
|
if (containerWidth > 0 && panelWidth > 0 && !isActive) {
|
||||||
|
// On side change set the position and the rotation
|
||||||
|
xSpring.target = targetX;
|
||||||
|
rotateSpring.target = side === 'right' ? 3.5 : -3.5;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
rotateSpring.target = 0;
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Elevation and scale on focus and mouse over
|
||||||
|
$effect(() => {
|
||||||
|
if (isActive && !isDragging) {
|
||||||
|
// 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>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onclick={handleWrapperClick}
|
||||||
|
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="
|
||||||
|
translate({xSpring.current}px, {ySpring.current}px)
|
||||||
|
scale({scaleSpring.current})
|
||||||
|
rotateZ({rotateSpring.current}deg)
|
||||||
|
"
|
||||||
|
role="button"
|
||||||
|
tabindex={0}
|
||||||
|
onkeydown={handleKeyDown}
|
||||||
|
aria-label="Font controls"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
'animate-nudge relative transition-all',
|
||||||
|
side === 'left' ? 'order-2' : 'order-0',
|
||||||
|
isActive ? 'opacity-0' : 'opacity-100',
|
||||||
|
isDragging && 'opacity-40 grayscale-[0.2]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AArrowUP class={cn('size-3', 'stroke-indigo-600')} />
|
||||||
|
</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-40 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
|
||||||
|
in:slide={{ duration: 250, delay: 50 }}
|
||||||
|
out:slide={{ duration: 250 }}
|
||||||
|
class="flex justify-between items-center-safe"
|
||||||
|
>
|
||||||
|
<ComboControlV2 control={weightControl} />
|
||||||
|
<ComboControlV2 control={sizeControl} />
|
||||||
|
<ComboControlV2 control={heightControl} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes nudge {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0) scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
2% {
|
||||||
|
transform: translateY(-2px) scale(1.1) rotate(-1deg);
|
||||||
|
}
|
||||||
|
4% {
|
||||||
|
transform: translateY(0) scale(1) rotate(1deg);
|
||||||
|
}
|
||||||
|
6% {
|
||||||
|
transform: translateY(-2px) scale(1.1) rotate(0deg);
|
||||||
|
}
|
||||||
|
8% {
|
||||||
|
transform: translateY(0) scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-nudge {
|
||||||
|
animation: nudge 10s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3
src/widgets/ComparisonSlider/ui/index.ts
Normal file
3
src/widgets/ComparisonSlider/ui/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import ComparisonSlider from './ComparisonSlider/ComparisonSlider.svelte';
|
||||||
|
|
||||||
|
export { ComparisonSlider };
|
||||||
Reference in New Issue
Block a user