Compare commits
10 Commits
cf8d3dffb9
...
88f4cd97f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88f4cd97f9 | ||
|
|
9167629616 | ||
|
|
b304e841de | ||
|
|
3ed63562b7 | ||
|
|
4b440496ba | ||
|
|
e4aacf609e | ||
|
|
51c2b6b5da | ||
|
|
195ae09fa2 | ||
|
|
b9eccbf627 | ||
|
|
63888e510c |
@@ -4,8 +4,12 @@
|
||||
- Handles font registration with the manager
|
||||
-->
|
||||
<script lang="ts" generics="T extends UnifiedFont">
|
||||
import { VirtualList } from '$shared/ui';
|
||||
import {
|
||||
Skeleton,
|
||||
VirtualList,
|
||||
} from '$shared/ui';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { getFontUrl } from '../../lib';
|
||||
import type { FontConfigRequest } from '../../model';
|
||||
import {
|
||||
@@ -13,13 +17,42 @@ import {
|
||||
appliedFontsManager,
|
||||
} from '../../model';
|
||||
|
||||
interface Props extends Omit<ComponentProps<typeof VirtualList<T>>, 'onVisibleItemsChange'> {
|
||||
interface Props extends
|
||||
Omit<
|
||||
ComponentProps<typeof VirtualList<T>>,
|
||||
'onVisibleItemsChange'
|
||||
>
|
||||
{
|
||||
/**
|
||||
* Callback for when visible items change
|
||||
*/
|
||||
onVisibleItemsChange?: (items: T[]) => void;
|
||||
/**
|
||||
* Callback for when near bottom is reached
|
||||
*/
|
||||
onNearBottom?: (lastVisibleIndex: number) => void;
|
||||
/**
|
||||
* Weight of the font
|
||||
*/
|
||||
/**
|
||||
* Weight of the font
|
||||
*/
|
||||
weight: number;
|
||||
/**
|
||||
* Whether the list is in a loading state
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
let { items, children, onVisibleItemsChange, onNearBottom, weight, ...rest }: Props = $props();
|
||||
let {
|
||||
items,
|
||||
children,
|
||||
onVisibleItemsChange,
|
||||
onNearBottom,
|
||||
weight,
|
||||
isLoading = false,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
function handleInternalVisibleChange(visibleItems: T[]) {
|
||||
const configs: FontConfigRequest[] = [];
|
||||
@@ -50,13 +83,31 @@ function handleNearBottom(lastVisibleIndex: number) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<VirtualList
|
||||
{items}
|
||||
{...rest}
|
||||
onVisibleItemsChange={handleInternalVisibleChange}
|
||||
onNearBottom={handleNearBottom}
|
||||
>
|
||||
{#snippet children(scope)}
|
||||
{@render children(scope)}
|
||||
{/snippet}
|
||||
</VirtualList>
|
||||
{#key isLoading}
|
||||
<div class="relative w-full h-full" transition:fade={{ duration: 300 }}>
|
||||
{#if isLoading}
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
{#each Array(5) as _, i}
|
||||
<div class="flex flex-col gap-2 p-4 border rounded-xl border-gray-200/50 bg-white/40">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<Skeleton class="h-8 w-1/3" />
|
||||
<Skeleton class="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
<Skeleton class="h-32 w-full" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<VirtualList
|
||||
{items}
|
||||
{...rest}
|
||||
onVisibleItemsChange={handleInternalVisibleChange}
|
||||
onNearBottom={handleNearBottom}
|
||||
>
|
||||
{#snippet children(scope)}
|
||||
{@render children(scope)}
|
||||
{/snippet}
|
||||
</VirtualList>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
@@ -10,8 +10,8 @@ import ComparisonSlider from '$widgets/ComparisonSlider/ui/ComparisonSlider/Comp
|
||||
import { FontSearch } from '$widgets/FontSearch';
|
||||
import { SampleList } from '$widgets/SampleList';
|
||||
import CodeIcon from '@lucide/svelte/icons/code';
|
||||
import EyeIcon from '@lucide/svelte/icons/eye';
|
||||
import LineSquiggleIcon from '@lucide/svelte/icons/line-squiggle';
|
||||
import ScanEyeIcon from '@lucide/svelte/icons/scan-eye';
|
||||
import ScanSearchIcon from '@lucide/svelte/icons/search';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -59,7 +59,7 @@ function handleTitleStatusChanged(index: number, isPast: boolean, title?: Snippe
|
||||
|
||||
<Section class="my-12 gap-8" index={1} onTitleStatusChange={handleTitleStatusChanged}>
|
||||
{#snippet icon({ className })}
|
||||
<ScanEyeIcon class={className} />
|
||||
<EyeIcon class={className} />
|
||||
{/snippet}
|
||||
{#snippet title({ className })}
|
||||
<h1 class={className}>
|
||||
|
||||
1
src/shared/shadcn/ui/spinner/index.ts
Normal file
1
src/shared/shadcn/ui/spinner/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Spinner } from './spinner.svelte';
|
||||
14
src/shared/shadcn/ui/spinner/spinner.svelte
Normal file
14
src/shared/shadcn/ui/spinner/spinner.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import Loader2Icon from '@lucide/svelte/icons/loader-2';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let { class: className, ...restProps }: ComponentProps<typeof Loader2Icon> = $props();
|
||||
</script>
|
||||
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
class={cn('size-4 animate-spin', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
33
src/shared/ui/Loader/Loader.stories.svelte
Normal file
33
src/shared/ui/Loader/Loader.stories.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Loader from './Loader.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Shared/Loader',
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Spinner with optional message',
|
||||
},
|
||||
story: { inline: false }, // Render stories in iframe for state isolation
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
message: {
|
||||
control: 'text',
|
||||
description: 'Optional message to display',
|
||||
defaultValue: 'analyzing_data',
|
||||
},
|
||||
size: {
|
||||
control: 'number',
|
||||
description: 'Size of the spinner',
|
||||
defaultValue: 20,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Default">
|
||||
<Loader />
|
||||
</Story>
|
||||
77
src/shared/ui/Loader/Loader.svelte
Normal file
77
src/shared/ui/Loader/Loader.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<!--
|
||||
Component: Loader
|
||||
Displays a loading spinner with an optional message.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Icon size (in pixels)
|
||||
* @default 20
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* Additional classes for container
|
||||
*/
|
||||
class?: string;
|
||||
/**
|
||||
* Message text
|
||||
* @default analyzing_data
|
||||
*/
|
||||
message?: string;
|
||||
}
|
||||
|
||||
let { size = 20, class: className = '', message = 'analyzing_data' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="absolute inset-x-0 inset-y-0 flex items-center justify-center gap-4 {className}"
|
||||
in:fade={{ duration: 300 }}
|
||||
out:fade={{ duration: 300 }}
|
||||
>
|
||||
<div style:width="{size}px" style:height="{size}px">
|
||||
<svg class="stroke-gray-900 stroke-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(12, 12)">
|
||||
<!-- Four corner brackets rotating -->
|
||||
<g>
|
||||
<path
|
||||
d="M -8 -8 L -4 -8 M -8 -8 L -8 -4"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M 8 -8 L 4 -8 M 8 -8 L 8 -4"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M -8 8 L -4 8 M -8 8 L -8 4"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path d="M 8 8 L 4 8 M 8 8 L 8 4" stroke="currentColor" stroke-width="1" stroke-linecap="round" />
|
||||
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0"
|
||||
to="360"
|
||||
dur="3s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Divider -->
|
||||
<div class="w-px h-3 bg-gray-400/50"></div>
|
||||
|
||||
<!-- Message -->
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.2em] text-gray-600 font-medium">
|
||||
{message}
|
||||
</span>
|
||||
</div>
|
||||
41
src/shared/ui/Skeleton/Skeleton.stories.svelte
Normal file
41
src/shared/ui/Skeleton/Skeleton.stories.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Shared/Skeleton',
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Skeleton component for loading states. Displays a shimmer animation when `animate` prop is true.',
|
||||
},
|
||||
story: { inline: false }, // Render stories in iframe for state isolation
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
animate: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show the shimmer animation',
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Default"
|
||||
args={{
|
||||
animate: true,
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-4 p-4 w-full">
|
||||
<div class="flex flex-col gap-2 p-4 border rounded-xl border-gray-200/50 bg-white/40">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<Skeleton class="h-8 w-1/3" />
|
||||
<Skeleton class="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
<Skeleton class="h-32 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</Story>
|
||||
27
src/shared/ui/Skeleton/Skeleton.svelte
Normal file
27
src/shared/ui/Skeleton/Skeleton.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
Component: Skeleton
|
||||
Generic loading placeholder with shimmer animation.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Whether to show the shimmer animation
|
||||
*/
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
let { class: className, animate = true, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
'rounded-md bg-gray-100/50 backdrop-blur-sm',
|
||||
animate && 'animate-pulse',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
</div>
|
||||
@@ -101,13 +101,25 @@ interface Props {
|
||||
* @template T - The type of items in the list
|
||||
*/
|
||||
children: Snippet<
|
||||
[{ item: T; index: number; isFullyVisible: boolean; isPartiallyVisible: boolean; proximity: number }]
|
||||
[
|
||||
{
|
||||
item: T;
|
||||
index: number;
|
||||
isFullyVisible: boolean;
|
||||
isPartiallyVisible: boolean;
|
||||
proximity: number;
|
||||
},
|
||||
]
|
||||
>;
|
||||
/**
|
||||
* Whether to use the window as the scroll container.
|
||||
* @default false
|
||||
*/
|
||||
useWindowScroll?: boolean;
|
||||
/**
|
||||
* Flag to show loading state
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -120,6 +132,7 @@ let {
|
||||
onNearBottom,
|
||||
children,
|
||||
useWindowScroll = false,
|
||||
isLoading = false,
|
||||
}: Props = $props();
|
||||
|
||||
// Reference to the ScrollArea viewport element for attaching the virtualizer
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
/**
|
||||
* Shared UI components exports
|
||||
*
|
||||
* Exports all shared UI components and their types
|
||||
*/
|
||||
|
||||
import CheckboxFilter from './CheckboxFilter/CheckboxFilter.svelte';
|
||||
import ComboControl from './ComboControl/ComboControl.svelte';
|
||||
import ComboControlV2 from './ComboControlV2/ComboControlV2.svelte';
|
||||
import ContentEditable from './ContentEditable/ContentEditable.svelte';
|
||||
import ExpandableWrapper from './ExpandableWrapper/ExpandableWrapper.svelte';
|
||||
import IconButton from './IconButton/IconButton.svelte';
|
||||
import SearchBar from './SearchBar/SearchBar.svelte';
|
||||
import Section from './Section/Section.svelte';
|
||||
import VirtualList from './VirtualList/VirtualList.svelte';
|
||||
|
||||
export {
|
||||
CheckboxFilter,
|
||||
ComboControl,
|
||||
ComboControlV2,
|
||||
ContentEditable,
|
||||
ExpandableWrapper,
|
||||
IconButton,
|
||||
SearchBar,
|
||||
Section,
|
||||
VirtualList,
|
||||
};
|
||||
export { default as CheckboxFilter } from './CheckboxFilter/CheckboxFilter.svelte';
|
||||
export { default as ComboControl } from './ComboControl/ComboControl.svelte';
|
||||
// ComboControlV2 might vary, assuming pattern holds or I'll fix later if build fails
|
||||
export { default as ComboControlV2 } from './ComboControlV2/ComboControlV2.svelte';
|
||||
export { default as ContentEditable } from './ContentEditable/ContentEditable.svelte';
|
||||
export { default as ExpandableWrapper } from './ExpandableWrapper/ExpandableWrapper.svelte';
|
||||
export { default as IconButton } from './IconButton/IconButton.svelte';
|
||||
export { default as Loader } from './Loader/Loader.svelte';
|
||||
export { default as SearchBar } from './SearchBar/SearchBar.svelte';
|
||||
export { default as Section } from './Section/Section.svelte';
|
||||
export { default as Skeleton } from './Skeleton/Skeleton.svelte';
|
||||
export { default as VirtualList } from './VirtualList/VirtualList.svelte';
|
||||
|
||||
@@ -136,6 +136,10 @@ class ComparisonStore {
|
||||
return !!this.#fontA && !!this.#fontB;
|
||||
}
|
||||
|
||||
get isLoading() {
|
||||
return this.#isRestoring;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public initializer (optional, as constructor starts it)
|
||||
* Kept for compatibility if manual re-init is needed
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
createTypographyControl,
|
||||
} from '$shared/lib';
|
||||
import type { LineData } from '$shared/lib';
|
||||
import { Loader } from '$shared/ui';
|
||||
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
||||
import { Spring } from 'svelte/motion';
|
||||
import { fade } from 'svelte/transition';
|
||||
import CharacterSlot from './components/CharacterSlot.svelte';
|
||||
import ControlsWrapper from './components/ControlsWrapper.svelte';
|
||||
import Labels from './components/Labels.svelte';
|
||||
@@ -26,6 +28,8 @@ import SliderLine from './components/SliderLine.svelte';
|
||||
const fontA = $derived(comparisonStore.fontA);
|
||||
const fontB = $derived(comparisonStore.fontB);
|
||||
|
||||
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
|
||||
|
||||
let container: HTMLElement | undefined = $state();
|
||||
let controlsWrapperElement = $state<HTMLDivElement | null>(null);
|
||||
let measureCanvas: HTMLCanvasElement | undefined = $state();
|
||||
@@ -164,31 +168,33 @@ $effect(() => {
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if fontA && fontB}
|
||||
<!-- Hidden canvas used for text measurement by the helper -->
|
||||
<canvas bind:this={measureCanvas} class="hidden" width="1" height="1"></canvas>
|
||||
<!-- Hidden canvas used for text measurement by the helper -->
|
||||
<canvas bind:this={measureCanvas} class="hidden" width="1" height="1"></canvas>
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
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-24 sm:py-24 sm:px-24 overflow-hidden
|
||||
rounded-[2.5rem]
|
||||
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center
|
||||
backdrop-blur-lg bg-gradient-to-br from-gray-100/70 via-white/50 to-gray-100/60
|
||||
border border-gray-300/40
|
||||
shadow-[inset_0_4px_12px_0_rgba(0,0,0,0.12),inset_0_2px_4px_0_rgba(0,0,0,0.08),0_1px_2px_0_rgba(255,255,255,0.8)]
|
||||
before:absolute before:inset-0 before:rounded-[2.5rem] before:p-[1px]
|
||||
before:bg-gradient-to-br before:from-black/5 before:via-black/2 before:to-transparent
|
||||
before:-z-10 before:blur-sm
|
||||
"
|
||||
>
|
||||
<!-- Text Rendering Container -->
|
||||
<div class="relative">
|
||||
<div
|
||||
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-24 sm:py-24 sm:px-24 overflow-hidden
|
||||
rounded-[2.5rem]
|
||||
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center
|
||||
backdrop-blur-lg bg-gradient-to-br from-gray-100/70 via-white/50 to-gray-100/60
|
||||
border border-gray-300/40
|
||||
shadow-[inset_0_4px_12px_0_rgba(0,0,0,0.12),inset_0_2px_4px_0_rgba(0,0,0,0.08),0_1px_2px_0_rgba(255,255,255,0.8)]
|
||||
before:absolute before:inset-0 before:rounded-[2.5rem] before:p-[1px]
|
||||
before:bg-gradient-to-br before:from-black/5 before:via-black/2 before:to-transparent
|
||||
before:-z-10 before:blur-sm
|
||||
"
|
||||
>
|
||||
<!-- Text Rendering Container -->
|
||||
{#if isLoading}
|
||||
<Loader size={24} />
|
||||
{:else}
|
||||
<div
|
||||
class="
|
||||
relative flex flex-col items-center gap-4
|
||||
@@ -197,6 +203,8 @@ $effect(() => {
|
||||
drop-shadow-[0_3px_6px_rgba(255,255,255,0.9)]
|
||||
"
|
||||
style:perspective="1000px"
|
||||
in:fade={{ duration: 300, delay: 300 }}
|
||||
out:fade={{ duration: 300 }}
|
||||
>
|
||||
{#each charComparison.lines as line, lineIndex}
|
||||
<div
|
||||
@@ -212,8 +220,10 @@ $effect(() => {
|
||||
</div>
|
||||
|
||||
<SliderLine {sliderPos} {isDragging} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if fontA && fontB && !isLoading}
|
||||
<Labels {fontA} {fontB} {sliderPos} weight={weightControl.value} />
|
||||
<!-- Since there're slider controls inside we put them outside the main one -->
|
||||
<ControlsWrapper
|
||||
@@ -226,5 +236,5 @@ $effect(() => {
|
||||
{sizeControl}
|
||||
{heightControl}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ComboControlV2 } from '$shared/ui';
|
||||
import { ExpandableWrapper } from '$shared/ui';
|
||||
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
|
||||
import { Spring } from 'svelte/motion';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
@@ -121,6 +122,8 @@ $effect(() => {
|
||||
translateX({xSpring.current}px)
|
||||
rotateZ({rotateSpring.current}deg)
|
||||
"
|
||||
in:fade={{ duration: 300, delay: 300 }}
|
||||
out:fade={{ duration: 300, delay: 300 }}
|
||||
>
|
||||
<ExpandableWrapper
|
||||
bind:element={wrapper}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '$shared/shadcn/ui/select';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props<T> {
|
||||
/**
|
||||
@@ -60,6 +61,8 @@ function selectFontB(font: UnifiedFont) {
|
||||
<div
|
||||
class="z-50 pointer-events-auto"
|
||||
onpointerdown={(e => e.stopPropagation())}
|
||||
in:fade={{ duration: 300, delay: 300 }}
|
||||
out:fade={{ duration: 300, delay: 300 }}
|
||||
>
|
||||
<SelectRoot type="single" disabled={!fontList.length}>
|
||||
<SelectTrigger
|
||||
|
||||
@@ -50,11 +50,9 @@ const displayRange = $derived.by(() => {
|
||||
const loadedCount = Math.min(offset + limit, total);
|
||||
return `Showing ${loadedCount} of ${total} fonts`;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if unifiedFontStore.isFetching || unifiedFontStore.isLoading}
|
||||
<span class="ml-2 text-xs text-muted-foreground/70">(Loading...)</span>
|
||||
{/if}
|
||||
const isLoading = $derived(unifiedFontStore.isFetching || unifiedFontStore.isLoading);
|
||||
</script>
|
||||
|
||||
<FontVirtualList
|
||||
items={unifiedFontStore.fonts}
|
||||
@@ -63,8 +61,15 @@ const displayRange = $derived.by(() => {
|
||||
itemHeight={280}
|
||||
useWindowScroll={true}
|
||||
weight={controlManager.weight}
|
||||
{isLoading}
|
||||
>
|
||||
{#snippet children({ item: font, isFullyVisible, isPartiallyVisible, proximity, index })}
|
||||
{#snippet children({
|
||||
item: font,
|
||||
isFullyVisible,
|
||||
isPartiallyVisible,
|
||||
proximity,
|
||||
index,
|
||||
})}
|
||||
<FontListItem {font} {isFullyVisible} {isPartiallyVisible} {proximity}>
|
||||
<FontSampler {font} bind:text {index} />
|
||||
</FontListItem>
|
||||
|
||||
Reference in New Issue
Block a user