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
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2024 Hunter Johnston <https://github.com/huntabyte>
Copyright (c) 2024 Thomas G. Lopes <https://github.com/tglide>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+449
View File
@@ -0,0 +1,449 @@
# Svelte Toolbelt
Utilities for Svelte 5 that I find useful and will use in the various projects I work on. It's maintained by me, for me.
For more robust and feature-rich utilities, I recommend checking out/using [runed](https://runed.dev).
## Installation
```bash
npm install svelte-toolbelt
```
## Box
### `box`
Initializes a writable boxed state.
```svelte
<script lang="ts">
import { box } from "runed";
const count = box(0);
</script>
<button onclick={() => count.current++}>
clicks: {count.current}
</button>
```
### `box.with`
Creates reactive state using getter and setter functions. If a setter function is provided, the box
is writable. If not, the box is readonly.
Useful for passing synced reactive values across boundaries.
```svelte
<script lang="ts">
import { type WritableBox, box } from "runed";
function useCounter(count: WritableBox<number>) {
return {
increment() {
count.current++;
},
// We pass a box that doubles the count value
double: box.with(() => count.current * 2)
};
}
let count = $state(0);
// We pass count to box.with so it stays in sync
const { double, increment } = useCounter(
box.with(
() => count.current,
(v) => (count = v)
)
);
</script>
<button onclick={increment}>
clicks: {count}
double: {double.current}
</button>
```
### `box.from`
Creates a box from an existing box, a getter function, or a static value.
Useful for receiving arguments that may or may not be reactive.
```svelte
<script lang="ts">
import { box } from "runed";
function useCounter(_count: WritableBox<number> | number) {
const count = box.from(_count);
return {
count,
increment() {
count.current++;
},
// We pass a box that doubles the count value
double: box.with(() => count.current * 2)
};
}
const counter1 = useCounter(1);
console.log(counter1.count.current); // 1
console.log(counter1.double.current); // 2
const counter2 = useCounter(box(2));
console.log(counter2.count.current); // 2
console.log(counter2.double.current); // 4
function useDouble(_count: number | (() => number) | ReadableBox<number>) {
const count = box.from(_count);
return box.with(() => count.current * 2);
}
const double1 = useDouble(1);
console.log(double1.current); // 2
const double2 = useDouble(box(2));
console.log(double2.current); // 4
const double3 = useDouble(() => counter1.count.current);
console.log(double3.current); // 2
</script>
```
### `box.flatten`
Transforms any boxes within an object to reactive properties, removing the need to access each
property with `.current`.
```ts
const count = box(1);
const flat = box.flatten({
count,
double: box.with(() => count.current * 2),
increment() {
count.current++;
}
});
console.log(flat.count); // 1
console.log(flat.double); // 2
flat.increment();
console.log(flat.count); // 2
```
### `box.readonly`
Creates a readonly box from a writable box that remains in sync with the original box.
```ts
const count = box(1);
const readonlyCount = box.readonly(count);
console.log(readonlyCount.current); // 1
count.current++;
console.log(readonlyCount.current); // 2
readonlyCount.current = 3; // Error: Cannot assign to read only property 'value' of object
```
### `box.isBox`
Checks if a value is a `Box`.
```ts
const count = box(1);
console.log(box.isBox(count)); // true
console.log(box.isBox(1)); // false
```
### `box.isWritableBox`
Checks if a value is a `WritableBox`.
```ts
const count = box(1);
const double = box.with(() => count.current * 2);
console.log(box.isWritableBox(count)); // true
console.log(box.isWritableBox(double)); // false
```
### `unbox`
Unboxes the value from a box.
```ts
const count = box(1);
const double = box.with(() => count.current * 2);
console.log(unbox(double)); // 2
```
## Utils
### `afterSleep`
Executes a callback after a specified number of milliseconds.
```ts
afterSleep(1000, () => console.log("Hello, world!"));
```
### `afterTick`
Executes a callback after the next tick.
```ts
afterTick(() => console.log("Hello, world!"));
```
### `composeHandlers`
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.
```ts
import { composeHandlers } from "svelte-toolbelt";
const handler1 = () => console.log("Handler 1");
const handler2 = () => console.log("Handler 2");
const composedHandler = composeHandlers(handler1, handler2);
const event = new MouseEvent("click", { cancelable: true });
console.log(composedHandler(event)); // Handler 1, Handler 2
```
### `cssToStyleObj`
Converts a CSS string to a style object.
```ts
const css = "color: red; font-size: 16px;";
const styleObj = cssToStyleObj(css);
console.log(styleObj); // { color: "red", fontSize: "16px" }
```
### `executeCallbacks`
Executes an array of callback functions with the same arguments.
```ts
const callback1 = () => console.log("Callback 1");
const callback2 = () => console.log("Callback 2");
console.log(executeCallbacks(callback1, callback2)); // Callback 1, Callback 2
```
### `addEventListener`
Adds an event listener to the specified target element(s) for the given event(s), and returns a function to remove it.
```ts
import { addEventListener } from "svelte-toolbelt";
const target = document.getElementById("my-element");
const event = "click";
const handler = () => console.log("Clicked!");
const removeListener = addEventListener(target, event, handler);
// Later, remove the listener
removeListener();
```
### `mergeProps`
Merges props into a single object.
```ts
import { mergeProps } from "svelte-toolbelt";
const props1 = { a: 1 };
const props2 = { b: 2 };
const result = mergeProps(props1, props2);
console.log(result); // { a: 1, b: 2 }
```
#### Event Handlers
Event handlers are chained in the order they're passed. If a handler calls `event.preventDefault()`, subsequent handlers in the chain are not executed.
```ts
const props1 = { onclick: (e: MouseEvent) => console.log("First click") };
const props2 = { onclick: (e: MouseEvent) => console.log("Second click") };
const mergedProps = mergeProps(props1, props2);
mergedProps.onclick(new MouseEvent("click")); // Logs: "First click" then "Second click"
```
If `preventDefault()` is called:
```ts
const props1 = { onclick: (e: MouseEvent) => console.log("First click") };
const props2 = {
onclick: (e: MouseEvent) => {
console.log("Second click");
e.preventDefault();
}
};
const props3 = { onclick: (e: MouseEvent) => console.log("Third click") };
const mergedProps = mergeProps(props1, props2, props3);
mergedProps.onclick(new MouseEvent("click")); // Logs: "First click" then "Second click" only
```
Since `props2` called `event.preventDefault()`, `props3`'s `onclick` handler will not be called.
#### Non-Event Handler Functions
Non-event handler functions are also chained, but without the ability to prevent subsequent functions from executing:
```ts
const props1 = { doSomething: () => console.log("Action 1") };
const props2 = { doSomething: () => console.log("Action 2") };
const mergedProps = mergeProps(props1, props2);
mergedProps.doSomething(); // Logs: "Action 1" then "Action 2"
```
#### Classes
Class names are merged using [`clsx`](https://www.npmjs.com/package/clsx):
```ts
const props1 = { class: "text-lg font-bold" };
const props2 = { class: ["bg-blue-500", "hover:bg-blue-600"] };
const mergedProps = mergeProps(props1, props2);
console.log(mergedProps.class); // "text-lg font-bold bg-blue-500 hover:bg-blue-600"
```
#### Styles
Style objects and strings are merged, with later properties overriding earlier ones:
```ts
const props1 = { style: { color: "red", fontSize: "16px" } };
const props2 = { style: "background-color: blue; font-weight: bold;" };
const mergedProps = mergeProps(props1, props2);
console.log(mergedProps.style);
// "color: red; font-size: 16px; background-color: blue; font-weight: bold;"
```
```ts
import { mergeProps } from "bits-ui";
const props1 = { style: "--foo: red" };
const props2 = { style: { "--foo": "green", color: "blue" } };
const mergedProps = mergeProps(props1, props2);
console.log(mergedProps.style); // "--foo: green; color: blue;"
```
### `onDestroyEffect`
Executes a callback when a component is destroyed.
### `onMountEffect`
Executes a callback when a component is mounted.
### `styleToString`
Converts a style object to a CSS string.
```ts
const style = { color: "red", fontSize: "16px" };
const css = styleToString(style);
console.log(css); // "color: red; font-size: 16px;"
```
### `srOnlyStyles`
An object of styles that can be used to hide content from the DOM but still be accessible to screen readers.
```ts
export const srOnlyStyles: StyleProperties = {
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%)"
};
```
### `srOnlyStylesString`
A string representation of `srOnlyStyles`.
### `useRefById`
Finds the node with the given boxed `id` and sets it to the boxed `ref`. Reactive using `$effect` to ensure when the `id` or `deps` change, an update is triggered and the node is re-found.
#### Props
```ts
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 `() => (typeof document !== "undefined" ? document : undefined)
*/
getRootNode?: Getter<Document | ShadowRoot | undefined>;
};
```
### `useOnChange`
A simple helper function to react to changes to reactive state. This is useful for syncing a read-only dependency that may change with some writable state in your app.
```svelte
<script lang="ts">
import { useOnChange } from "svelte-toolbelt";
let { data } = $props();
let myData = $state(data);
useOnChange(
() => myData,
(newData) => (myData = newData)
);
</script>
```
### `attachRef`
Creates a Svelte attachment that attaches a DOM element to a ref.
```svelte
<script lang="ts">
import { attachRef } from "svelte-toolbelt";
const ref = box<Element | null>(null);
</script>
<div {...attachRef(ref)}>Content</div>
```
or
```svelte
<script lang="ts">
import { attachRef } from "svelte-toolbelt";
let ref = $state<HTMLElement | null>(null);
</script>
<div {...attachRef((node) => (ref = node))}>Content</div>
```
+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;
}
+80
View File
@@ -0,0 +1,80 @@
{
"name": "svelte-toolbelt",
"version": "0.10.6",
"funding": [
"https://github.com/sponsors/huntabyte"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*"
],
"peerDependencies": {
"svelte": "^5.30.2"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@eslint/js": "^9.12.0",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.21.1",
"@sveltejs/package": "^2.3.11",
"@sveltejs/vite-plugin-svelte": "4.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.12.12",
"@typescript-eslint/eslint-plugin": "^8.10.0",
"@typescript-eslint/scope-manager": "^8.10.0",
"@typescript-eslint/utils": "^8.10.0",
"csstype": "^3.1.3",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.11.0",
"jsdom": "^24.0.0",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.3.3",
"publint": "^0.1.9",
"svelte": "^5.30.2",
"svelte-check": "^4.2.1",
"svelte-eslint-parser": "^0.43.0",
"tslib": "^2.4.1",
"typescript": "^5.6.2",
"typescript-eslint": "^8.10.0",
"vite": "^5.4.8",
"vitest": "^2.1.2"
},
"dependencies": {
"clsx": "^2.1.1",
"runed": "^0.35.1",
"style-to-object": "^1.0.8"
},
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"engines": {
"pnpm": ">=8.7.0",
"node": ">=18"
},
"packageManager": "pnpm@8.15.8",
"sideEffects": false,
"scripts": {
"dev": "pnpm --reporter append-only --color \"/dev:/\"",
"dev:svelte": "vite dev",
"dev:pkg": "svelte-package --watch",
"build": "vite build && npm run package",
"preview": "vite preview",
"package": "svelte-kit sync && svelte-package && publint",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"ci:publish": "pnpm package && changeset publish"
}
}