refactor(shared): update utilities, API layer, and types

This commit is contained in:
Ilia Mashkov
2026-03-02 22:19:13 +03:00
parent ac73fd5044
commit 13818d5844
17 changed files with 554 additions and 96 deletions

View File

@@ -1,3 +1,9 @@
/**
* Shared library
*
* Reusable utilities, helpers, and providers for the application.
*/
export {
type CharacterComparison,
type ControlDataModel,

View File

@@ -11,6 +11,9 @@ import { setContext } from 'svelte';
import type { Snippet } from 'svelte';
interface Props {
/**
* Content snippet
*/
children: Snippet;
}

View File

@@ -15,15 +15,15 @@ import type {
interface Props {
/**
* The Lucide icon component
* Lucide icon component
*/
icon: Component;
/**
* CSS classes to apply to the icon
* CSS classes
*/
class?: string;
/**
* Additional icon-specific attributes
* Additional attributes
*/
attrs?: Record<string, unknown>;
}

View File

@@ -15,17 +15,22 @@ import { setContext } from 'svelte';
import type { Snippet } from 'svelte';
interface Props {
/**
* Content snippet
*/
children: Snippet;
/**
* Initial viewport width for the responsive context (default: 1280)
* Initial viewport width
* @default 1280
*/
initialWidth?: number;
/**
* Initial viewport height for the responsive context (default: 720)
* Initial viewport height
* @default 720
*/
initialHeight?: number;
/**
* Tooltip provider options
* Tooltip delay duration
*/
tooltipDelayDuration?: number;
/**

View File

@@ -1,56 +1,46 @@
/**
* Build query string from URL parameters
* Builds URL query strings from parameter objects
*
* Generic, type-safe function to build properly encoded query strings
* from URL parameters. Supports primitives, arrays, and optional values.
*
* @param params - Object containing query parameters
* @returns Encoded query string (empty string if no parameters)
* Creates properly encoded query strings from typed parameter objects.
* Handles primitives, arrays, and omits null/undefined values.
*
* @example
* ```ts
* buildQueryString({ category: 'serif', subsets: ['latin', 'latin-ext'] })
* // Returns: "category=serif&subsets=latin&subsets=latin-ext"
* // Returns: "?category=serif&subsets=latin%2Clatin-ext"
*
* buildQueryString({ limit: 50, page: 1 })
* // Returns: "limit=50&page=1"
* // Returns: "?limit=50&page=1"
*
* buildQueryString({})
* // Returns: ""
*
* buildQueryString({ search: 'hello world', active: true })
* // Returns: "search=hello%20world&active=true"
* // Returns: "?search=hello%20world&active=true"
* ```
*/
/**
* Query parameter value type
* Supports primitives, arrays, and excludes null/undefined
* Supported query parameter value types
*/
export type QueryParamValue = string | number | boolean | string[] | number[];
/**
* Query parameters object
* Query parameters object with optional values
*/
export type QueryParams = Record<string, QueryParamValue | undefined | null>;
/**
* Build query string from URL parameters
* Builds a URL query string from a parameters object
*
* Handles:
* - Primitive values (string, number, boolean)
* - Arrays (multiple values with same key)
* - Optional values (excludes undefined/null)
* - Proper URL encoding
*
* Edge cases:
* - Empty object → empty string
* - No parameters → empty string
* - Nested objects → flattens to string representation
* - Special characters → proper encoding
* - Primitive values (string, number, boolean) - converted to strings
* - Arrays - comma-separated values
* - null/undefined - omitted from output
* - Special characters - URL encoded
*
* @param params - Object containing query parameters
* @returns Encoded query string (with "?" prefix if non-empty)
* @returns Encoded query string with "?" prefix, or empty string if no params
*/
export function buildQueryString(params: QueryParams): string {
const searchParams = new URLSearchParams();
@@ -61,12 +51,14 @@ export function buildQueryString(params: QueryParams): string {
continue;
}
// Handle arrays (multiple values with same key)
// Handle arrays (comma-separated values)
if (Array.isArray(value)) {
for (const item of value) {
if (item !== undefined && item !== null) {
searchParams.append(key, String(item));
}
const joined = value
.filter(item => item !== undefined && item !== null)
.map(String)
.join(',');
if (joined) {
searchParams.append(key, joined);
}
} else {
// Handle primitives

View File

@@ -1,9 +1,20 @@
/**
* Clamp a number within a range.
* @param value The number to clamp.
* @param min minimum value
* @param max maximum value
* @returns The clamped number.
* Clamps a number within a specified range
*
* Ensures a value falls between minimum and maximum bounds.
* Values below min return min, values above max return max.
*
* @param value - The number to clamp
* @param min - Minimum allowed value (inclusive)
* @param max - Maximum allowed value (inclusive)
* @returns The clamped number
*
* @example
* ```ts
* clampNumber(5, 0, 10); // 5
* clampNumber(-5, 0, 10); // 0
* clampNumber(15, 0, 10); // 10
* ```
*/
export function clampNumber(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);

View File

@@ -1,28 +1,24 @@
/**
* ============================================================================
* DEBOUNCE UTILITY
* ============================================================================
*
* Creates a debounced function that delays execution until after wait milliseconds
* have elapsed since the last time it was invoked.
*
* @example
* ```typescript
* const debouncedSearch = debounce((query: string) => {
* console.log('Searching for:', query);
* }, 300);
*
* debouncedSearch('hello');
* debouncedSearch('hello world'); // Only this will execute after 300ms
* ```
*/
/**
* Creates a debounced version of a function
*
* Delays function execution until after `wait` milliseconds have elapsed
* since the last invocation. Useful for rate-limiting expensive operations
* like API calls or expensive DOM updates.
*
* @example
* ```ts
* const search = debounce((query: string) => {
* console.log('Searching for:', query);
* }, 300);
*
* search('a');
* search('ab');
* search('abc'); // Only this triggers the function after 300ms
* ```
*
* @param fn - The function to debounce
* @param wait - The delay in milliseconds
* @returns A debounced function that will execute after the specified delay
* @returns A debounced function that executes after the delay
*/
export function debounce<T extends (...args: any[]) => any>(
fn: T,

View File

@@ -1,14 +1,19 @@
/**
* Get the number of decimal places in a number
* Counts the number of decimal places in a number
*
* For example:
* - 1 -> 0
* - 0.1 -> 1
* - 0.01 -> 2
* - 0.05 -> 2
* Returns the length of the decimal portion of a number.
* Used to determine precision for rounding operations.
*
* @param step - The step number to analyze
* @returns The number of decimal places
* @param step - The number to analyze
* @returns The number of decimal places (0 for integers)
*
* @example
* ```ts
* getDecimalPlaces(1); // 0
* getDecimalPlaces(0.1); // 1
* getDecimalPlaces(0.01); // 2
* getDecimalPlaces(0.005); // 3
* ```
*/
export function getDecimalPlaces(step: number): number {
const str = step.toString();

View File

@@ -1,5 +1,12 @@
/**
* Shared utility functions
*
* Provides common utilities for:
* - Number manipulation (clamping, precision, decimal places)
* - Function execution control (debounce, throttle)
* - Array operations (split by predicate)
* - URL handling (query string building)
* - DOM interactions (smooth scrolling)
*/
export {

View File

@@ -1,19 +1,25 @@
import { getDecimalPlaces } from '$shared/lib/utils';
/**
* Round a value to the precision of the given step
* Rounds a value to match the precision of a given step
*
* This fixes floating-point precision errors that occur with decimal steps.
* For example, with step=0.05, adding it repeatedly can produce values like
* 1.3499999999999999 instead of 1.35.
* Fixes floating-point precision errors that occur with decimal arithmetic.
* For example, repeatedly adding 0.05 can produce 1.3499999999999999
* instead of 1.35 due to IEEE 754 floating-point representation.
*
* We use toFixed() to round to the appropriate decimal places instead of
* Math.round(value / step) * step, which doesn't always work correctly
* due to floating-point arithmetic errors.
* Uses toFixed() instead of Math.round() for correct decimal rounding.
*
* @param value - The value to round
* @param step - The step to round to (defaults to 1)
* @param step - The step size to match precision of (default: 1)
* @returns The rounded value
*
* @example
* ```ts
* roundToStepPrecision(1.3499999999999999, 0.05); // 1.35
* roundToStepPrecision(1.2345, 0.01); // 1.23
* roundToStepPrecision(1.2345, 0.1); // 1.2
* roundToStepPrecision(1.5, 1); // 2
* ```
*/
export function roundToStepPrecision(value: number, step: number = 1): number {
if (step <= 0) {

View File

@@ -1,6 +1,17 @@
/**
* Smoothly scrolls to the target element when an anchor element is clicked.
* @param node - The anchor element to listen for clicks on.
* Svelte action for smooth anchor scrolling
*
* Intercepts anchor link clicks to smoothly scroll to the target element
* instead of jumping instantly. Updates URL hash without causing scroll.
*
* @example
* ```svelte
* <a href="#section" use:smoothScroll>Go to Section</a>
* <div id="section">Section Content</div>
* ```
*
* @param node - The anchor element to attach to
* @returns Action object with destroy method
*/
export function smoothScroll(node: HTMLAnchorElement) {
const handleClick = (event: MouseEvent) => {
@@ -17,7 +28,7 @@ export function smoothScroll(node: HTMLAnchorElement) {
block: 'start',
});
// Update URL hash without jumping
// Update URL hash without triggering scroll
history.pushState(null, '', hash);
}
};

View File

@@ -1,8 +1,26 @@
/**
* Splits an array into two arrays based on a callback function.
* @param array The array to split.
* @param callback The callback function to determine which array to push each item to.
* @returns - An array containing two arrays, the first array contains items that passed the callback, the second array contains items that failed the callback.
* Splits an array into two groups based on a predicate
*
* Partitions an array into pass/fail groups using a callback function.
* More efficient than calling filter() twice.
*
* @param array - The array to split
* @param callback - Predicate function (true = first array, false = second)
* @returns Tuple of [passing items, failing items]
*
* @example
* ```ts
* const numbers = [1, 2, 3, 4, 5, 6];
* const [even, odd] = splitArray(numbers, n => n % 2 === 0);
* // even: [2, 4, 6]
* // odd: [1, 3, 5]
*
* const users = [
* { name: 'Alice', active: true },
* { name: 'Bob', active: false }
* ];
* const [active, inactive] = splitArray(users, u => u.active);
* ```
*/
export function splitArray<T>(array: T[], callback: (item: T) => boolean) {
return array.reduce<[T[], T[]]>(

View File

@@ -1,9 +1,23 @@
/**
* Throttle function execution to a maximum frequency.
* Throttles a function to limit execution frequency
*
* @param fn Function to throttle.
* @param wait Maximum time between function calls.
* @returns Throttled function.
* Ensures a function executes at most once per `wait` milliseconds.
* Unlike debounce, throttled functions execute on the leading edge
* and trailing edge if called repeatedly.
*
* @example
* ```ts
* const logScroll = throttle(() => {
* console.log('Scroll position:', window.scrollY);
* }, 100);
*
* window.addEventListener('scroll', logScroll);
* // Will log at most once every 100ms
* ```
*
* @param fn - Function to throttle
* @param wait - Minimum time between executions in milliseconds
* @returns Throttled function
*/
export function throttle<T extends (...args: any[]) => any>(
fn: T,
@@ -20,7 +34,7 @@ export function throttle<T extends (...args: any[]) => any>(
lastCall = now;
fn(...args);
} else {
// Schedule for end of wait period
// Schedule for end of wait period (trailing edge)
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
lastCall = Date.now();