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
+109
View File
@@ -0,0 +1,109 @@
import type { Getter, MaybeBoxOrGetter } from "../types.js";
export declare const BoxSymbol: unique symbol;
export declare const isWritableSymbol: unique symbol;
export type ReadableBox<T> = {
readonly [BoxSymbol]: true;
readonly current: T;
};
export type WritableBox<T> = ReadableBox<T> & {
readonly [isWritableSymbol]: true;
current: T;
};
/**
* Creates a readonly box
*
* @param getter Function to get the value of the box
* @returns A box with a `current` property whose value is the result of the getter.
* Useful to pass state to other functions.
*
* @see {@link https://runed.dev/docs/functions/box}
*/
declare function boxWith<T>(getter: () => T): ReadableBox<T>;
/**
* Creates a writable box
*
* @param getter Function to get the value of the box
* @param setter Function to set the value of the box
* @returns A box with a `current` property which can be set to a new value.
* Useful to pass state to other functions.
*
* @see {@link https://runed.dev/docs/functions/box}
*/
declare function boxWith<T>(getter: () => T, setter: (v: T) => void): WritableBox<T>;
/**
* @returns Whether the value is a Box
*
* @see {@link https://runed.dev/docs/functions/box}
*/
declare function isBox(value: unknown): value is ReadableBox<unknown>;
/**
* @returns Whether the value is a WritableBox
*
* @see {@link https://runed.dev/docs/functions/box}
*/
declare function isWritableBox(value: unknown): value is WritableBox<unknown>;
/**
* Creates a box from either a static value, a box, or a getter function.
* Useful when you want to receive any of these types of values and generate a boxed version of it.
*
* @returns A box with a `current` property whose value.
*
* @see {@link https://runed.dev/docs/functions/box}
*/
declare function boxFrom<T>(value: T | WritableBox<T>): WritableBox<T>;
declare function boxFrom<T>(value: ReadableBox<T>): ReadableBox<T>;
declare function boxFrom<T>(value: Getter<T>): ReadableBox<T>;
declare function boxFrom<T>(value: MaybeBoxOrGetter<T>): ReadableBox<T>;
declare function boxFrom<T>(value: T): WritableBox<T>;
type GetKeys<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
type RemoveValues<T, U> = Omit<T, GetKeys<T, U>>;
type BoxFlatten<R extends Record<string, unknown>> = Expand<RemoveValues<{
[K in keyof R]: R[K] extends WritableBox<infer T> ? T : never;
}, never> & RemoveValues<{
readonly [K in keyof R]: R[K] extends WritableBox<infer _> ? never : R[K] extends ReadableBox<infer T> ? T : never;
}, never>> & RemoveValues<{
[K in keyof R]: R[K] extends ReadableBox<infer _> ? never : R[K];
}, never>;
/**
* Function that gets an object of boxes, and returns an object of reactive values
*
* @example
* const count = box(0)
* const flat = box.flatten({ count, double: box.with(() => count.current) })
* // type of flat is { count: number, readonly double: number }
*
* @see {@link https://runed.dev/docs/functions/box}
*/
declare function boxFlatten<R extends Record<string, unknown>>(boxes: R): BoxFlatten<R>;
/**
* Function that converts a box to a readonly box.
*
* @example
* const count = box(0) // WritableBox<number>
* const countReadonly = box.readonly(count) // ReadableBox<number>
*
* @see {@link https://runed.dev/docs/functions/box}
*/
declare function toReadonlyBox<T>(b: ReadableBox<T>): ReadableBox<T>;
/**
* Creates a writable box.
*
* @returns A box with a `current` property which can be set to a new value.
* Useful to pass state to other functions.
*
* @see {@link https://runed.dev/docs/functions/box}
*/
declare function simpleBox<T>(): WritableBox<T | undefined>;
/**
* Creates a writable box with an initial value.
*
* @param initialValue The initial value of the box.
* @returns A box with a `current` property which can be set to a new value.
* Useful to pass state to other functions.
*
* @see {@link https://runed.dev/docs/functions/box}
*/
declare function simpleBox<T>(initialValue: T): WritableBox<T>;
export { boxWith, isBox, isWritableBox, boxFrom, boxFlatten, toReadonlyBox, simpleBox };
+116
View File
@@ -0,0 +1,116 @@
import { isFunction, isObject } from "../utils/is.js";
export const BoxSymbol = Symbol("box");
export const isWritableSymbol = Symbol("is-writable");
function boxWith(getter, setter) {
const derived = $derived.by(getter);
if (setter) {
return {
[BoxSymbol]: true,
[isWritableSymbol]: true,
get current() {
return derived;
},
set current(v) {
setter(v);
}
};
}
return {
[BoxSymbol]: true,
get current() {
return getter();
}
};
}
/**
* @returns Whether the value is a Box
*
* @see {@link https://runed.dev/docs/functions/box}
*/
function isBox(value) {
return isObject(value) && BoxSymbol in value;
}
/**
* @returns Whether the value is a WritableBox
*
* @see {@link https://runed.dev/docs/functions/box}
*/
function isWritableBox(value) {
return isBox(value) && isWritableSymbol in value;
}
function boxFrom(value) {
if (isBox(value))
return value;
if (isFunction(value))
return boxWith(value);
return simpleBox(value);
}
/**
* Function that gets an object of boxes, and returns an object of reactive values
*
* @example
* const count = box(0)
* const flat = box.flatten({ count, double: box.with(() => count.current) })
* // type of flat is { count: number, readonly double: number }
*
* @see {@link https://runed.dev/docs/functions/box}
*/
function boxFlatten(boxes) {
return Object.entries(boxes).reduce((acc, [key, b]) => {
if (!isBox(b)) {
return Object.assign(acc, { [key]: b });
}
if (isWritableBox(b)) {
Object.defineProperty(acc, key, {
get() {
return b.current;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set(v) {
b.current = v;
}
});
}
else {
Object.defineProperty(acc, key, {
get() {
return b.current;
}
});
}
return acc;
}, {});
}
/**
* Function that converts a box to a readonly box.
*
* @example
* const count = box(0) // WritableBox<number>
* const countReadonly = box.readonly(count) // ReadableBox<number>
*
* @see {@link https://runed.dev/docs/functions/box}
*/
function toReadonlyBox(b) {
if (!isWritableBox(b))
return b;
return {
[BoxSymbol]: true,
get current() {
return b.current;
}
};
}
function simpleBox(initialValue) {
let current = $state(initialValue);
return {
[BoxSymbol]: true,
[isWritableSymbol]: true,
get current() {
return current;
},
set current(v) {
current = v;
}
};
}
export { boxWith, isBox, isWritableBox, boxFrom, boxFlatten, toReadonlyBox, simpleBox };
+29
View File
@@ -0,0 +1,29 @@
import { boxFrom, boxWith, boxFlatten, toReadonlyBox, type WritableBox } from "./box-extras.svelte.js";
/**
* Creates a writable box.
*
* @returns A box with a `current` property which can be set to a new value.
* Useful to pass state to other functions.
*
* @see {@link https://runed.dev/docs/functions/box}
*/
export declare function box<T>(): WritableBox<T | undefined>;
/**
* Creates a writable box with an initial value.
*
* @param initialValue The initial value of the box.
* @returns A box with a `current` property which can be set to a new value.
* Useful to pass state to other functions.
*
* @see {@link https://runed.dev/docs/functions/box}
*/
export declare function box<T>(initialValue: T): WritableBox<T>;
export declare namespace box {
export var from: typeof boxFrom;
var _a: typeof boxWith;
export var flatten: typeof boxFlatten;
export var readonly: typeof toReadonlyBox;
export var isBox: typeof import("./box-extras.svelte.js").isBox;
export var isWritableBox: typeof import("./box-extras.svelte.js").isWritableBox;
export { _a as with };
}
+20
View File
@@ -0,0 +1,20 @@
import { boxFrom, boxWith, boxFlatten, toReadonlyBox, isBox, isWritableBox, BoxSymbol, isWritableSymbol } from "./box-extras.svelte.js";
export function box(initialValue) {
let current = $state(initialValue);
return {
[BoxSymbol]: true,
[isWritableSymbol]: true,
get current() {
return current;
},
set current(v) {
current = v;
}
};
}
box.from = boxFrom;
box.with = boxWith;
box.flatten = boxFlatten;
box.readonly = toReadonlyBox;
box.isBox = isBox;
box.isWritableBox = isWritableBox;
+10
View File
@@ -0,0 +1,10 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type * as CSS from "csstype";
declare module "csstype" {
interface Properties {
// Allow any CSS Custom Properties
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[index: `--${string}`]: any;
}
}
+22
View File
@@ -0,0 +1,22 @@
export { box } from "./box/box.svelte.js";
export { boxWith, isBox, isWritableBox, boxFrom, boxFlatten, toReadonlyBox, simpleBox, type WritableBox, type ReadableBox } from "./box/box-extras.svelte.js";
export { unbox } from "./unbox/unbox.svelte.js";
export * from "./types.js";
export { composeHandlers } from "./utils/compose-handlers.js";
export { cssToStyleObj } from "./utils/css-to-style-obj.js";
export { executeCallbacks } from "./utils/execute-callbacks.js";
export { addEventListener, type EventCallback } from "./utils/events.js";
export { mergeProps } from "./utils/merge-props.js";
export { styleToString } from "./utils/style.js";
export { srOnlyStyles, srOnlyStylesString } from "./utils/sr-only-styles.js";
export { useRefById } from "./utils/use-ref-by-id.svelte.js";
export { pascalCase, camelCase, kebabCase } from "./utils/strings.js";
export { onDestroyEffect } from "./utils/on-destroy-effect.svelte.js";
export { onMountEffect } from "./utils/on-mount-effect.svelte.js";
export { afterSleep } from "./utils/after-sleep.js";
export { afterTick } from "./utils/after-tick.js";
export { useOnChange } from "./utils/use-on-change.svelte.js";
export { styleToCSS } from "./utils/style-to-css.js";
export { isHTMLElement, isDocument, isWindow, getNodeName, isNode, isShadowRoot, contains, getDocument, getDocumentElement, getWindow, getActiveElement, getParentNode } from "./utils/dom.js";
export { DOMContext } from "./utils/dom-context.svelte.js";
export { attachRef } from "./utils/attach-ref.js";
+22
View File
@@ -0,0 +1,22 @@
export { box } from "./box/box.svelte.js";
export { boxWith, isBox, isWritableBox, boxFrom, boxFlatten, toReadonlyBox, simpleBox } from "./box/box-extras.svelte.js";
export { unbox } from "./unbox/unbox.svelte.js";
export * from "./types.js";
export { composeHandlers } from "./utils/compose-handlers.js";
export { cssToStyleObj } from "./utils/css-to-style-obj.js";
export { executeCallbacks } from "./utils/execute-callbacks.js";
export { addEventListener } from "./utils/events.js";
export { mergeProps } from "./utils/merge-props.js";
export { styleToString } from "./utils/style.js";
export { srOnlyStyles, srOnlyStylesString } from "./utils/sr-only-styles.js";
export { useRefById } from "./utils/use-ref-by-id.svelte.js";
export { pascalCase, camelCase, kebabCase } from "./utils/strings.js";
export { onDestroyEffect } from "./utils/on-destroy-effect.svelte.js";
export { onMountEffect } from "./utils/on-mount-effect.svelte.js";
export { afterSleep } from "./utils/after-sleep.js";
export { afterTick } from "./utils/after-tick.js";
export { useOnChange } from "./utils/use-on-change.svelte.js";
export { styleToCSS } from "./utils/style-to-css.js";
export { isHTMLElement, isDocument, isWindow, getNodeName, isNode, isShadowRoot, contains, getDocument, getDocumentElement, getWindow, getActiveElement, getParentNode } from "./utils/dom.js";
export { DOMContext } from "./utils/dom-context.svelte.js";
export { attachRef } from "./utils/attach-ref.js";
+89
View File
@@ -0,0 +1,89 @@
import type { Snippet } from "svelte";
import type * as CSS from "csstype";
import type { ReadableBox, WritableBox } from "./box/box-extras.svelte.js";
export type FunctionArgs<Args extends any[] = any[], Return = void> = (...args: Args) => Return;
export type Getter<T> = () => T;
export type MaybeGetter<T> = T | Getter<T>;
export type MaybeBoxOrGetter<T> = T | Getter<T> | ReadableBox<T>;
export type BoxOrGetter<T> = Getter<T> | ReadableBox<T>;
export type Box<T> = ReadableBox<T> | WritableBox<T>;
export type Expand<T> = T extends infer U ? {
[K in keyof U]: U[K];
} : never;
/**
* Given a Record type T, returns a type that represents the same type, but with
* all values wrapped in a `ReadableBox`.
*/
export type ReadableBoxedValues<T> = {
[K in keyof T]: ReadableBox<T[K]>;
};
/**
* Given a Record type T, returns a type that represents the same type, but with
* all values wrapped in a `WritableBox`.
*/
export type WritableBoxedValues<T> = {
[K in keyof T]: WritableBox<T[K]>;
};
export type WithChild<
/**
* The props that the component accepts.
*/
Props extends Record<PropertyKey, unknown> = {},
/**
* The props that are passed to the `child` and `children` snippets. The `ElementProps` are
* merged with these props for the `child` snippet.
*/
SnippetProps extends Record<PropertyKey, unknown> = {
_default: never;
},
/**
* The underlying DOM element being rendered. You can bind to this prop to
* programatically interact with the element.
*/
Ref = HTMLElement> = Omit<Props, "child" | "children"> & {
child?: SnippetProps extends {
_default: never;
} ? Snippet<[{
props: Record<string, unknown>;
}]> : Snippet<[SnippetProps & {
props: Record<string, unknown>;
}]>;
children?: SnippetProps extends {
_default: never;
} ? Snippet : Snippet<[SnippetProps]>;
style?: string | null | undefined;
ref?: Ref | null | undefined;
};
export type WithChildren<Props = {}> = Props & {
children?: Snippet | undefined;
};
/**
* Constructs a new type by omitting properties from type
* 'T' that exist in type 'U'.
*
* @template T - The base object type from which properties will be omitted.
* @template U - The object type whose properties will be omitted from 'T'.
* @example
* type Result = Without<{ a: number; b: string; }, { b: string; }>;
* // Result type will be { a: number; }
*/
export type Without<T extends object, U extends object> = Omit<T, keyof U>;
export type WithRefProps<T = {}> = T & ReadableBoxedValues<{
id: string;
}> & WritableBoxedValues<{
ref: HTMLElement | null;
}>;
export type WithoutChild<T> = T extends {
child?: any;
} ? Omit<T, "child"> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithoutChildren<T> = T extends {
children?: any;
} ? Omit<T, "children"> : T;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
ref?: U | null;
};
export type StyleProperties = CSS.Properties & {
[str: `--${string}`]: any;
};
export type AnyFn = (...args: any[]) => any;
+1
View File
@@ -0,0 +1 @@
export {};
+2
View File
@@ -0,0 +1,2 @@
import type { MaybeBoxOrGetter } from "../types.js";
export declare function unbox<T>(value: MaybeBoxOrGetter<T>): T;
+11
View File
@@ -0,0 +1,11 @@
import { isBox } from "../box/box-extras.svelte.js";
import { isFunction } from "../utils/is.js";
export function unbox(value) {
if (isBox(value))
return value.current;
if (isFunction(value)) {
const getter = value;
return getter();
}
return value;
}
+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);
});
}
+14
View File
@@ -0,0 +1,14 @@
import type { Box } from "../types.js";
type WatcherCallback<T> = (curr: T, prev: T) => void | Promise<void> | (() => void) | (() => Promise<void>);
type WatchOptions = {
/**
* Whether to eagerly run the watcher before the state is updated.
*/
immediate?: boolean;
/**
* Whether to run the watcher only once.
*/
once?: boolean;
};
export declare function watch<T>(box: Box<T>, callback: WatcherCallback<T>, options?: WatchOptions): () => void;
export {};
+26
View File
@@ -0,0 +1,26 @@
import { untrack } from "svelte";
export function watch(box, callback, options = {}) {
let prev = $state(box.current);
let ranOnce = false;
const watchEffect = $effect.root(() => {
$effect.pre(() => {
if (prev === box.current || !options.immediate)
return;
if (options.once && ranOnce)
return;
callback(box.current, untrack(() => prev));
untrack(() => (prev = box.current));
ranOnce = true;
});
$effect(() => {
if (prev === box.current || options.immediate)
return;
if (options.once && ranOnce)
return;
callback(box.current, untrack(() => prev));
untrack(() => (prev = box.current));
ranOnce = true;
});
});
return watchEffect;
}