Compare commits
18 Commits
59b0d9c620
...
c07800cc96
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c07800cc96 | ||
|
|
b49bf0d397 | ||
|
|
ed4ee8bb44 | ||
|
|
8a2059ac4a | ||
|
|
7ffc5d6a34 | ||
|
|
08cccc5ede | ||
|
|
71266f8b22 | ||
|
|
d5221ad449 | ||
|
|
873b697e8c | ||
|
|
3dce409034 | ||
|
|
cf08f7adfa | ||
|
|
4b01b1592d | ||
|
|
ecb4bea642 | ||
|
|
e89c6369cb | ||
|
|
18a311c6b1 | ||
|
|
732f77f504 | ||
|
|
b7992ca138 | ||
|
|
32b1367877 |
@@ -140,3 +140,25 @@
|
||||
.peer:focus-visible ~ * {
|
||||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -3,23 +3,25 @@
|
||||
* Layout Component
|
||||
*
|
||||
* Root layout wrapper that provides the application shell structure. Handles favicon,
|
||||
* sidebar provider initialization, and renders child routes with consistent structure.
|
||||
* toolbar provider initialization, and renders child routes with consistent structure.
|
||||
*
|
||||
* Layout structure:
|
||||
* - Header area (currently empty, reserved for future use)
|
||||
* - Collapsible sidebar with main content area
|
||||
* - Footer area (currently empty, reserved for future use)
|
||||
*
|
||||
* Uses Sidebar.Provider to enable mobile-responsive collapsible sidebar behavior
|
||||
* throughout the application.
|
||||
* - Footer area (currently empty, reserved for future use)
|
||||
*/
|
||||
import favicon from '$shared/assets/favicon.svg';
|
||||
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
||||
import { FiltersSidebar } from '$widgets/FiltersSidebar';
|
||||
import TypographyMenu from '$widgets/TypographySettings/ui/TypographyMenu.svelte';
|
||||
import { ScrollArea } from '$shared/shadcn/ui/scroll-area';
|
||||
import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip';
|
||||
import { TypographyMenu } from '$widgets/TypographySettings';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
/** Slot content for route pages to render */
|
||||
let { children } = $props();
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -35,22 +37,16 @@ let { children } = $props();
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
<div id="app-root">
|
||||
<div id="app-root" class="min-h-screen flex flex-col bg-background">
|
||||
<header></header>
|
||||
|
||||
<Sidebar.Provider>
|
||||
<FiltersSidebar />
|
||||
<main class="w-dvw">
|
||||
<ScrollArea class="h-screen w-screen">
|
||||
<main class="flex-1 w-full max-w-5xl mx-auto px-4 py-6 md:px-8 lg:py-10 relative">
|
||||
<TooltipProvider>
|
||||
<TypographyMenu />
|
||||
{@render children?.()}
|
||||
</TooltipProvider>
|
||||
</main>
|
||||
</Sidebar.Provider>
|
||||
</ScrollArea>
|
||||
<footer></footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#app-root {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { displayedFontsStore } from './model';
|
||||
export { FontDisplay } from './ui';
|
||||
|
||||
@@ -28,6 +28,8 @@ export class DisplayedFontsStore {
|
||||
|
||||
#selectedPair = $state<Partial<[UnifiedFont, UnifiedFont]>>([]);
|
||||
|
||||
#hasAnySelectedFonts = $derived(this.#displayedFonts.length > 0);
|
||||
|
||||
get fonts() {
|
||||
return this.#displayedFonts;
|
||||
}
|
||||
@@ -41,7 +43,9 @@ export class DisplayedFontsStore {
|
||||
}
|
||||
|
||||
set selectedPair(pair: Partial<[UnifiedFont, UnifiedFont]>) {
|
||||
this.#selectedPair = pair;
|
||||
const [first, second] = this.#selectedPair;
|
||||
const [newFist, newSecond] = pair;
|
||||
this.#selectedPair = [newFist ?? first, newSecond ?? second];
|
||||
}
|
||||
|
||||
get text() {
|
||||
@@ -52,6 +56,10 @@ export class DisplayedFontsStore {
|
||||
this.#sampleText = text;
|
||||
}
|
||||
|
||||
get hasAnyFonts() {
|
||||
return this.#hasAnySelectedFonts;
|
||||
}
|
||||
|
||||
isSelectedPairEmpty(): boolean {
|
||||
const [font1, font2] = this.#selectedPair;
|
||||
return !font1 || !font2;
|
||||
|
||||
@@ -15,5 +15,5 @@ export { filterManager } from './model/state/manager.svelte';
|
||||
export {
|
||||
FilterControls,
|
||||
Filters,
|
||||
FontSearch,
|
||||
SuggestedFonts,
|
||||
} from './ui';
|
||||
|
||||
@@ -5,15 +5,42 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Button } from '$shared/shadcn/ui/button';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import Rotate from '@lucide/svelte/icons/rotate-ccw';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Tween } from 'svelte/motion';
|
||||
import { filterManager } from '../../model';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { class: className }: Props = $props();
|
||||
|
||||
const transform = new Tween(
|
||||
{ scale: 1, rotate: 0 },
|
||||
{ duration: 150, easing: cubicOut },
|
||||
);
|
||||
|
||||
function handleClick() {
|
||||
filterManager.deselectAllGlobal();
|
||||
|
||||
transform.set({ scale: 0.98, rotate: 1 }).then(() => {
|
||||
transform.set({ scale: 1, rotate: 0 });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="flex-1 cursor-pointer"
|
||||
onclick={filterManager.deselectAllGlobal}
|
||||
<div
|
||||
class={cn('flex flex-row gap-2', className)}
|
||||
style:transform="scale({transform.current.scale}) rotate({transform.current.rotate}deg)"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group flex flex-1 cursor-pointer gap-1"
|
||||
onclick={handleClick}
|
||||
>
|
||||
<Rotate class="size-4 group-hover:-rotate-180 transition-transform duration-300" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<!--
|
||||
Component: FontSearch
|
||||
|
||||
Combines search input with font list display
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { fontshareStore } from '$entities/Font';
|
||||
import { SearchBar } from '$shared/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { mapManagerToParams } from '../../lib';
|
||||
import { filterManager } from '../../model';
|
||||
import SuggestedFonts from '../SuggestedFonts/SuggestedFonts.svelte';
|
||||
|
||||
onMount(() => {
|
||||
/**
|
||||
* The Pairing:
|
||||
* We "plug" this manager into the global store.
|
||||
* addBinding returns a function that removes this binding when the component unmounts.
|
||||
*/
|
||||
const unbind = fontshareStore.addBinding(() => mapManagerToParams(filterManager));
|
||||
|
||||
return unbind;
|
||||
});
|
||||
</script>
|
||||
|
||||
<SearchBar
|
||||
id="font-search"
|
||||
class="w-full"
|
||||
placeholder="Search fonts by name..."
|
||||
bind:value={filterManager.queryValue}
|
||||
>
|
||||
<SuggestedFonts />
|
||||
</SearchBar>
|
||||
@@ -1,9 +1,9 @@
|
||||
import Filters from './Filters/Filters.svelte';
|
||||
import FilterControls from './FiltersControl/FilterControls.svelte';
|
||||
import FontSearch from './FontSearch/FontSearch.svelte';
|
||||
import SuggestedFonts from './SuggestedFonts/SuggestedFonts.svelte';
|
||||
|
||||
export {
|
||||
FilterControls,
|
||||
Filters,
|
||||
FontSearch,
|
||||
SuggestedFonts,
|
||||
};
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
Contains controls for setting up font properties.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Separator } from '$shared/shadcn/ui/separator/index';
|
||||
import { Trigger as SidebarTrigger } from '$shared/shadcn/ui/sidebar';
|
||||
import { ComboControl } from '$shared/ui';
|
||||
import { controlManager } from '../model';
|
||||
</script>
|
||||
|
||||
<div class="p-2 flex flex-row items-center gap-2">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" class="h-full" />
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="py-2 px-10 flex flex-row items-center gap-2">
|
||||
<div class="flex flex-row gap-3">
|
||||
{#each controlManager.controls as control (control.id)}
|
||||
<ComboControl control={control.instance} />
|
||||
<ComboControl
|
||||
control={control.instance}
|
||||
increaseLabel={control.increaseLabel}
|
||||
decreaseLabel={control.decreaseLabel}
|
||||
controlLabel={control.controlLabel}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
<script lang="ts">
|
||||
import FontDisplay from '$features/DisplayFont/ui/FontDisplay/FontDisplay.svelte';
|
||||
import { FontSearch } from '$widgets/FontSearch';
|
||||
|
||||
/**
|
||||
* Page Component
|
||||
*/
|
||||
|
||||
let searchContainer: HTMLElement;
|
||||
|
||||
let isExpanded = $state(false);
|
||||
</script>
|
||||
|
||||
<!-- Font List -->
|
||||
<div class="p-2">
|
||||
<div class="p-2 will-change-[height]">
|
||||
<div bind:this={searchContainer}>
|
||||
<FontSearch bind:showFilters={isExpanded} />
|
||||
</div>
|
||||
|
||||
<div class="will-change-tranform transition-transform content">
|
||||
<FontDisplay />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
/* Tells the browser to skip rendering off-screen content */
|
||||
content-visibility: auto;
|
||||
/* Helps the browser reserve space without calculating everything */
|
||||
contain-intrinsic-size: 1px 1000px;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,3 +20,5 @@ export {
|
||||
} from './helpers';
|
||||
|
||||
export { splitArray } from './utils';
|
||||
|
||||
export { springySlideFade } from './transitions';
|
||||
|
||||
1
src/shared/lib/transitions/index.ts
Normal file
1
src/shared/lib/transitions/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { springySlideFade } from './springySlideFade/springySlideFade';
|
||||
@@ -0,0 +1,60 @@
|
||||
import type {
|
||||
SlideParams,
|
||||
TransitionConfig,
|
||||
} from 'svelte/transition';
|
||||
|
||||
function elasticOut(t: number) {
|
||||
return Math.pow(2, -10 * t) * Math.sin((t - 0.075) * (2 * Math.PI) / 0.3) + 1;
|
||||
}
|
||||
|
||||
function gentleSpring(t: number) {
|
||||
return 1 - Math.pow(1 - t, 3) * Math.cos(t * Math.PI * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Svelte slide transition function for custom slide+fade
|
||||
* @param node - The element to apply the transition to
|
||||
* @param params - Transition parameters
|
||||
* @returns Transition configuration
|
||||
*/
|
||||
export function springySlideFade(
|
||||
node: HTMLElement,
|
||||
params: SlideParams = {},
|
||||
): TransitionConfig {
|
||||
const { duration = 400 } = params;
|
||||
const height = node.scrollHeight;
|
||||
|
||||
// Check if the browser is Firefox to work around specific rendering issues
|
||||
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');
|
||||
|
||||
return {
|
||||
duration,
|
||||
// We use 'tick' for the most precise control over the
|
||||
// coordination with the elements below.
|
||||
css: t => {
|
||||
// Use elastic easing
|
||||
const eased = gentleSpring(t);
|
||||
|
||||
return `
|
||||
height: ${eased * height}px;
|
||||
opacity: ${t};
|
||||
transform: translateY(${(1 - t) * -10}px);
|
||||
transform-origin: top;
|
||||
overflow: hidden;
|
||||
contain: size layout style;
|
||||
will-change: max-height, opacity, transform;
|
||||
backface-visibility: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
${
|
||||
isFirefox
|
||||
? `
|
||||
perspective: 1000px;
|
||||
isolation: isolate;
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`;
|
||||
},
|
||||
};
|
||||
}
|
||||
10
src/shared/shadcn/ui/scroll-area/index.ts
Normal file
10
src/shared/shadcn/ui/scroll-area/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Scrollbar from './scroll-area-scrollbar.svelte';
|
||||
import Root from './scroll-area.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
// ,
|
||||
Root as ScrollArea,
|
||||
Scrollbar,
|
||||
Scrollbar as ScrollAreaScrollbar,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithoutChild,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = 'vertical',
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<ScrollAreaPrimitive.ScrollbarProps> = $props();
|
||||
</script>
|
||||
|
||||
<ScrollAreaPrimitive.Scrollbar
|
||||
bind:ref
|
||||
data-slot="scroll-area-scrollbar"
|
||||
{orientation}
|
||||
class={cn(
|
||||
'flex touch-none p-px transition-colors select-none',
|
||||
orientation === 'vertical' && 'h-full w-2.5 border-s border-s-transparent',
|
||||
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ScrollAreaPrimitive.Thumb
|
||||
data-slot="scroll-area-thumb"
|
||||
class="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.Scrollbar>
|
||||
46
src/shared/shadcn/ui/scroll-area/scroll-area.svelte
Normal file
46
src/shared/shadcn/ui/scroll-area/scroll-area.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithoutChild,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
|
||||
import { Scrollbar } from './index.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
viewportRef = $bindable(null),
|
||||
class: className,
|
||||
orientation = 'vertical',
|
||||
scrollbarXClasses = '',
|
||||
scrollbarYClasses = '',
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<ScrollAreaPrimitive.RootProps> & {
|
||||
orientation?: 'vertical' | 'horizontal' | 'both' | undefined;
|
||||
scrollbarXClasses?: string | undefined;
|
||||
scrollbarYClasses?: string | undefined;
|
||||
viewportRef?: HTMLElement | null;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ScrollAreaPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="scroll-area"
|
||||
class={cn('relative', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
bind:ref={viewportRef}
|
||||
data-slot="scroll-area-viewport"
|
||||
class="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
|
||||
>
|
||||
{@render children?.()}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
{#if orientation === 'vertical' || orientation === 'both'}
|
||||
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
|
||||
{/if}
|
||||
{#if orientation === 'horizontal' || orientation === 'both'}
|
||||
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
|
||||
{/if}
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
@@ -8,7 +8,10 @@
|
||||
- Local transition prevents animation when component first renders
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Filter } from '$shared/lib';
|
||||
import {
|
||||
type Filter,
|
||||
springySlideFade,
|
||||
} from '$shared/lib';
|
||||
import { Badge } from '$shared/shadcn/ui/badge';
|
||||
import { buttonVariants } from '$shared/shadcn/ui/button';
|
||||
import { Checkbox } from '$shared/shadcn/ui/checkbox';
|
||||
@@ -20,7 +23,6 @@ import { Label } from '$shared/shadcn/ui/label';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { prefersReducedMotion } from 'svelte/motion';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
interface PropertyFilterProps {
|
||||
/** Label for this filter group (e.g., "Properties", "Tags") */
|
||||
@@ -37,7 +39,7 @@ let isOpen = $state(true);
|
||||
// Animation config respects user preferences - zero duration if reduced motion enabled
|
||||
// Local modifier prevents animation on initial render, only animates user interactions
|
||||
const slideConfig = $derived({
|
||||
duration: prefersReducedMotion.current ? 0 : 250,
|
||||
duration: prefersReducedMotion.current ? 0 : 150,
|
||||
easing: cubicOut,
|
||||
});
|
||||
|
||||
@@ -49,7 +51,7 @@ const hasSelection = $derived(selectedCount > 0);
|
||||
<!-- Collapsible card wrapper with subtle hover state for affordance -->
|
||||
<CollapsibleRoot
|
||||
bind:open={isOpen}
|
||||
class="w-full rounded-lg border bg-card transition-colors hover:bg-accent/5"
|
||||
class="w-full bg-card transition-colors hover:bg-accent/5"
|
||||
>
|
||||
<!-- Trigger row: title, expand indicator, and optional count badge -->
|
||||
<div class="flex items-center justify-between px-4 py-2">
|
||||
@@ -88,8 +90,8 @@ const hasSelection = $derived(selectedCount > 0);
|
||||
<!-- Expandable content with slide animation -->
|
||||
{#if isOpen}
|
||||
<div
|
||||
transition:slide|local={slideConfig}
|
||||
class="border-t"
|
||||
transition:springySlideFade|local={slideConfig}
|
||||
class="will-change-[height,opacity]"
|
||||
>
|
||||
<div class="px-4 py-3">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
|
||||
@@ -16,6 +16,11 @@ import {
|
||||
Trigger as PopoverTrigger,
|
||||
} from '$shared/shadcn/ui/popover';
|
||||
import { Slider } from '$shared/shadcn/ui/slider';
|
||||
import {
|
||||
Content as TooltipContent,
|
||||
Root as TooltipRoot,
|
||||
Trigger as TooltipTrigger,
|
||||
} from '$shared/shadcn/ui/tooltip';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import type { ChangeEventHandler } from 'svelte/elements';
|
||||
@@ -71,16 +76,35 @@ const handleSliderChange = (newValue: number) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<TooltipRoot>
|
||||
<ButtonGroupRoot class="bg-transparent border-none shadow-none">
|
||||
<TooltipTrigger class="flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="hover:bg-white/50 bg-white/20 border-none"
|
||||
class="
|
||||
group relative border-none size-9
|
||||
bg-white/20 hover:bg-white/60
|
||||
transition-all duration-200 ease-out
|
||||
will-change-transform
|
||||
hover:-translate-y-0.5
|
||||
active:translate-y-0 active:scale-95 active:shadow-none
|
||||
cursor-pointer
|
||||
disabled:opacity-50 disabled:pointer-events-none
|
||||
"
|
||||
size="icon"
|
||||
aria-label={decreaseLabel}
|
||||
onclick={control.decrease}
|
||||
disabled={control.isAtMin}
|
||||
aria-label={decreaseLabel}
|
||||
>
|
||||
<MinusIcon class="size-4" />
|
||||
<MinusIcon
|
||||
class="
|
||||
size-4 transition-all duration-200
|
||||
stroke-slate-600/50
|
||||
group-hover:stroke-indigo-500 group-hover:scale-110 group-hover:stroke-3
|
||||
group-active:scale-90 group-active:-rotate-6
|
||||
group-disabled:stroke-transparent
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
<PopoverRoot>
|
||||
<PopoverTrigger>
|
||||
@@ -88,7 +112,7 @@ const handleSliderChange = (newValue: number) => {
|
||||
<Button
|
||||
{...props}
|
||||
variant="ghost"
|
||||
class="hover:bg-white/50 bg-white/20 border-none"
|
||||
class="hover:bg-white/50 hover:font-bold bg-white/20 border-none duration-150 will-change-transform active:scale-95 cursor-pointer"
|
||||
size="icon"
|
||||
aria-label={controlLabel}
|
||||
>
|
||||
@@ -118,14 +142,39 @@ const handleSliderChange = (newValue: number) => {
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="hover:bg-white/50 bg-white/20 border-none"
|
||||
class="
|
||||
group relative border-none size-9
|
||||
bg-white/20 hover:bg-white/60
|
||||
transition-all duration-200 ease-out
|
||||
will-change-transform
|
||||
hover:-translate-y-0.5
|
||||
active:translate-y-0 active:scale-95 active:shadow-none
|
||||
disabled:opacity-50 disabled:pointer-events-none
|
||||
cursor-pointer
|
||||
"
|
||||
size="icon"
|
||||
aria-label={increaseLabel}
|
||||
onclick={control.increase}
|
||||
disabled={control.isAtMax}
|
||||
>
|
||||
<PlusIcon class="size-4" />
|
||||
<PlusIcon
|
||||
class="
|
||||
size-4 transition-all duration-200
|
||||
stroke-slate-600/50
|
||||
group-hover:stroke-indigo-500 group-hover:scale-110 group-hover:stroke-3
|
||||
group-active:scale-90 group-active:rotate-6
|
||||
group-disabled:stroke-transparent
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</ButtonGroupRoot>
|
||||
{#if controlLabel}
|
||||
<TooltipContent>
|
||||
{controlLabel}
|
||||
</TooltipContent>
|
||||
{/if}
|
||||
</TooltipRoot>
|
||||
|
||||
188
src/shared/ui/ExpandableWrapper/ExpandableWrapper.svelte
Normal file
188
src/shared/ui/ExpandableWrapper/ExpandableWrapper.svelte
Normal file
@@ -0,0 +1,188 @@
|
||||
<!--
|
||||
Component: ExpandableWrapper
|
||||
Animated wrapper for content that can be expanded and collapsed.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { Spring } from 'svelte/motion';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Bindable property to control the expanded state of the wrapper.
|
||||
* @default false
|
||||
*/
|
||||
expanded?: boolean;
|
||||
/**
|
||||
* Disabled flag
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Bindable property to bind:this
|
||||
* @default null
|
||||
*/
|
||||
element?: HTMLElement | null;
|
||||
/**
|
||||
* Content that's always visible
|
||||
*/
|
||||
visibleContent?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
|
||||
/**
|
||||
* Content that's hidden when the wrapper is collapsed
|
||||
*/
|
||||
hiddenContent?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
|
||||
/**
|
||||
* Optional badge to render
|
||||
*/
|
||||
badge?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
|
||||
/**
|
||||
* Rotation animation direction
|
||||
* @default 'clockwise'
|
||||
*/
|
||||
rotation?: 'clockwise' | 'counterclockwise';
|
||||
}
|
||||
|
||||
let {
|
||||
expanded = $bindable(false),
|
||||
disabled = false,
|
||||
element = $bindable(null),
|
||||
visibleContent,
|
||||
hiddenContent,
|
||||
badge,
|
||||
rotation = 'clockwise',
|
||||
class: className = '',
|
||||
...props
|
||||
}: Props = $props();
|
||||
|
||||
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
export const xSpring = new Spring(0, {
|
||||
stiffness: 0.14, // Lower is slower
|
||||
damping: 0.5, // Settle
|
||||
});
|
||||
|
||||
const ySpring = new Spring(0, {
|
||||
stiffness: 0.32,
|
||||
damping: 0.65,
|
||||
});
|
||||
|
||||
const scaleSpring = new Spring(1, {
|
||||
stiffness: 0.32,
|
||||
damping: 0.65,
|
||||
});
|
||||
|
||||
export const rotateSpring = new Spring(0, {
|
||||
stiffness: 0.12,
|
||||
damping: 0.55,
|
||||
});
|
||||
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (element && !element.contains(e.target as Node)) {
|
||||
expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleWrapperClick() {
|
||||
if (!disabled) {
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleWrapperClick();
|
||||
}
|
||||
|
||||
if (expanded && e.key === 'Escape') {
|
||||
expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Elevation and scale on activation
|
||||
$effect(() => {
|
||||
if (expanded && !disabled) {
|
||||
// Lift up
|
||||
ySpring.target = 8;
|
||||
// Slightly bigger
|
||||
scaleSpring.target = 1.1;
|
||||
|
||||
rotateSpring.target = rotation === 'clockwise' ? -0.5 : 0.5;
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
rotateSpring.target = 0;
|
||||
scaleSpring.target = 1.05;
|
||||
}, 300);
|
||||
} else {
|
||||
ySpring.target = 0;
|
||||
scaleSpring.target = 1;
|
||||
rotateSpring.target = 0;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Click outside handling
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (disabled) {
|
||||
expanded = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={element}
|
||||
onclick={handleWrapperClick}
|
||||
onkeydown={handleKeyDown}
|
||||
role="button"
|
||||
tabindex={0}
|
||||
class={cn(
|
||||
'will-change-transform duration-300',
|
||||
disabled ? 'pointer-events-none' : 'pointer-events-auto',
|
||||
className,
|
||||
)}
|
||||
style:transform="
|
||||
translate({xSpring.current}px, {ySpring.current}px)
|
||||
scale({scaleSpring.current})
|
||||
rotateZ({rotateSpring.current}deg)
|
||||
"
|
||||
{...props}
|
||||
>
|
||||
{@render badge?.({ expanded, disabled })}
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
'relative p-2 rounded-2xl border transition-all duration-250 ease-out flex flex-col gap-1.5 backdrop-blur-lg',
|
||||
expanded
|
||||
? '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)]',
|
||||
disabled && 'opacity-80 grayscale-[0.2]',
|
||||
)}
|
||||
>
|
||||
{@render visibleContent?.({ expanded, disabled })}
|
||||
|
||||
{#if expanded}
|
||||
<div
|
||||
in:slide={{ duration: 250, delay: 50 }}
|
||||
out:slide={{ duration: 250 }}
|
||||
>
|
||||
{@render hiddenContent?.({ expanded, disabled })}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,8 +72,9 @@ function handleInputClick() {
|
||||
onclick={handleInputClick}
|
||||
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
|
||||
ring-2 ring-slate-200/50
|
||||
active:ring-indigo-500/50
|
||||
focus-visible:border-indigo-500/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
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 SearchBar from './SearchBar/SearchBar.svelte';
|
||||
import VirtualList from './VirtualList/VirtualList.svelte';
|
||||
|
||||
@@ -16,6 +17,7 @@ export {
|
||||
ComboControl,
|
||||
ComboControlV2,
|
||||
ContentEditable,
|
||||
ExpandableWrapper,
|
||||
SearchBar,
|
||||
VirtualList,
|
||||
};
|
||||
|
||||
1
src/widgets/ComparisonSlider/index.ts
Normal file
1
src/widgets/ComparisonSlider/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ComparisonSlider } from './ui';
|
||||
@@ -3,9 +3,9 @@ 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 { ExpandableWrapper } 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;
|
||||
@@ -29,36 +29,27 @@ let {
|
||||
heightControl,
|
||||
}: Props = $props();
|
||||
|
||||
let panelWidth = $state(0);
|
||||
let panelWidth = $derived(wrapper?.clientWidth ?? 0);
|
||||
const margin = 24;
|
||||
let side = $state<'left' | 'right'>('left');
|
||||
|
||||
// Unified active state for the entire wrapper
|
||||
let isActive = $state(false);
|
||||
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
function handleWrapperClick() {
|
||||
if (!isDragging) {
|
||||
isActive = true;
|
||||
}
|
||||
}
|
||||
const xSpring = new Spring(0, {
|
||||
stiffness: 0.14, // Lower is slower
|
||||
damping: 0.5, // Settle
|
||||
});
|
||||
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (wrapper && !wrapper.contains(e.target as Node)) {
|
||||
isActive = false;
|
||||
}
|
||||
}
|
||||
const rotateSpring = new Spring(0, {
|
||||
stiffness: 0.12,
|
||||
damping: 0.55,
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -74,98 +65,45 @@ $effect(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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) {
|
||||
if (containerWidth > 0 && panelWidth > 0) {
|
||||
// On side change set the position and the rotation
|
||||
xSpring.target = targetX;
|
||||
rotateSpring.target = side === 'right' ? 3.5 : -3.5;
|
||||
|
||||
setTimeout(() => {
|
||||
timeoutId = 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;
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
|
||||
$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'}
|
||||
class="absolute top-6 left-6 z-50 will-change-transform"
|
||||
style:transform="
|
||||
translate({xSpring.current}px, {ySpring.current}px)
|
||||
scale({scaleSpring.current})
|
||||
translateX({xSpring.current}px)
|
||||
rotateZ({rotateSpring.current}deg)
|
||||
"
|
||||
role="button"
|
||||
tabindex={0}
|
||||
onkeydown={handleKeyDown}
|
||||
aria-label="Font controls"
|
||||
>
|
||||
<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',
|
||||
)}
|
||||
>
|
||||
{#snippet badge()}
|
||||
<div
|
||||
class={cn(
|
||||
'animate-nudge relative transition-all',
|
||||
@@ -176,17 +114,9 @@ $effect(() => {
|
||||
>
|
||||
<AArrowUP class={cn('size-3', 'stroke-indigo-600')} />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<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-80 grayscale-[0.2]',
|
||||
)}
|
||||
style:backdrop-filter="blur(24px)"
|
||||
>
|
||||
{#snippet visibleContent()}
|
||||
<div class="relative px-2 py-1">
|
||||
<Input
|
||||
bind:value={text}
|
||||
@@ -198,44 +128,17 @@ $effect(() => {
|
||||
: '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..."
|
||||
placeholder="The quick brown fox..."
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if isActive}
|
||||
<div
|
||||
in:slide={{ duration: 250, delay: 50 }}
|
||||
out:slide={{ duration: 250 }}
|
||||
class="flex justify-between items-center-safe"
|
||||
>
|
||||
{#snippet hiddenContent()}
|
||||
<div class="flex justify-between items-center-safe">
|
||||
<ComboControlV2 control={weightControl} />
|
||||
<ComboControlV2 control={sizeControl} />
|
||||
<ComboControlV2 control={heightControl} />
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ExpandableWrapper>
|
||||
</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>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import FiltersSidebar from './ui/FiltersSidebar.svelte';
|
||||
|
||||
export { FiltersSidebar };
|
||||
@@ -1,38 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
FilterControls,
|
||||
Filters,
|
||||
} from '$features/GetFonts';
|
||||
import {
|
||||
Content as SidebarContent,
|
||||
Root as SidebarRoot,
|
||||
} from '$shared/shadcn/ui/sidebar/index';
|
||||
/**
|
||||
* FiltersSidebar Component
|
||||
*
|
||||
* Main application sidebar widget. Contains filter controls and action buttons
|
||||
* for font filtering operations. Organized into two sections:
|
||||
*
|
||||
* - Filters: Category-based filter groups (providers, subsets, categories)
|
||||
* - Controls: Reset button for filter actions
|
||||
*
|
||||
* Features:
|
||||
* - Loading indicator during font fetch operations
|
||||
* - Empty state message when no fonts match filters
|
||||
* - Error display for failed font operations
|
||||
* - Responsive sidebar behavior via shadcn Sidebar component
|
||||
*
|
||||
* Uses Sidebar.Root from shadcn for responsive sidebar behavior including
|
||||
* mobile drawer and desktop persistent sidebar modes.
|
||||
*/
|
||||
</script>
|
||||
|
||||
<SidebarRoot>
|
||||
<SidebarContent class="p-2">
|
||||
<!-- Filter groups -->
|
||||
<Filters />
|
||||
|
||||
<!-- Action buttons -->
|
||||
<FilterControls />
|
||||
</SidebarContent>
|
||||
</SidebarRoot>
|
||||
1
src/widgets/FontSearch/index.ts
Normal file
1
src/widgets/FontSearch/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { FontSearch } from './ui';
|
||||
107
src/widgets/FontSearch/ui/FontSearch/FontSearch.svelte
Normal file
107
src/widgets/FontSearch/ui/FontSearch/FontSearch.svelte
Normal file
@@ -0,0 +1,107 @@
|
||||
<!--
|
||||
Component: FontSearch
|
||||
|
||||
Combines search input with font list display
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { fontshareStore } from '$entities/Font';
|
||||
import {
|
||||
FilterControls,
|
||||
Filters,
|
||||
SuggestedFonts,
|
||||
filterManager,
|
||||
mapManagerToParams,
|
||||
} from '$features/GetFonts';
|
||||
import { springySlideFade } from '$shared/lib';
|
||||
import { Button } from '$shared/shadcn/ui/button';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import { SearchBar } from '$shared/ui';
|
||||
import FunnelIcon from '@lucide/svelte/icons/funnel';
|
||||
import { onMount } from 'svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import {
|
||||
Tween,
|
||||
prefersReducedMotion,
|
||||
} from 'svelte/motion';
|
||||
import { type SlideParams } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
showFilters?: boolean;
|
||||
}
|
||||
|
||||
let { showFilters = $bindable(false) }: Props = $props();
|
||||
|
||||
onMount(() => {
|
||||
/**
|
||||
* The Pairing:
|
||||
* We "plug" this manager into the global store.
|
||||
* addBinding returns a function that removes this binding when the component unmounts.
|
||||
*/
|
||||
const unbind = fontshareStore.addBinding(() => mapManagerToParams(filterManager));
|
||||
|
||||
return unbind;
|
||||
});
|
||||
|
||||
const transform = new Tween(
|
||||
{ scale: 1, rotate: 0 },
|
||||
{ duration: 250, easing: cubicOut },
|
||||
);
|
||||
|
||||
const slideConfig = $derived<SlideParams>({
|
||||
duration: prefersReducedMotion.current ? 0 : 200,
|
||||
easing: cubicOut,
|
||||
axis: 'y',
|
||||
});
|
||||
|
||||
function toggleFilters() {
|
||||
showFilters = !showFilters;
|
||||
|
||||
transform.set({ scale: 0.9, rotate: 2 }).then(() => {
|
||||
transform.set({ scale: 1, rotate: 0 });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 relative">
|
||||
<SearchBar
|
||||
id="font-search"
|
||||
class="w-full"
|
||||
placeholder="Search fonts by name..."
|
||||
bind:value={filterManager.queryValue}
|
||||
>
|
||||
<SuggestedFonts />
|
||||
</SearchBar>
|
||||
|
||||
<div class="absolute right-5 top-10 translate-y-[-50%] pl-5 border-l-2">
|
||||
<div style:transform="scale({transform.current.scale}) rotate({transform.current.rotate}deg)">
|
||||
<Button
|
||||
class={cn(
|
||||
'cursor-pointer will-change-transform hover:bg-inherit hover:*:stroke-indigo-500',
|
||||
showFilters ? 'hover:*:stroke-indigo-500/80' : 'hover:*:stroke-indigo-500',
|
||||
)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onclick={toggleFilters}
|
||||
>
|
||||
<FunnelIcon
|
||||
class={cn(
|
||||
'size-8 stroke-indigo-600/50 transition-all duration-150',
|
||||
showFilters ? 'stroke-indigo-600' : 'stroke-indigo-600/50',
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showFilters}
|
||||
<div
|
||||
transition:springySlideFade|local={slideConfig}
|
||||
class="will-change-[height,opacity] contain-layout overflow-hidden"
|
||||
>
|
||||
<div class="grid gap-1 grid-cols-[repeat(auto-fit,minmax(8em,14em))]">
|
||||
<Filters />
|
||||
<FilterControls class="ml-auto py-1" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
3
src/widgets/FontSearch/ui/index.ts
Normal file
3
src/widgets/FontSearch/ui/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import FontSearch from './FontSearch/FontSearch.svelte';
|
||||
|
||||
export { FontSearch };
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { FontSearch } from '$features/GetFonts';
|
||||
import SetupFontMenu from '$features/SetupFont/ui/SetupFontMenu.svelte';
|
||||
import {
|
||||
Content as ItemContent,
|
||||
@@ -7,11 +6,10 @@ import {
|
||||
} from '$shared/shadcn/ui/item';
|
||||
</script>
|
||||
|
||||
<div class="w-full p-2">
|
||||
<div class="w-full p-2 backdrop-blur-2xl">
|
||||
<ItemRoot variant="outline" class="w-full p-2.5">
|
||||
<ItemContent class="flex flex-row justify-center items-center">
|
||||
<SetupFontMenu />
|
||||
<FontSearch />
|
||||
</ItemContent>
|
||||
</ItemRoot>
|
||||
</div>
|
||||
|
||||
3
src/widgets/index.ts
Normal file
3
src/widgets/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { ComparisonSlider } from './ComparisonSlider';
|
||||
export { FontSearch } from './FontSearch';
|
||||
export { TypographyMenu } from './TypographySettings';
|
||||
Reference in New Issue
Block a user