This commit is contained in:
eewing
2026-02-17 14:10:16 -06:00
parent 2bca5834c5
commit cf73cd3b4c
11246 changed files with 1690552 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
/**
* A utility function that executes a callback after a specified number of milliseconds.
*/
export declare function afterSleep(ms: number, cb: () => void): NodeJS.Timeout;
+6
View File
@@ -0,0 +1,6 @@
/**
* A utility function that executes a callback after a specified number of milliseconds.
*/
export function afterSleep(ms, cb) {
return setTimeout(cb, ms);
}
+2
View File
@@ -0,0 +1,2 @@
import type { AnyFn } from "../types.js";
export declare function afterTick(fn: AnyFn): void;
+4
View File
@@ -0,0 +1,4 @@
import { tick } from "svelte";
export function afterTick(fn) {
tick().then(fn);
}
+27
View File
@@ -0,0 +1,27 @@
import { type WritableBox } from "../box/box-extras.svelte.js";
type RefSetter<T> = (v: T) => void;
/**
* Creates a Svelte Attachment that attaches a DOM element to a ref.
* The ref can be either a WritableBox or a callback function.
*
* @param ref - Either a WritableBox to store the element in, or a callback function that receives the element
* @param onChange - Optional callback that fires when the ref changes
* @returns An object with a spreadable attachment key that should be spread onto the element
*
* @example
* // Using with WritableBox
* const ref = box<HTMLDivElement | null>(null);
* <div {...attachRef(ref)}>Content</div>
*
* @example
* // Using with callback
* <div {...attachRef((node) => myNode = node)}>Content</div>
*
* @example
* // Using with onChange
* <div {...attachRef(ref, (node) => console.log(node))}>Content</div>
*/
export declare function attachRef<T extends EventTarget = Element>(ref: WritableBox<T | null> | RefSetter<T | null>, onChange?: (v: T | null) => void): {
[x: symbol]: (node: T) => () => void;
};
export {};
+50
View File
@@ -0,0 +1,50 @@
import { untrack } from "svelte";
import { isBox } from "../box/box-extras.svelte.js";
import { createAttachmentKey } from "svelte/attachments";
/**
* Creates a Svelte Attachment that attaches a DOM element to a ref.
* The ref can be either a WritableBox or a callback function.
*
* @param ref - Either a WritableBox to store the element in, or a callback function that receives the element
* @param onChange - Optional callback that fires when the ref changes
* @returns An object with a spreadable attachment key that should be spread onto the element
*
* @example
* // Using with WritableBox
* const ref = box<HTMLDivElement | null>(null);
* <div {...attachRef(ref)}>Content</div>
*
* @example
* // Using with callback
* <div {...attachRef((node) => myNode = node)}>Content</div>
*
* @example
* // Using with onChange
* <div {...attachRef(ref, (node) => console.log(node))}>Content</div>
*/
export function attachRef(ref, onChange) {
return {
[createAttachmentKey()]: (node) => {
if (isBox(ref)) {
ref.current = node;
untrack(() => onChange?.(node));
return () => {
// we don't want to detach the node if it's still connected
if ("isConnected" in node && node.isConnected)
return;
ref.current = null;
onChange?.(null);
};
}
ref(node);
untrack(() => onChange?.(node));
return () => {
// we don't want to detach the node if it's still connected
if ("isConnected" in node && node.isConnected)
return;
ref(null);
onChange?.(null);
};
}
};
}
+8
View File
@@ -0,0 +1,8 @@
import type { EventCallback } from "./events.js";
import type { ReadableBox } from "../box/box-extras.svelte.js";
/**
* Composes event handlers into a single function that can be called with an event.
* If the previous handler cancels the event using `event.preventDefault()`, the handlers
* that follow will not be called.
*/
export declare function composeHandlers<E extends Event = Event, T extends Element = Element>(...handlers: Array<EventCallback<E> | ReadableBox<EventCallback<E>> | undefined>): (e: E) => void;
+21
View File
@@ -0,0 +1,21 @@
/**
* Composes event handlers into a single function that can be called with an event.
* If the previous handler cancels the event using `event.preventDefault()`, the handlers
* that follow will not be called.
*/
export function composeHandlers(...handlers) {
return function (e) {
for (const handler of handlers) {
if (!handler)
continue;
if (e.defaultPrevented)
return;
if (typeof handler === "function") {
handler.call(this, e);
}
else {
handler.current?.call(this, e);
}
}
};
}
+2
View File
@@ -0,0 +1,2 @@
import type { StyleProperties } from "../types.js";
export declare function cssToStyleObj(css: string | null | undefined): StyleProperties;
+23
View File
@@ -0,0 +1,23 @@
import parse from "style-to-object";
import { camelCase, pascalCase } from "./strings.js";
export function cssToStyleObj(css) {
if (!css)
return {};
const styleObj = {};
function iterator(name, value) {
if (name.startsWith("-moz-") ||
name.startsWith("-webkit-") ||
name.startsWith("-ms-") ||
name.startsWith("-o-")) {
styleObj[pascalCase(name)] = value;
return;
}
if (name.startsWith("--")) {
styleObj[name] = value;
return;
}
styleObj[camelCase(name)] = value;
}
parse(css, iterator);
return styleObj;
}
+17
View File
@@ -0,0 +1,17 @@
import type { Box } from "../types.js";
type ElementGetter = () => HTMLElement | null;
export declare class DOMContext {
readonly element: Box<HTMLElement | null>;
readonly root: Document | ShadowRoot;
constructor(element: Box<HTMLElement | null> | ElementGetter);
getDocument: () => Document;
getWindow: () => Window & typeof globalThis;
getActiveElement: () => HTMLElement | null;
isActiveElement: (node: HTMLElement | null) => boolean;
getElementById<T extends Element = HTMLElement>(id: string): T | null;
querySelector: (selector: string) => Element | null;
querySelectorAll: (selector: string) => NodeListOf<Element>;
setTimeout: (callback: () => void, delay: number) => number;
clearTimeout: (timeoutId: number) => void;
}
export {};
+50
View File
@@ -0,0 +1,50 @@
import { boxWith } from "../box/box-extras.svelte.js";
import { getActiveElement, getDocument } from "./dom.js";
export class DOMContext {
element;
root = $derived.by(() => {
if (!this.element.current)
return document;
const rootNode = this.element.current.getRootNode() ?? document;
return rootNode;
});
constructor(element) {
if (typeof element === "function") {
this.element = boxWith(element);
}
else {
this.element = element;
}
}
getDocument = () => {
return getDocument(this.root);
};
getWindow = () => {
return this.getDocument().defaultView ?? window;
};
getActiveElement = () => {
return getActiveElement(this.root);
};
isActiveElement = (node) => {
return node === this.getActiveElement();
};
getElementById(id) {
return this.root.getElementById(id);
}
querySelector = (selector) => {
if (!this.root)
return null;
return this.root.querySelector(selector);
};
querySelectorAll = (selector) => {
if (!this.root)
return [];
return this.root.querySelectorAll(selector);
};
setTimeout = (callback, delay) => {
return this.getWindow().setTimeout(callback, delay);
};
clearTimeout = (timeoutId) => {
return this.getWindow().clearTimeout(timeoutId);
};
}
+14
View File
@@ -0,0 +1,14 @@
export declare function isHTMLElement(node: unknown): node is HTMLElement;
export declare function isDocument(node: unknown): node is Document;
export declare function isWindow(node: unknown): node is Window;
export declare function getNodeName(node: Node | Window): string;
export declare function isNode(node: unknown): node is Node;
export declare function isShadowRoot(node: unknown): node is ShadowRoot;
type Target = HTMLElement | EventTarget | null | undefined;
export declare function contains(parent: Target, child: Target): boolean;
export declare function getDocument(node: Element | Window | Node | Document | null | undefined): Document;
export declare function getDocumentElement(node: Element | Node | Window | Document | null | undefined): HTMLElement;
export declare function getWindow(node: Node | ShadowRoot | Document | null | undefined): Window & typeof globalThis;
export declare function getActiveElement(rootNode: Document | ShadowRoot): HTMLElement | null;
export declare function getParentNode(node: Node): Node;
export {};
+86
View File
@@ -0,0 +1,86 @@
import { isObject } from "./is.js";
const ELEMENT_NODE = 1;
const DOCUMENT_NODE = 9;
const DOCUMENT_FRAGMENT_NODE = 11;
export function isHTMLElement(node) {
return isObject(node) && node.nodeType === ELEMENT_NODE && typeof node.nodeName === "string";
}
export function isDocument(node) {
return isObject(node) && node.nodeType === DOCUMENT_NODE;
}
export function isWindow(node) {
return isObject(node) && node.constructor?.name === "VisualViewport";
}
export function getNodeName(node) {
if (isHTMLElement(node))
return node.localName ?? "";
return "#document";
}
export function isNode(node) {
return isObject(node) && node.nodeType !== undefined;
}
export function isShadowRoot(node) {
return isNode(node) && node.nodeType === DOCUMENT_FRAGMENT_NODE && "host" in node;
}
export function contains(parent, child) {
if (!parent || !child)
return false;
if (!isHTMLElement(parent) || !isHTMLElement(child))
return false;
const rootNode = child.getRootNode?.();
if (parent === child)
return true;
if (parent.contains(child))
return true;
if (rootNode && isShadowRoot(rootNode)) {
let next = child;
while (next) {
if (parent === next)
return true;
// @ts-expect-error - host is not typed
next = next.parentNode || next.host;
}
}
return false;
}
export function getDocument(node) {
if (isDocument(node))
return node;
if (isWindow(node))
return node.document;
return node?.ownerDocument ?? document;
}
export function getDocumentElement(node) {
return getDocument(node).documentElement;
}
export function getWindow(node) {
if (isShadowRoot(node))
return getWindow(node.host);
if (isDocument(node))
return node.defaultView ?? window;
if (isHTMLElement(node))
return node.ownerDocument?.defaultView ?? window;
return window;
}
export function getActiveElement(rootNode) {
let activeElement = rootNode.activeElement;
while (activeElement?.shadowRoot) {
const el = activeElement.shadowRoot.activeElement;
if (el === activeElement)
break;
else
activeElement = el;
}
return activeElement;
}
export function getParentNode(node) {
if (getNodeName(node) === "html")
return node;
const result =
// @ts-expect-error - assignedSlot is not typed
node.assignedSlot ||
node.parentNode ||
(isShadowRoot(node) && node.host) ||
getDocumentElement(node);
return isShadowRoot(result) ? result.host : result;
}
+1
View File
@@ -0,0 +1 @@
export declare const EVENT_LIST_SET: Set<string>;
+106
View File
@@ -0,0 +1,106 @@
const EVENT_LIST = [
"onabort",
"onanimationcancel",
"onanimationend",
"onanimationiteration",
"onanimationstart",
"onauxclick",
"onbeforeinput",
"onbeforetoggle",
"onblur",
"oncancel",
"oncanplay",
"oncanplaythrough",
"onchange",
"onclick",
"onclose",
"oncompositionend",
"oncompositionstart",
"oncompositionupdate",
"oncontextlost",
"oncontextmenu",
"oncontextrestored",
"oncopy",
"oncuechange",
"oncut",
"ondblclick",
"ondrag",
"ondragend",
"ondragenter",
"ondragleave",
"ondragover",
"ondragstart",
"ondrop",
"ondurationchange",
"onemptied",
"onended",
"onerror",
"onfocus",
"onfocusin",
"onfocusout",
"onformdata",
"ongotpointercapture",
"oninput",
"oninvalid",
"onkeydown",
"onkeypress",
"onkeyup",
"onload",
"onloadeddata",
"onloadedmetadata",
"onloadstart",
"onlostpointercapture",
"onmousedown",
"onmouseenter",
"onmouseleave",
"onmousemove",
"onmouseout",
"onmouseover",
"onmouseup",
"onpaste",
"onpause",
"onplay",
"onplaying",
"onpointercancel",
"onpointerdown",
"onpointerenter",
"onpointerleave",
"onpointermove",
"onpointerout",
"onpointerover",
"onpointerup",
"onprogress",
"onratechange",
"onreset",
"onresize",
"onscroll",
"onscrollend",
"onsecuritypolicyviolation",
"onseeked",
"onseeking",
"onselect",
"onselectionchange",
"onselectstart",
"onslotchange",
"onstalled",
"onsubmit",
"onsuspend",
"ontimeupdate",
"ontoggle",
"ontouchcancel",
"ontouchend",
"ontouchmove",
"ontouchstart",
"ontransitioncancel",
"ontransitionend",
"ontransitionrun",
"ontransitionstart",
"onvolumechange",
"onwaiting",
"onwebkitanimationend",
"onwebkitanimationiteration",
"onwebkitanimationstart",
"onwebkittransitionend",
"onwheel"
];
export const EVENT_LIST_SET = new Set(EVENT_LIST);
+7
View File
@@ -0,0 +1,7 @@
type Arrayable<T> = T | T[];
export type EventCallback<E extends Event = Event> = (event: E) => void;
type GeneralEventListener<E = Event> = (evt: E) => unknown;
export declare function addEventListener<E extends keyof WindowEventMap>(target: Window, event: Arrayable<E>, handler: (this: Window, ev: WindowEventMap[E]) => unknown, options?: boolean | AddEventListenerOptions): VoidFunction;
export declare function addEventListener<E extends keyof DocumentEventMap>(target: Document, event: Arrayable<E>, handler: (this: Document, ev: DocumentEventMap[E]) => unknown, options?: boolean | AddEventListenerOptions): VoidFunction;
export declare function addEventListener<E extends keyof HTMLElementEventMap>(target: EventTarget, event: Arrayable<E>, handler: GeneralEventListener<HTMLElementEventMap[E]>, options?: boolean | AddEventListenerOptions): VoidFunction;
export {};
+17
View File
@@ -0,0 +1,17 @@
/**
* Adds an event listener to the specified target element(s) for the given event(s), and returns a function to remove it.
* @param target The target element(s) to add the event listener to.
* @param event The event(s) to listen for.
* @param handler The function to be called when the event is triggered.
* @param options An optional object that specifies characteristics about the event listener.
* @returns A function that removes the event listener from the target element(s).
*/
export function addEventListener(target, event, handler, options) {
const events = Array.isArray(event) ? event : [event];
// Add the event listener to each specified event for the target element(s).
events.forEach((_event) => target.addEventListener(_event, handler, options));
// Return a function that removes the event listener from the target element(s).
return () => {
events.forEach((_event) => target.removeEventListener(_event, handler, options));
};
}
+7
View File
@@ -0,0 +1,7 @@
/**
* Executes an array of callback functions with the same arguments.
* @template T The types of the arguments that the callback functions take.
* @param callbacks array of callback functions to execute.
* @returns A new function that executes all of the original callback functions with the same arguments.
*/
export declare function executeCallbacks<T extends unknown[]>(...callbacks: T): (...args: unknown[]) => void;
+15
View File
@@ -0,0 +1,15 @@
/**
* Executes an array of callback functions with the same arguments.
* @template T The types of the arguments that the callback functions take.
* @param callbacks array of callback functions to execute.
* @returns A new function that executes all of the original callback functions with the same arguments.
*/
export function executeCallbacks(...callbacks) {
return (...args) => {
for (const callback of callbacks) {
if (typeof callback === "function") {
callback(...args);
}
}
};
}
+4
View File
@@ -0,0 +1,4 @@
import type { ClassValue } from "clsx";
export declare function isFunction(value: unknown): value is (...args: unknown[]) => unknown;
export declare function isObject(value: unknown): value is Record<PropertyKey, unknown>;
export declare function isClassValue(value: unknown): value is ClassValue;
+25
View File
@@ -0,0 +1,25 @@
export function isFunction(value) {
return typeof value === "function";
}
export function isObject(value) {
return value !== null && typeof value === "object";
}
const CLASS_VALUE_PRIMITIVE_TYPES = ["string", "number", "bigint", "boolean"];
export function isClassValue(value) {
// handle primitive types
if (value === null || value === undefined)
return true;
if (CLASS_VALUE_PRIMITIVE_TYPES.includes(typeof value))
return true;
// handle arrays (ClassArray)
if (Array.isArray(value))
return value.every((item) => isClassValue(item));
// handle objects (ClassDictionary)
if (typeof value === "object") {
// ensure it's a plain object and not some other object type
if (Object.getPrototypeOf(value) !== Object.prototype)
return false;
return true;
}
return false;
}
+22
View File
@@ -0,0 +1,22 @@
type Props = Record<string, unknown>;
type PropsArg = Props | null | undefined;
type TupleTypes<T> = {
[P in keyof T]: T[P];
} extends {
[key: number]: infer V;
} ? NullToObject<V> : never;
type NullToObject<T> = T extends null | undefined ? {} : T;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
/**
* Given a list of prop objects, merges them into a single object.
* - Automatically composes event handlers (e.g. `onclick`, `oninput`, etc.)
* - Chains regular functions with the same name so they are called in order
* - Merges class strings with `clsx`
* - Merges style objects and converts them to strings
* - Handles a bug with Svelte where setting the `hidden` attribute to `false` doesn't remove it
* - Overrides other values with the last one
*/
export declare function mergeProps<T extends PropsArg[]>(...args: T): UnionToIntersection<TupleTypes<T>> & {
style?: string;
};
export {};
+126
View File
@@ -0,0 +1,126 @@
/**
* Modified from https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/utils/src/mergeProps.ts (see NOTICE.txt for source)
*/
import { clsx } from "clsx";
import { composeHandlers } from "./compose-handlers.js";
import { cssToStyleObj } from "./css-to-style-obj.js";
import { isClassValue } from "./is.js";
import { executeCallbacks } from "./execute-callbacks.js";
import { styleToString } from "./style.js";
import { EVENT_LIST_SET } from "./event-list.js";
function isEventHandler(key) {
return EVENT_LIST_SET.has(key);
}
/**
* Given a list of prop objects, merges them into a single object.
* - Automatically composes event handlers (e.g. `onclick`, `oninput`, etc.)
* - Chains regular functions with the same name so they are called in order
* - Merges class strings with `clsx`
* - Merges style objects and converts them to strings
* - Handles a bug with Svelte where setting the `hidden` attribute to `false` doesn't remove it
* - Overrides other values with the last one
*/
export function mergeProps(...args) {
const result = { ...args[0] };
for (let i = 1; i < args.length; i++) {
const props = args[i];
if (!props)
continue;
// Handle string keys
for (const key of Object.keys(props)) {
const a = result[key];
const b = props[key];
const aIsFunction = typeof a === "function";
const bIsFunction = typeof b === "function";
// compose event handlers
if (aIsFunction && typeof bIsFunction && isEventHandler(key)) {
// handle merging of event handlers
const aHandler = a;
const bHandler = b;
result[key] = composeHandlers(aHandler, bHandler);
}
else if (aIsFunction && bIsFunction) {
// chain non-event handler functions
result[key] = executeCallbacks(a, b);
}
else if (key === "class") {
// handle merging acceptable class values from clsx
const aIsClassValue = isClassValue(a);
const bIsClassValue = isClassValue(b);
if (aIsClassValue && bIsClassValue) {
result[key] = clsx(a, b);
}
else if (aIsClassValue) {
result[key] = clsx(a);
}
else if (bIsClassValue) {
result[key] = clsx(b);
}
}
else if (key === "style") {
const aIsObject = typeof a === "object";
const bIsObject = typeof b === "object";
const aIsString = typeof a === "string";
const bIsString = typeof b === "string";
if (aIsObject && bIsObject) {
// both are style objects, merge them
result[key] = { ...a, ...b };
}
else if (aIsObject && bIsString) {
// a is style object, b is string, convert b to style object and merge
const parsedStyle = cssToStyleObj(b);
result[key] = { ...a, ...parsedStyle };
}
else if (aIsString && bIsObject) {
// a is string, b is style object, convert a to style object and merge
const parsedStyle = cssToStyleObj(a);
result[key] = { ...parsedStyle, ...b };
}
else if (aIsString && bIsString) {
// both are strings, convert both to objects and merge
const parsedStyleA = cssToStyleObj(a);
const parsedStyleB = cssToStyleObj(b);
result[key] = { ...parsedStyleA, ...parsedStyleB };
}
else if (aIsObject) {
result[key] = a;
}
else if (bIsObject) {
result[key] = b;
}
else if (aIsString) {
result[key] = a;
}
else if (bIsString) {
result[key] = b;
}
}
else {
// override other values
result[key] = b !== undefined ? b : a;
}
}
// handle symbol keys (mostly for `Attachments`)
for (const key of Object.getOwnPropertySymbols(props)) {
const a = result[key];
const b = props[key];
// for matching symbols, we just override
result[key] = b !== undefined ? b : a;
}
}
// convert style object to string
if (typeof result.style === "object") {
result.style = styleToString(result.style).replaceAll("\n", " ");
}
// handle weird svelte bug where `hidden` is not removed when set to `false`
if (result.hidden === false) {
result.hidden = undefined;
delete result.hidden;
}
// handle weird svelte bug where `disabled` is not removed when set to `false`
if (result.disabled === false) {
result.disabled = undefined;
delete result.disabled;
}
return result;
}
@@ -0,0 +1 @@
export declare function onDestroyEffect(fn: () => void): void;
+7
View File
@@ -0,0 +1,7 @@
export function onDestroyEffect(fn) {
$effect(() => {
return () => {
fn();
};
});
}
+1
View File
@@ -0,0 +1 @@
export declare function onMountEffect(fn: () => void): void;
+7
View File
@@ -0,0 +1,7 @@
import { untrack } from "svelte";
export function onMountEffect(fn) {
$effect(() => {
const cleanup = untrack(() => fn());
return cleanup;
});
}
+3
View File
@@ -0,0 +1,3 @@
import type { StyleProperties } from "../types.js";
export declare const srOnlyStyles: StyleProperties;
export declare const srOnlyStylesString: string;
+14
View File
@@ -0,0 +1,14 @@
import { styleToString } from "./style.js";
export const srOnlyStyles = {
position: "absolute",
width: "1px",
height: "1px",
padding: "0",
margin: "-1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
whiteSpace: "nowrap",
borderWidth: "0",
transform: "translateX(-100%)"
};
export const srOnlyStylesString = styleToString(srOnlyStyles);
+3
View File
@@ -0,0 +1,3 @@
export declare function pascalCase(str?: string): string;
export declare function camelCase(str?: string): string;
export declare function kebabCase(str?: string): string;
+70
View File
@@ -0,0 +1,70 @@
const NUMBER_CHAR_RE = /\d/;
const STR_SPLITTERS = ["-", "_", "/", "."];
function isUppercase(char = "") {
if (NUMBER_CHAR_RE.test(char))
return undefined;
return char !== char.toLowerCase();
}
function splitByCase(str) {
const parts = [];
let buff = "";
let previousUpper;
let previousSplitter;
for (const char of str) {
// Splitter
const isSplitter = STR_SPLITTERS.includes(char);
if (isSplitter === true) {
parts.push(buff);
buff = "";
previousUpper = undefined;
continue;
}
const isUpper = isUppercase(char);
if (previousSplitter === false) {
// Case rising edge
if (previousUpper === false && isUpper === true) {
parts.push(buff);
buff = char;
previousUpper = isUpper;
continue;
}
// Case falling edge
if (previousUpper === true && isUpper === false && buff.length > 1) {
const lastChar = buff.at(-1);
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
buff = lastChar + char;
previousUpper = isUpper;
continue;
}
}
// Normal char
buff += char;
previousUpper = isUpper;
previousSplitter = isSplitter;
}
parts.push(buff);
return parts;
}
export function pascalCase(str) {
if (!str)
return "";
return splitByCase(str)
.map((p) => upperFirst(p))
.join("");
}
export function camelCase(str) {
return lowerFirst(pascalCase(str || ""));
}
export function kebabCase(str) {
return str
? splitByCase(str)
.map((p) => p.toLowerCase())
.join("-")
: "";
}
function upperFirst(str) {
return str ? str[0].toUpperCase() + str.slice(1) : "";
}
function lowerFirst(str) {
return str ? str[0].toLowerCase() + str.slice(1) : "";
}
+1
View File
@@ -0,0 +1 @@
export declare function styleToCSS(styleObj: object): string;
+23
View File
@@ -0,0 +1,23 @@
function createParser(matcher, replacer) {
const regex = RegExp(matcher, "g");
return (str) => {
// throw an error if not a string
if (typeof str !== "string") {
throw new TypeError(`expected an argument of type string, but got ${typeof str}`);
}
// if no match between string and matcher
if (!str.match(regex))
return str;
// executes the replacer function for each match
return str.replace(regex, replacer);
};
}
const camelToKebab = createParser(/[A-Z]/, (match) => `-${match.toLowerCase()}`);
export function styleToCSS(styleObj) {
if (!styleObj || typeof styleObj !== "object" || Array.isArray(styleObj)) {
throw new TypeError(`expected an argument of type object, but got ${typeof styleObj}`);
}
return Object.keys(styleObj)
.map((property) => `${camelToKebab(property)}: ${styleObj[property]};`)
.join("\n");
}
+2
View File
@@ -0,0 +1,2 @@
import type { StyleProperties } from "../types.js";
export declare function styleToString(style?: StyleProperties): string;
+4
View File
@@ -0,0 +1,4 @@
import { styleToCSS } from "./style-to-css.js";
export function styleToString(style = {}) {
return styleToCSS(style).replace("\n", " ");
}
+12
View File
@@ -0,0 +1,12 @@
import type { Getter } from "../types.js";
/**
* Simple helper function to sync a read-only dependency with writable state. This only syncs
* in one direction, from the dependency to the state. If you need to sync both directions, you
* should use the `box.with(() => dep, (v) => (dep = v))` pattern.
*
* @param getDep - A getter that returns the value that may change and whose new value will be
* passed to the `onChange` function passed as the second argument.
* @param onChange - A function that accepts a new value to react to or keep other state in sync
* with the dependency.
*/
export declare function useOnChange<T>(getDep: Getter<T>, onChange: (value: T) => void): void;
+14
View File
@@ -0,0 +1,14 @@
import { watch } from "runed";
/**
* Simple helper function to sync a read-only dependency with writable state. This only syncs
* in one direction, from the dependency to the state. If you need to sync both directions, you
* should use the `box.with(() => dep, (v) => (dep = v))` pattern.
*
* @param getDep - A getter that returns the value that may change and whose new value will be
* passed to the `onChange` function passed as the second argument.
* @param onChange - A function that accepts a new value to react to or keep other state in sync
* with the dependency.
*/
export function useOnChange(getDep, onChange) {
watch(getDep, onChange);
}
+32
View File
@@ -0,0 +1,32 @@
import type { WritableBox } from "../box/box-extras.svelte.js";
import type { Box, Getter } from "../types.js";
type UseRefByIdProps = {
/**
* The ID of the node to find.
*/
id: Box<string>;
/**
* The ref to set the node to.
*/
ref: WritableBox<HTMLElement | null>;
/**
* A reactive condition that will cause the node to be set.
*/
deps?: Getter<unknown>;
/**
* A callback fired when the ref changes.
*/
onRefChange?: (node: HTMLElement | null) => void;
/**
* A function that returns the root node to search for the element by ID.
* Defaults to `() => document`.
*/
getRootNode?: Getter<Document | ShadowRoot | undefined>;
};
/**
* Finds the node with that ID and sets it to the boxed node.
* Reactive using `$effect` to ensure when the ID or deps change,
* an update is triggered and new node is found.
*/
export declare function useRefById({ id, ref, deps, onRefChange, getRootNode }: UseRefByIdProps): void;
export {};
+22
View File
@@ -0,0 +1,22 @@
import { watch } from "runed";
import { onDestroyEffect } from "./on-destroy-effect.svelte.js";
/**
* Finds the node with that ID and sets it to the boxed node.
* Reactive using `$effect` to ensure when the ID or deps change,
* an update is triggered and new node is found.
*/
export function useRefById({ id, ref, deps = () => true, onRefChange, getRootNode }) {
watch([() => id.current, deps], ([_id]) => {
const rootNode = getRootNode?.() ?? document;
const node = rootNode?.getElementById(_id);
if (node)
ref.current = node;
else
ref.current = null;
onRefChange?.(ref.current);
});
onDestroyEffect(() => {
ref.current = null;
onRefChange?.(null);
});
}