import type { IconComponentProps } from 'phosphor-svelte'; import type { Component } from 'svelte'; /** * IconDef is an object that represents an icon element from the phosphor library, alongside an optional set of typed properties. */ export type IconDef = { component: Component; props?: IconComponentProps; }; /** * Defines a set of default properties for icons used in the application. */ export const defaultIconProps: IconComponentProps = { size: '1.2rem', weight: 'bold' }; /** * MaybeGetter is a type that can either be a value of type T or a function that returns a value of type T. * This is useful for cases where you might want to pass a value directly or a function that computes the * value later, potentially taking advantage of reactivity. */ export type MaybeGetter = T | (() => T); /** * ResolveGetter returns the underlying value stored by a MaybeGetter type. * @returns Raw value T or function return T. */ export const resolveGetter = (getter: MaybeGetter): T => { if (typeof getter === 'function') { return (getter as () => T)(); } return getter; }; /** * Generates a unique identifier string unless an identifier is provided. * If a prefix is provided, it will be prepended to the identifier. * The identifier is a combination of a random part and a timestamp. * * @param {string} [prefix] - Optional prefix to prepend to the identifier. * @param {string} [identifier] - Optional identifier to use instead of generating a new one. * @returns {string} - A unique identifier string. */ export const generateIdentifier = (prefix?: string, identifier?: string): string => { if (identifier) { return `${prefix ? prefix + '-' : ''}${identifier}`; } const randomPart = Math.random().toString(36).substring(2, 10); const timestampPart = Date.now().toString(36); return `${prefix ? prefix + '-' : ''}${randomPart}-${timestampPart}`; }; /** * Option type definition for a select option. Used in various components. */ export type Option = | { value: string; label?: string; } | string; /** getLabel returns the option label if it exists*/ export const getLabel = (option: Option): string => { if (typeof option === 'string') { return option; } else { return option.label ?? option.value; } }; /** getValue returns the option value */ export const getValue = (option: Option): string => { if (typeof option === 'string') { return option; } else { return option.value; } }; /** * targetMust returns the target of an event coerced to a particular type and * throws an error if the target does not exist or is not connected. * * @returns The target element coerced to a particular type. * @throws Will throw an error if the target is null or not connected to the DOM. */ export function targetMust(event: Event): T { const target = event.target as T | null; if (!target) { throw new Error('Event target is null'); } if (!target.isConnected) { throw new Error('Event target is not connected to the DOM'); } return target; } /** * capitalizeFirstLetter capitalizes the first letter of a string * @param str The string to capitalize * @returns The string with the first letter capitalized and all others lowercase */ export const capitalizeFirstLetter = (str: string): string => { const lower = str.toLowerCase(); return lower.charAt(0).toUpperCase() + lower.slice(1); }; /** * prefixZero adds a leading zero to the string if it is less than 10 and not 0 * @param str The string to prefix * @returns The string with a leading zero if it was only 1 digit long */ export const prefixZero = (str: string): string => { if (str.length === 1 && str !== '0') { return '0' + str; } return str; }; /** * Trims the specified character from the start and/or end of the string. * @param str The string to trim. * @param char The character to trim. * @param trimStart Whether to trim from the start of the string. Default: true. * @param trimEnd Whether to trim from the end of the string. Default: true. * @returns The trimmed string. */ export const trimEdges = (str: string, char: string, trimStart?: boolean, trimEnd?: boolean) => { let start = 0, end = str.length; if (trimStart || trimStart === undefined) { while (start < end && str[start] === char) start++; } if (trimEnd || trimEnd === undefined) { while (end > start && str[end - 1] === char) end--; } return str.substring(start, end); }; // helper: only treat plain objects as mergeable const isPlainObject = (v: unknown): v is Record => typeof v === 'object' && v !== null && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype; /** Merge two plain object maps. No `any` used. */ function mergePlainObjects( baseObj: Record, overrideObj: Record ): Record { const res: Record = { ...baseObj }; for (const k of Object.keys(overrideObj)) { const v = overrideObj[k]; if (v === undefined) continue; // undefined preserves base const b = res[k]; if (isPlainObject(v) && isPlainObject(b)) { res[k] = mergePlainObjects(b as Record, v as Record); } else { // primitives, null, arrays, non-plain objects replace base res[k] = v; } } return res; } /** * Merge `base` with `override`. * - `null` in `override` replaces (kept as valid override) * - `undefined` in `override` is ignored (keeps base) * - Only plain objects are deep-merged * - If `override` is null/undefined we return a shallow copy of `base` */ export const mergeOverrideObject = >( base: T, override?: Partial | null ): T => { if (override == null) return { ...base } as T; // Use plain maps internally to avoid explicit any const baseMap = { ...base } as Record; const overrideMap = override as Record; const merged = mergePlainObjects(baseMap, overrideMap); return merged as T; };