Compare commits
11 Commits
1d0ca31262
...
59b0d9c620
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59b0d9c620 | ||
|
|
be13a5c8a0 | ||
|
|
80efa49ad0 | ||
|
|
7e9675be80 | ||
|
|
ac979c816c | ||
|
|
272c2c2d22 | ||
|
|
a9e2898945 | ||
|
|
1712134f64 | ||
|
|
52111ee941 | ||
|
|
e4970e43ba | ||
|
|
b41c48da67 |
@@ -117,6 +117,8 @@
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
font-family: 'Karla', system-ui, sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ let { children } = $props();
|
|||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
<link rel="preconnect" href="https://api.fontshare.com" />
|
<link rel="preconnect" href="https://api.fontshare.com" />
|
||||||
<link rel="preconnect" href="https://cdn.fontshare.com" crossorigin="anonymous" />
|
<link rel="preconnect" href="https://cdn.fontshare.com" crossorigin="anonymous" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous">
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Karla:ital,wght@0,200..800;1,200..800&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div id="app-root">
|
<div id="app-root">
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
- Adds smooth transition when font appears
|
- Adds smooth transition when font appears
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { motion } from '$shared/lib';
|
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import { prefersReducedMotion } from 'svelte/motion';
|
||||||
import { appliedFontsManager } from '../../model';
|
import { appliedFontsManager } from '../../model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -59,7 +59,7 @@ const status = $derived(appliedFontsManager.getFontStatus(id, weight, false));
|
|||||||
const shouldReveal = $derived(hasEnteredViewport && (status === 'loaded' || status === 'error'));
|
const shouldReveal = $derived(hasEnteredViewport && (status === 'loaded' || status === 'error'));
|
||||||
|
|
||||||
const transitionClasses = $derived(
|
const transitionClasses = $derived(
|
||||||
motion.reduced
|
prefersReducedMotion.current
|
||||||
? 'transition-none' // Disable CSS transitions if motion is reduced
|
? 'transition-none' // Disable CSS transitions if motion is reduced
|
||||||
: 'transition-all duration-700 ease-[cubic-bezier(0.22,1,0.36,1)]',
|
: 'transition-all duration-700 ease-[cubic-bezier(0.22,1,0.36,1)]',
|
||||||
);
|
);
|
||||||
@@ -71,8 +71,9 @@ const transitionClasses = $derived(
|
|||||||
class={cn(
|
class={cn(
|
||||||
transitionClasses,
|
transitionClasses,
|
||||||
// If reduced motion is on, we skip the transform/blur entirely
|
// If reduced motion is on, we skip the transform/blur entirely
|
||||||
!shouldReveal && !motion.reduced && 'opacity-0 translate-y-8 scale-[0.98] blur-sm',
|
!shouldReveal && !prefersReducedMotion.current
|
||||||
!shouldReveal && motion.reduced && 'opacity-0', // Still hide until font is ready, but no movement
|
&& 'opacity-0 translate-y-8 scale-[0.98] blur-sm',
|
||||||
|
!shouldReveal && prefersReducedMotion.current && 'opacity-0', // Still hide until font is ready, but no movement
|
||||||
shouldReveal && 'opacity-100 translate-y-0 scale-100 blur-0',
|
shouldReveal && 'opacity-100 translate-y-0 scale-100 blur-0',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Badge } from '$shared/shadcn/ui/badge';
|
import { Badge } from '$shared/shadcn/ui/badge';
|
||||||
import { Checkbox } from '$shared/shadcn/ui/checkbox';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import { Label } from '$shared/shadcn/ui/label';
|
import { Spring } from 'svelte/motion';
|
||||||
import {
|
import {
|
||||||
type UnifiedFont,
|
type UnifiedFont,
|
||||||
selectedFontsStore,
|
selectedFontsStore,
|
||||||
@@ -17,32 +17,96 @@ interface Props {
|
|||||||
* Object with information about font
|
* Object with information about font
|
||||||
*/
|
*/
|
||||||
font: UnifiedFont;
|
font: UnifiedFont;
|
||||||
|
/**
|
||||||
|
* Is element fully visible
|
||||||
|
*/
|
||||||
|
isVisible: boolean;
|
||||||
|
/**
|
||||||
|
* From 0 to 1
|
||||||
|
*/
|
||||||
|
proximity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { font }: Props = $props();
|
const { font, isVisible, proximity }: Props = $props();
|
||||||
|
|
||||||
const handleChange = (checked: boolean) => {
|
let selected = $state(selectedFontsStore.has(font.id));
|
||||||
if (checked) {
|
let timeoutId = $state<NodeJS.Timeout | null>(null);
|
||||||
selectedFontsStore.addOne(font);
|
|
||||||
} else {
|
// Create a spring for smooth scale animation
|
||||||
selectedFontsStore.removeOne(font.id);
|
const scale = new Spring(1, {
|
||||||
|
stiffness: 0.3,
|
||||||
|
damping: 0.7,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Springs react to the virtualizer's computed state
|
||||||
|
const bloom = new Spring(0, {
|
||||||
|
stiffness: 0.15,
|
||||||
|
damping: 0.6,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync spring to proximity for a "Lens" effect
|
||||||
|
$effect(() => {
|
||||||
|
bloom.target = isVisible ? 1 : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
selected = selectedFontsStore.has(font.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const selected = $derived(selectedFontsStore.has(font.id));
|
function handleClick() {
|
||||||
|
animateSelection();
|
||||||
|
selected ? selectedFontsStore.removeOne(font.id) : selectedFontsStore.addOne(font);
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateSelection() {
|
||||||
|
scale.target = 0.98;
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
scale.target = 1;
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="pb-1">
|
<div
|
||||||
<Label
|
class={cn('pb-1 will-change-transform')}
|
||||||
for={font.id}
|
style:opacity={bloom.current}
|
||||||
class="
|
style:transform="
|
||||||
w-full hover:bg-accent/50 flex items-start gap-3 rounded-lg border border-transparent p-3
|
scale({0.92 + (bloom.current * 0.08)})
|
||||||
active:scale-[0.98] active:transition-transform active:duration-75
|
translateY({(1 - bloom.current) * 10}px)
|
||||||
has-aria-checked:border-blue-600
|
|
||||||
has-aria-checked:bg-blue-50
|
|
||||||
dark:has-aria-checked:border-blue-900
|
|
||||||
dark:has-aria-checked:bg-blue-950
|
|
||||||
"
|
"
|
||||||
|
>
|
||||||
|
<div style:transform={`scale(${scale.current})`}>
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
'w-full hover:bg-accent/50 flex items-start gap-3 rounded-lg border border-transparent p-3',
|
||||||
|
'active:transition-transform active:duration-150',
|
||||||
|
'border dark:border-slate-800',
|
||||||
|
'bg-white/10 border-white/20',
|
||||||
|
isVisible && 'bg-white/40 border-white/40',
|
||||||
|
selected && 'ring-2 ring-indigo-600 ring-inset bg-indigo-50/50 hover:bg-indigo-50',
|
||||||
|
)}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onmousedown={(e => {
|
||||||
|
// Prevent browser focus-jump
|
||||||
|
if (e.currentTarget === document.activeElement) return;
|
||||||
|
e.preventDefault();
|
||||||
|
handleClick();
|
||||||
|
})}
|
||||||
|
onkeydown={(e => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleClick();
|
||||||
|
}
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="flex flex-row gap-1 w-full items-center justify-between">
|
<div class="flex flex-row gap-1 w-full items-center justify-between">
|
||||||
@@ -63,22 +127,8 @@ const selected = $derived(selectedFontsStore.has(font.id));
|
|||||||
{font.name}
|
{font.name}
|
||||||
</FontApplicator>
|
</FontApplicator>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
id={font.id}
|
|
||||||
checked={selected}
|
|
||||||
onCheckedChange={handleChange}
|
|
||||||
class="
|
|
||||||
transition-all duration-150 ease-out
|
|
||||||
data-[state=checked]:scale-100
|
|
||||||
data-[state=checked]:border-blue-600
|
|
||||||
data-[state=checked]:bg-blue-600
|
|
||||||
data-[state=checked]:text-white
|
|
||||||
dark:data-[state=checked]:border-blue-700
|
|
||||||
dark:data-[state=checked]:bg-blue-700
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { appliedFontsManager } from '$entities/Font';
|
import { appliedFontsManager } from '$entities/Font';
|
||||||
import { controlManager } from '$features/SetupFont';
|
import { controlManager } from '$features/SetupFont';
|
||||||
import { Input } from '$shared/shadcn/ui/input';
|
|
||||||
import { ComparisonSlider } from '$widgets/ComparisonSlider/ui';
|
import { ComparisonSlider } from '$widgets/ComparisonSlider/ui';
|
||||||
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
import { displayedFontsStore } from '../../model';
|
import { displayedFontsStore } from '../../model';
|
||||||
import PairSelector from '../PairSelector/PairSelector.svelte';
|
import PairSelector from '../PairSelector/PairSelector.svelte';
|
||||||
|
|
||||||
@@ -20,16 +21,17 @@ $effect(() => {
|
|||||||
{#if hasAnyPairs}
|
{#if hasAnyPairs}
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex flex-row gap-4">
|
<div class="flex flex-row gap-4">
|
||||||
<Input bind:value={displayedText} />
|
|
||||||
<PairSelector />
|
<PairSelector />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if fontA && fontB}
|
{#if fontA && fontB}
|
||||||
|
<div in:fly={{ y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 }}>
|
||||||
<ComparisonSlider
|
<ComparisonSlider
|
||||||
fontA={fontA}
|
fontA={fontA}
|
||||||
fontB={fontB}
|
fontB={fontB}
|
||||||
bind:text={displayedText}
|
bind:text={displayedText}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FontVirtualList items={fontshareStore.fonts}>
|
<FontVirtualList items={fontshareStore.fonts}>
|
||||||
{#snippet children({ item: font })}
|
{#snippet children({ item: font, isVisible, proximity })}
|
||||||
<FontListItem {font} />
|
<FontListItem {font} {isVisible} {proximity} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FontVirtualList>
|
</FontVirtualList>
|
||||||
|
|||||||
@@ -21,3 +21,11 @@ export const DEFAULT_LINE_HEIGHT = 1.5;
|
|||||||
export const MIN_LINE_HEIGHT = 1;
|
export const MIN_LINE_HEIGHT = 1;
|
||||||
export const MAX_LINE_HEIGHT = 2;
|
export const MAX_LINE_HEIGHT = 2;
|
||||||
export const LINE_HEIGHT_STEP = 0.05;
|
export const LINE_HEIGHT_STEP = 0.05;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Letter spacing constants
|
||||||
|
*/
|
||||||
|
export const DEFAULT_LETTER_SPACING = 0;
|
||||||
|
export const MIN_LETTER_SPACING = -0.1;
|
||||||
|
export const MAX_LETTER_SPACING = 0.5;
|
||||||
|
export const LETTER_SPACING_STEP = 0.01;
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ import { createTypographyControlManager } from '../../lib';
|
|||||||
import {
|
import {
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_FONT_WEIGHT,
|
DEFAULT_FONT_WEIGHT,
|
||||||
|
DEFAULT_LETTER_SPACING,
|
||||||
DEFAULT_LINE_HEIGHT,
|
DEFAULT_LINE_HEIGHT,
|
||||||
FONT_SIZE_STEP,
|
FONT_SIZE_STEP,
|
||||||
FONT_WEIGHT_STEP,
|
FONT_WEIGHT_STEP,
|
||||||
|
LETTER_SPACING_STEP,
|
||||||
LINE_HEIGHT_STEP,
|
LINE_HEIGHT_STEP,
|
||||||
MAX_FONT_SIZE,
|
MAX_FONT_SIZE,
|
||||||
MAX_FONT_WEIGHT,
|
MAX_FONT_WEIGHT,
|
||||||
|
MAX_LETTER_SPACING,
|
||||||
MAX_LINE_HEIGHT,
|
MAX_LINE_HEIGHT,
|
||||||
MIN_FONT_SIZE,
|
MIN_FONT_SIZE,
|
||||||
MIN_FONT_WEIGHT,
|
MIN_FONT_WEIGHT,
|
||||||
|
MIN_LETTER_SPACING,
|
||||||
MIN_LINE_HEIGHT,
|
MIN_LINE_HEIGHT,
|
||||||
} from '../const/const';
|
} from '../const/const';
|
||||||
|
|
||||||
@@ -49,6 +53,17 @@ const controlData: ControlModel[] = [
|
|||||||
decreaseLabel: 'Decrease Line Height',
|
decreaseLabel: 'Decrease Line Height',
|
||||||
controlLabel: 'Line Height',
|
controlLabel: 'Line Height',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'letter_spacing',
|
||||||
|
value: DEFAULT_LETTER_SPACING,
|
||||||
|
max: MAX_LETTER_SPACING,
|
||||||
|
min: MIN_LETTER_SPACING,
|
||||||
|
step: LETTER_SPACING_STEP,
|
||||||
|
|
||||||
|
increaseLabel: 'Increase Letter Spacing',
|
||||||
|
decreaseLabel: 'Decrease Letter Spacing',
|
||||||
|
controlLabel: 'Letter Spacing',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const controlManager = createTypographyControlManager(controlData);
|
export const controlManager = createTypographyControlManager(controlData);
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// Check if we are in a browser environment
|
|
||||||
const isBrowser = typeof window !== 'undefined';
|
|
||||||
|
|
||||||
// A class to manage motion preference and provide a single instance for use everywhere
|
|
||||||
class MotionPreference {
|
|
||||||
// Reactive state
|
|
||||||
#reduced = $state(false);
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (isBrowser) {
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
||||||
|
|
||||||
// Set initial value immediately
|
|
||||||
this.#reduced = mediaQuery.matches;
|
|
||||||
|
|
||||||
// Simple listener that updates the reactive state
|
|
||||||
const handleChange = (e: MediaQueryListEvent) => {
|
|
||||||
this.#reduced = e.matches;
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaQuery.addEventListener('change', handleChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getter allows us to use 'motion.reduced' reactively in components
|
|
||||||
get reduced() {
|
|
||||||
return this.#reduced;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export a single instance to be used everywhere
|
|
||||||
export const motion = new MotionPreference();
|
|
||||||
@@ -49,10 +49,6 @@ export function createCharacterComparison(
|
|||||||
* Matches the Tailwind breakpoints used in the component.
|
* Matches the Tailwind breakpoints used in the component.
|
||||||
*/
|
*/
|
||||||
function getFontSize() {
|
function getFontSize() {
|
||||||
// const fontSize = size();
|
|
||||||
// if (fontSize) {
|
|
||||||
// return fontSize;
|
|
||||||
// }
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return 64;
|
return 64;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export interface VirtualItem {
|
|||||||
end: number;
|
end: number;
|
||||||
/** Unique key for the item (for Svelte's {#each} keying) */
|
/** Unique key for the item (for Svelte's {#each} keying) */
|
||||||
key: string | number;
|
key: string | number;
|
||||||
|
/** Whether the item is currently visible in the viewport */
|
||||||
|
isVisible: boolean;
|
||||||
|
/** Proximity of the item to the center of the viewport */
|
||||||
|
proximity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,6 +140,8 @@ export function createVirtualizer<T>(
|
|||||||
|
|
||||||
let endIdx = startIdx;
|
let endIdx = startIdx;
|
||||||
const viewportEnd = scrollOffset + containerHeight;
|
const viewportEnd = scrollOffset + containerHeight;
|
||||||
|
const viewportCenter = scrollOffset + (containerHeight / 2);
|
||||||
|
|
||||||
while (endIdx < count && offsets[endIdx] < viewportEnd) {
|
while (endIdx < count && offsets[endIdx] < viewportEnd) {
|
||||||
endIdx++;
|
endIdx++;
|
||||||
}
|
}
|
||||||
@@ -144,13 +150,31 @@ export function createVirtualizer<T>(
|
|||||||
const end = Math.min(count, endIdx + overscan);
|
const end = Math.min(count, endIdx + overscan);
|
||||||
|
|
||||||
const result: VirtualItem[] = [];
|
const result: VirtualItem[] = [];
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
for (let i = start; i < end; i++) {
|
||||||
|
const itemStart = offsets[i];
|
||||||
|
const itemSize = measuredSizes[i] ?? options.estimateSize(i);
|
||||||
|
const itemEnd = itemStart + itemSize;
|
||||||
|
|
||||||
|
// Visibility check: Does the item overlap the viewport?
|
||||||
|
// const isVisible = itemStart < viewportEnd && itemEnd > scrollOffset;
|
||||||
|
// Fully visible
|
||||||
|
const isVisible = itemStart >= scrollOffset && itemEnd <= viewportEnd;
|
||||||
|
|
||||||
|
// Proximity calculation: 1.0 at center, 0.0 at edges
|
||||||
|
const itemCenter = itemStart + (itemSize / 2);
|
||||||
|
const distanceToCenter = Math.abs(viewportCenter - itemCenter);
|
||||||
|
const maxDistance = containerHeight / 2;
|
||||||
|
const proximity = Math.max(0, 1 - (distanceToCenter / maxDistance));
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
index: i,
|
index: i,
|
||||||
start: offsets[i],
|
start: itemStart,
|
||||||
size: measuredSizes[i] ?? options.estimateSize(i),
|
size: itemSize,
|
||||||
end: offsets[i] + (measuredSizes[i] ?? options.estimateSize(i)),
|
end: itemEnd,
|
||||||
key: options.getItemKey?.(i) ?? i,
|
key: options.getItemKey?.(i) ?? i,
|
||||||
|
isVisible,
|
||||||
|
proximity,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,23 +231,25 @@ export function createVirtualizer<T>(
|
|||||||
const index = parseInt(node.dataset.index || '', 10);
|
const index = parseInt(node.dataset.index || '', 10);
|
||||||
const height = entry.borderBoxSize[0]?.blockSize ?? node.offsetHeight;
|
const height = entry.borderBoxSize[0]?.blockSize ?? node.offsetHeight;
|
||||||
|
|
||||||
if (!isNaN(index) && measuredSizes[index] !== height) {
|
if (!isNaN(index)) {
|
||||||
// 1. Stuff the measurement into a temporary buffer
|
const oldHeight = measuredSizes[index];
|
||||||
|
// Only update if the height difference is significant (> 0.5px)
|
||||||
|
// This prevents "jitter" from focus rings or sub-pixel border changes
|
||||||
|
if (oldHeight === undefined || Math.abs(oldHeight - height) > 0.5) {
|
||||||
|
// Stuff the measurement into a temporary buffer
|
||||||
measurementBuffer[index] = height;
|
measurementBuffer[index] = height;
|
||||||
|
|
||||||
// 2. Schedule a single update for the next animation frame
|
// Schedule a single update for the next animation frame
|
||||||
if (frameId === null) {
|
if (frameId === null) {
|
||||||
frameId = requestAnimationFrame(() => {
|
frameId = requestAnimationFrame(() => {
|
||||||
// 3. Update the state once for all collected measurements
|
|
||||||
// We use spread to trigger a single fine-grained update
|
|
||||||
measuredSizes = { ...measuredSizes, ...measurementBuffer };
|
measuredSizes = { ...measuredSizes, ...measurementBuffer };
|
||||||
|
// Reset the buffer
|
||||||
// 4. Reset the buffer
|
|
||||||
measurementBuffer = {};
|
measurementBuffer = {};
|
||||||
frameId = null;
|
frameId = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeObserver.observe(node);
|
resizeObserver.observe(node);
|
||||||
|
|||||||
@@ -19,5 +19,4 @@ export {
|
|||||||
type VirtualizerOptions,
|
type VirtualizerOptions,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
|
||||||
export { motion } from './accessibility/motion.svelte';
|
|
||||||
export { splitArray } from './utils';
|
export { splitArray } from './utils';
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Filter } from '$shared/lib';
|
import type { Filter } from '$shared/lib';
|
||||||
import { motion } from '$shared/lib';
|
|
||||||
import { Badge } from '$shared/shadcn/ui/badge';
|
import { Badge } from '$shared/shadcn/ui/badge';
|
||||||
import { buttonVariants } from '$shared/shadcn/ui/button';
|
import { buttonVariants } from '$shared/shadcn/ui/button';
|
||||||
import { Checkbox } from '$shared/shadcn/ui/checkbox';
|
import { Checkbox } from '$shared/shadcn/ui/checkbox';
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
import { Label } from '$shared/shadcn/ui/label';
|
import { Label } from '$shared/shadcn/ui/label';
|
||||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
import { prefersReducedMotion } from 'svelte/motion';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
|
|
||||||
interface PropertyFilterProps {
|
interface PropertyFilterProps {
|
||||||
@@ -37,7 +37,7 @@ let isOpen = $state(true);
|
|||||||
// Animation config respects user preferences - zero duration if reduced motion enabled
|
// Animation config respects user preferences - zero duration if reduced motion enabled
|
||||||
// Local modifier prevents animation on initial render, only animates user interactions
|
// Local modifier prevents animation on initial render, only animates user interactions
|
||||||
const slideConfig = $derived({
|
const slideConfig = $derived({
|
||||||
duration: motion.reduced ? 0 : 250,
|
duration: prefersReducedMotion.current ? 0 : 250,
|
||||||
easing: cubicOut,
|
easing: cubicOut,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,14 @@ function handleInputClick() {
|
|||||||
bind:value={value}
|
bind:value={value}
|
||||||
onkeydown={handleKeyDown}
|
onkeydown={handleKeyDown}
|
||||||
onclick={handleInputClick}
|
onclick={handleInputClick}
|
||||||
class="flex flex-row flex-1"
|
class="
|
||||||
|
h-20 w-full md:text-2xl backdrop-blur-sm bg-white/60 dark:bg-slate-900/40
|
||||||
|
border-2 border-slate-200/50 dark:border-slate-700/50
|
||||||
|
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/50
|
||||||
|
hover:bg-white/70 dark:hover:bg-slate-900/50 text-slate-900 dark:text-slate-100
|
||||||
|
placeholder:text-slate-400 px-6 py-4 rounded-2xl transition-all duration-300
|
||||||
|
font-medium
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -83,7 +90,7 @@ function handleInputClick() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
class="w-(--bits-popover-anchor-width) min-w-(--bits-popover-anchor-width)"
|
class="w-(--bits-popover-anchor-width) min-w-(--bits-popover-anchor-width) md:rounded-2xl"
|
||||||
>
|
>
|
||||||
{@render children?.({ id: contentId })}
|
{@render children?.({ id: contentId })}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ interface Props {
|
|||||||
*
|
*
|
||||||
* @template T - The type of items in the list
|
* @template T - The type of items in the list
|
||||||
*/
|
*/
|
||||||
children: Snippet<[{ item: T; index: number }]>;
|
children: Snippet<[{ item: T; index: number; isVisible: boolean; proximity: number }]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { items, itemHeight = 80, overscan = 5, class: className, onVisibleItemsChange, children }:
|
let { items, itemHeight = 80, overscan = 5, class: className, onVisibleItemsChange, children }:
|
||||||
@@ -76,8 +76,13 @@ $effect(() => {
|
|||||||
class={cn(
|
class={cn(
|
||||||
'relative overflow-auto rounded-md bg-background',
|
'relative overflow-auto rounded-md bg-background',
|
||||||
'h-150 w-full',
|
'h-150 w-full',
|
||||||
|
'scroll-smooth',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
onfocusin={(e => {
|
||||||
|
// Prevent the browser from jumping the scroll when an inner element gets focus
|
||||||
|
e.preventDefault();
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style:height="{virtualizer.totalSize}px"
|
style:height="{virtualizer.totalSize}px"
|
||||||
@@ -92,7 +97,12 @@ $effect(() => {
|
|||||||
class="absolute top-0 left-0 w-full"
|
class="absolute top-0 left-0 w-full"
|
||||||
style:transform="translateY({item.start}px)"
|
style:transform="translateY({item.start}px)"
|
||||||
>
|
>
|
||||||
{@render children({ item: items[item.index], index: item.index })}
|
{@render children({
|
||||||
|
item: items[item.index],
|
||||||
|
index: item.index,
|
||||||
|
isVisible: item.isVisible,
|
||||||
|
proximity: item.proximity,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ $effect(() => {
|
|||||||
'animate-nudge relative transition-all',
|
'animate-nudge relative transition-all',
|
||||||
side === 'left' ? 'order-2' : 'order-0',
|
side === 'left' ? 'order-2' : 'order-0',
|
||||||
isActive ? 'opacity-0' : 'opacity-100',
|
isActive ? 'opacity-0' : 'opacity-100',
|
||||||
isDragging && 'opacity-40 grayscale-[0.2]',
|
isDragging && 'opacity-80 grayscale-[0.2]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<AArrowUP class={cn('size-3', 'stroke-indigo-600')} />
|
<AArrowUP class={cn('size-3', 'stroke-indigo-600')} />
|
||||||
@@ -183,7 +183,7 @@ $effect(() => {
|
|||||||
isActive
|
isActive
|
||||||
? 'bg-white/95 border-indigo-400/40 shadow-[0_30px_70px_-10px_rgba(99,102,241,0.25)]'
|
? '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)]',
|
: ' bg-white/25 border-white/40 shadow-[0_12px_40px_-12px_rgba(0,0,0,0.12)]',
|
||||||
isDragging && 'opacity-40 grayscale-[0.2]',
|
isDragging && 'opacity-80 grayscale-[0.2]',
|
||||||
)}
|
)}
|
||||||
style:backdrop-filter="blur(24px)"
|
style:backdrop-filter="blur(24px)"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user