diff --git a/src/app/ui/Layout.svelte b/src/app/ui/Layout.svelte index d9c7baa..a47a9d7 100644 --- a/src/app/ui/Layout.svelte +++ b/src/app/ui/Layout.svelte @@ -41,7 +41,7 @@ let { children }: Props = $props();
-
+
{@render children?.()} diff --git a/src/entities/Font/api/index.ts b/src/entities/Font/api/index.ts index 50c12ef..a3d8108 100644 --- a/src/entities/Font/api/index.ts +++ b/src/entities/Font/api/index.ts @@ -4,6 +4,17 @@ * Exports API clients and normalization utilities */ +// Proxy API (PRIMARY - NEW) +export { + fetchProxyFontById, + fetchProxyFonts, +} from './proxy/proxyFonts'; +export type { + ProxyFontsParams, + ProxyFontsResponse, +} from './proxy/proxyFonts'; + +// Google Fonts API (DEPRECATED - kept for backward compatibility) export { fetchGoogleFontFamily, fetchGoogleFonts, @@ -14,6 +25,7 @@ export type { GoogleFontsResponse, } from './google/googleFonts'; +// Fontshare API (DEPRECATED - kept for backward compatibility) export { fetchAllFontshareFonts, fetchFontshareFontBySlug, diff --git a/src/entities/Font/api/proxy/proxyFonts.ts b/src/entities/Font/api/proxy/proxyFonts.ts new file mode 100644 index 0000000..253b7fb --- /dev/null +++ b/src/entities/Font/api/proxy/proxyFonts.ts @@ -0,0 +1,160 @@ +/** + * Proxy API client + * + * Handles API requests to GlyphDiff proxy API for fetching font metadata. + * Provides error handling, pagination support, and type-safe responses. + * + * Proxy API normalizes font data from Google Fonts and Fontshare into a single + * unified format, eliminating the need for client-side normalization. + * + * @see https://api.glyphdiff.com/api/v1/fonts + */ + +import { api } from '$shared/api/api'; +import { buildQueryString } from '$shared/lib/utils'; +import type { QueryParams } from '$shared/lib/utils'; +import type { UnifiedFont } from '../../model/types'; +import type { + FontCategory, + FontSubset, +} from '../../model/types'; + +/** + * Proxy API base URL + */ +const PROXY_API_URL = 'https://api.glyphdiff.com/api/v1/fonts' as const; + +/** + * Proxy API parameters + * + * Maps directly to the proxy API query parameters + */ +export interface ProxyFontsParams extends QueryParams { + /** + * Font provider filter ("google" or "fontshare") + * Omit to fetch from both providers + */ + provider?: 'google' | 'fontshare'; + + /** + * Font category filter + */ + category?: FontCategory; + + /** + * Character subset filter + */ + subset?: FontSubset; + + /** + * Search query (e.g., "roboto", "satoshi") + */ + q?: string; + + /** + * Sort order for results + * "name" - Alphabetical by font name + * "popularity" - Most popular first + * "lastModified" - Recently updated first + */ + sort?: 'name' | 'popularity' | 'lastModified'; + + /** + * Number of items to return (pagination) + */ + limit?: number; + + /** + * Number of items to skip (pagination) + * Use for pagination: offset = (page - 1) * limit + */ + offset?: number; +} + +/** + * Proxy API response + * + * Includes pagination metadata alongside font data + */ +export interface ProxyFontsResponse { + /** Array of unified font objects */ + fonts: UnifiedFont[]; + + /** Total number of fonts matching the query */ + total: number; + + /** Limit used for this request */ + limit: number; + + /** Offset used for this request */ + offset: number; +} + +/** + * Fetch fonts from proxy API + * + * @param params - Query parameters for filtering and pagination + * @returns Promise resolving to proxy API response + * @throws ApiError when request fails + * + * @example + * ```ts + * // Fetch all sans-serif fonts from Google + * const response = await fetchProxyFonts({ + * provider: 'google', + * category: 'sans-serif', + * limit: 50, + * offset: 0 + * }); + * + * // Search fonts across all providers + * const searchResponse = await fetchProxyFonts({ + * q: 'roboto', + * limit: 20 + * }); + * + * // Fetch fonts with pagination + * const page1 = await fetchProxyFonts({ limit: 50, offset: 0 }); + * const page2 = await fetchProxyFonts({ limit: 50, offset: 50 }); + * ``` + */ +export async function fetchProxyFonts( + params: ProxyFontsParams = {}, +): Promise { + const queryString = buildQueryString(params); + const url = `${PROXY_API_URL}${queryString}`; + + try { + const response = await api.get(url); + return response.data; + } catch (error) { + // Re-throw ApiError with context + if (error instanceof Error) { + throw error; + } + throw new Error(`Failed to fetch fonts from proxy API: ${String(error)}`); + } +} + +/** + * Fetch font by ID + * + * Convenience function for fetching a single font by ID + * Note: This fetches a page and filters client-side, which is not ideal + * For production, consider adding a dedicated endpoint to the proxy API + * + * @param id - Font ID (family name for Google, slug for Fontshare) + * @returns Promise resolving to font or undefined + * + * @example + * ```ts + * const roboto = await fetchProxyFontById('Roboto'); + * const satoshi = await fetchProxyFontById('satoshi'); + * ``` + */ +export async function fetchProxyFontById( + id: string, +): Promise { + const response = await fetchProxyFonts({ limit: 1000, q: id }); + return response.fonts.find(font => font.id === id); +} diff --git a/src/routes/Page.svelte b/src/routes/Page.svelte index 3869dd4..08568e4 100644 --- a/src/routes/Page.svelte +++ b/src/routes/Page.svelte @@ -3,8 +3,15 @@ import { appliedFontsManager } from '$entities/Font'; import { displayedFontsStore } from '$features/DisplayFont'; import FontDisplay from '$features/DisplayFont/ui/FontDisplay/FontDisplay.svelte'; import { controlManager } from '$features/SetupFont'; +import { cn } from '$shared/shadcn/utils/shadcn-utils'; import ComparisonSlider from '$widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.svelte'; import { FontSearch } from '$widgets/FontSearch'; +import { cubicOut } from 'svelte/easing'; +import { Spring } from 'svelte/motion'; +import type { + SlideParams, + TransitionConfig, +} from 'svelte/transition'; /** * Page Component @@ -13,6 +20,9 @@ import { FontSearch } from '$widgets/FontSearch'; let searchContainer: HTMLElement; let isExpanded = $state(false); +let isOpen = $state(false); + +let isEmptyScreen = $derived(!displayedFontsStore.hasAnyFonts && !isExpanded && !isOpen); $effect(() => { appliedFontsManager.touch( @@ -22,19 +32,63 @@ $effect(() => { -
-
- +
+ {#key isEmptyScreen} +
+
+ +
+
+ {/key} + +
+
- - -
+
diff --git a/src/shared/ui/SearchBar/SearchBar.svelte b/src/shared/ui/SearchBar/SearchBar.svelte index 9edfc86..09abb16 100644 --- a/src/shared/ui/SearchBar/SearchBar.svelte +++ b/src/shared/ui/SearchBar/SearchBar.svelte @@ -17,30 +17,46 @@ import { useId } from 'bits-ui'; import type { Snippet } from 'svelte'; interface Props { - /** Unique identifier for the input element */ + /** + * Unique identifier for the input element + */ id?: string; - /** Current search value (bindable) */ + /** + * Current search value (bindable) + */ value: string; - /** Additional CSS classes for the container */ + /** + * Whether popover is open (bindable) + */ + isOpen?: boolean; + /** + * Additional CSS classes for the container + */ class?: string; - /** Placeholder text for the input */ + /** + * Placeholder text for the input + */ placeholder?: string; - /** Optional label displayed above the input */ + /** + * Optional label displayed above the input + */ label?: string; - /** Content to render inside the popover (receives unique content ID) */ + /** + * Content to render inside the popover (receives unique content ID) + */ children: Snippet<[{ id: string }]> | undefined; } let { id = 'search-bar', - value = $bindable(), + value = $bindable(''), + isOpen = $bindable(false), class: className, placeholder, label, children, }: Props = $props(); -let open = $state(false); let triggerRef = $state(null!); // svelte-ignore state_referenced_locally const contentId = useId(id); @@ -52,11 +68,11 @@ function handleKeyDown(event: KeyboardEvent) { } function handleInputClick() { - open = true; + isOpen = true; } - + {#snippet child({ props })} {@const { onclick, ...rest } = props} diff --git a/src/widgets/ComparisonSlider/ui/ComparisonSlider/FontShifter.svelte b/src/widgets/ComparisonSlider/ui/ComparisonSlider/FontShifter.svelte new file mode 100644 index 0000000..03135fd --- /dev/null +++ b/src/widgets/ComparisonSlider/ui/ComparisonSlider/FontShifter.svelte @@ -0,0 +1,52 @@ + + +
+ {#each chars as char, i} + + {char} + + {/each} +
+ + diff --git a/src/widgets/FontSearch/ui/FontSearch/FontSearch.svelte b/src/widgets/FontSearch/ui/FontSearch/FontSearch.svelte index 3a53119..10a0ae5 100644 --- a/src/widgets/FontSearch/ui/FontSearch/FontSearch.svelte +++ b/src/widgets/FontSearch/ui/FontSearch/FontSearch.svelte @@ -27,9 +27,10 @@ import { type SlideParams } from 'svelte/transition'; interface Props { showFilters?: boolean; + isOpen?: boolean; } -let { showFilters = $bindable(false) }: Props = $props(); +let { showFilters = $bindable(false), isOpen = $bindable(false) }: Props = $props(); onMount(() => { /** @@ -68,6 +69,7 @@ function toggleFilters() { class="w-full" placeholder="Search fonts by name..." bind:value={filterManager.queryValue} + bind:isOpen > diff --git a/src/widgets/TypographySettings/ui/TypographyMenu.svelte b/src/widgets/TypographySettings/ui/TypographyMenu.svelte index 8437736..f384493 100644 --- a/src/widgets/TypographySettings/ui/TypographyMenu.svelte +++ b/src/widgets/TypographySettings/ui/TypographyMenu.svelte @@ -24,7 +24,7 @@ const [send, receive] = crossfade({ {#if displayedFontsStore.hasAnyFonts}