31 |
32 | ## License
33 |
34 |
35 |
36 | Published under the [MIT](https://github.com/svecosystem/runed/blob/main/LICENSE) license. Made by
37 | [@TGlide](https://github.com/tglide), [@huntabyte](https://github.com/huntabyte) and
38 | [community](https://github.com/svecosystem/runed/graphs/contributors) 💛
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import prettier from "eslint-config-prettier";
2 | import js from "@eslint/js";
3 | import { includeIgnoreFile } from "@eslint/compat";
4 | import svelte from "eslint-plugin-svelte";
5 | import globals from "globals";
6 | import { fileURLToPath } from "node:url";
7 | import ts from "typescript-eslint";
8 | const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
9 |
10 | export default ts.config(
11 | includeIgnoreFile(gitignorePath),
12 | js.configs.recommended,
13 | ...ts.configs.recommended,
14 | ...svelte.configs.recommended,
15 | prettier,
16 | ...svelte.configs.prettier,
17 | {
18 | languageOptions: {
19 | globals: { ...globals.browser, ...globals.node },
20 | },
21 | rules: { "no-undef": "off" },
22 | },
23 | {
24 | files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
25 | ignores: ["eslint.config.js", "svelte.config.js"],
26 | languageOptions: {
27 | parserOptions: {
28 | projectService: true,
29 | extraFileExtensions: [".svelte"],
30 | parser: ts.parser,
31 | },
32 | },
33 | rules: {
34 | "prefer-const": "off",
35 | },
36 | },
37 | {
38 | rules: {
39 | "@typescript-eslint/no-unused-vars": [
40 | "error",
41 | {
42 | argsIgnorePattern: "^_",
43 | varsIgnorePattern: "^_",
44 | },
45 | ],
46 | "@typescript-eslint/no-unused-expressions": "off",
47 | "@typescript-eslint/no-empty-object-type": "off",
48 | },
49 | },
50 | {
51 | ignores: [
52 | "build/",
53 | ".svelte-kit/",
54 | "dist/",
55 | ".svelte-kit/**/*",
56 | "sites/docs/.svelte-kit/**/*",
57 | ".svelte-kit",
58 | "packages/runed/dist/**/*",
59 | "packages/runed/.svelte-kit/**/*",
60 | ],
61 | }
62 | );
63 |
--------------------------------------------------------------------------------
/maintainers.md:
--------------------------------------------------------------------------------
1 | # Maintainer's Guide: Shipping & Ownership
2 |
3 | ## Core Philosophy
4 |
5 | At Runed, we believe in empowering maintainers to make meaningful contributions without unnecessary
6 | bureaucracy. This document outlines our approach to shipping features and making decisions.
7 |
8 | ## Performance and Developer Experience
9 |
10 | Finding the right balance between performance and developer experience (DX) is a core responsibility
11 | of every maintainer. We believe that:
12 |
13 | - Neither performance nor DX should be sacrificed entirely for the other
14 | - Every feature should be evaluated through both lenses where applicable
15 | - Performance impacts should be measured, not assumed
16 | - DX improvements should be validated with real-world usage
17 | - Complex performance optimizations must be justified by measurable benefits
18 | - "Developer-friendly" shouldn't and doesn't mean "performance-ignorant"
19 |
20 | When making decisions, consider:
21 |
22 | - Will this make the library easier to use correctly and harder to use incorrectly?
23 | - Does the performance cost justify the DX benefit, or vice versa?
24 | - Can we achieve both good DX and performance through clever API design?
25 | - Are we making assumptions about performance without data to back them up?
26 |
27 | ## Ownership & Decision Making
28 |
29 | As a maintainer, you have full autonomy to ship features, improvements, and fixes that you believe
30 | add value to Runed. What this means in practice is:
31 |
32 | - You don't need explicit permission to start working on or ship something
33 | - Your judgement about what's valuable is trusted
34 | - You own the decisions about your contributions
35 | - Other maintainers can (and should) provide feedback, but you decide whether to act on it
36 | - You're empowered to merge your own PRs when you are confident in them
37 |
38 | ## Shipping Philosophy
39 |
40 | Ship early and ship often. As a maintainer, you're empowered to move quickly and make decisions.
41 | What this means in practice is:
42 |
43 | - Bug fixes should be released immediately - users shouldn't wait for fixes we've already made just
44 | to reduce the number of patch releases
45 | - Breaking changes need proper major version bumps and documentation, but not necessarily consensus.
46 | Use your best judgement if a change is worth breaking compatibility
47 | - Consider the impact on users when making breaking changes, but don't let that paralyze you
48 | - New features can be shipping when you believe they're ready
49 | - Experiment freely with new approaches - we can always iterate based on feedback
50 |
51 | Remember: It's often better to ship something good now than to wait for something perfect later.
52 |
53 | ## Best Practices
54 |
55 | To maintain a healthy codebase and team dynamic, we should strive to follow these best practices:
56 |
57 | ### Document Your Changes
58 |
59 | - Write clear and concise PR descriptions
60 | - Update the relevant documentation
61 | - Add inline comments for non-obvious code
62 |
63 | ### Maintain Quality
64 |
65 | - Write tests for new functionality
66 | - Ensure CI passes
67 | - Consider performance implications
68 |
69 | ### Communicate
70 |
71 | - Use PR descriptions to explain your reasoning
72 | - Tag relevant maintainers for awareness
73 | - Respond to feedback, even if you decide not to act on it (it's okay to disagree)
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "description": "Monorepo for Runed.",
4 | "private": true,
5 | "version": "0.0.0",
6 | "homepage": "https://runed.dev",
7 | "contributors": [
8 | {
9 | "name": "Thomas G. Lopes",
10 | "url": "https://thomasglopes.com"
11 | },
12 | {
13 | "name": "Hunter Johnston",
14 | "url": "https://x.com/huntabyte"
15 | }
16 | ],
17 | "funding": [
18 | "https://github.com/sponsors/huntabyte",
19 | "https://github.com/sponsors/tglide"
20 | ],
21 | "main": "index.js",
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/svecosystem/runed"
25 | },
26 | "scripts": {
27 | "dev": "pnpm sync && pnpm --parallel dev",
28 | "test": "pnpm -r test",
29 | "test:package": "pnpm -F \"./packages/**\" test",
30 | "test:package:watch": "pnpm -F \"./packages/**\" test:watch",
31 | "build": "pnpm -r build",
32 | "build:packages": "pnpm -F \"./packages/**\" --parallel build",
33 | "build:content": "pnpm -F \"./sites/**\" --parallel build:content",
34 | "ci:publish": "pnpm build:packages && changeset publish",
35 | "lint": "prettier --check . && eslint .",
36 | "lint:fix": "prettier --write . && eslint . --fix",
37 | "format": "prettier --write .",
38 | "check": "pnpm -r check",
39 | "sync": "pnpm -r sync",
40 | "add": "node ./scripts/add-utility.mjs"
41 | },
42 | "license": "MIT",
43 | "devDependencies": {
44 | "@changesets/cli": "^2.27.10",
45 | "@eslint/compat": "^1.2.8",
46 | "@eslint/js": "^9.18.0",
47 | "@svitejs/changesets-changelog-github-compact": "^1.2.0",
48 | "eslint": "^9.18.0",
49 | "eslint-config-prettier": "^10.0.1",
50 | "eslint-plugin-svelte": "^3.5.0",
51 | "globals": "^16.0.0",
52 | "prettier": "^3.3.3",
53 | "prettier-plugin-svelte": "^3.3.3",
54 | "prettier-plugin-tailwindcss": "^0.6.11",
55 | "readline-sync": "^1.4.10",
56 | "svelte": "^5.11.0",
57 | "typescript": "^5.6.3",
58 | "typescript-eslint": "^8.20.0",
59 | "wrangler": "^4.15.0"
60 | },
61 | "type": "module",
62 | "engines": {
63 | "pnpm": ">=9.0.0",
64 | "node": ">=18"
65 | },
66 | "packageManager": "pnpm@9.14.4"
67 | }
68 |
--------------------------------------------------------------------------------
/packages/runed/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /dist
5 | /.svelte-kit
6 | /package
7 | .env
8 | .env.*
9 | !.env.example
10 | vite.config.js.timestamp-*
11 | vite.config.ts.timestamp-*
12 |
--------------------------------------------------------------------------------
/packages/runed/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Hunter Johnston
4 | Copyright (c) 2024 Thomas G. Lopes
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/packages/runed/README.md:
--------------------------------------------------------------------------------
1 | # Runed
2 |
3 |
4 |
5 | [](https://npmjs.com/package/runed)
6 | [](https://npmjs.com/package/runed)
7 | [](https://github.com/svecosystem/runed/blob/main/LICENSE)
8 |
9 |
10 |
11 | Runed provides utilities to power your applications using the magic of
12 | [Svelte Runes](https://svelte.dev/blog/runes).
13 |
14 | ## Features
15 |
16 |
17 |
18 | ## Installation
19 |
20 | Runed will be published to NPM once Svelte 5 is released.
21 |
22 | ## License
23 |
24 |
25 |
26 | Published under the [MIT](https://github.com/svecosystem/runed/blob/main/LICENSE) license. Made by
27 | [@tglide](https://github.com/tglide), [@huntabyte](https://github.com/huntabyte) and
28 | [community](https://github.com/svecosystem/runed/graphs/contributors) 💛
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/runed/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 |
3 | describe("sum test", () => {
4 | it("adds 1 + 2 to equal 3", () => {
5 | expect(1 + 2).toBe(3);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./utilities/index.js";
2 | export type { MaybeGetter, Getter, Setter } from "./internal/types.js";
3 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/internal/configurable-globals.ts:
--------------------------------------------------------------------------------
1 | import { BROWSER } from "esm-env";
2 |
3 | export type ConfigurableWindow = {
4 | /** Provide a custom `window` object to use in place of the global `window` object. */
5 | window?: typeof globalThis & Window;
6 | };
7 |
8 | export type ConfigurableDocument = {
9 | /** Provide a custom `document` object to use in place of the global `document` object. */
10 | document?: Document;
11 | };
12 |
13 | export type ConfigurableDocumentOrShadowRoot = {
14 | /*
15 | * Specify a custom `document` instance or a shadow root, e.g. working with iframes or in testing environments.
16 | */
17 | document?: DocumentOrShadowRoot;
18 | };
19 |
20 | export type ConfigurableNavigator = {
21 | /** Provide a custom `navigator` object to use in place of the global `navigator` object. */
22 | navigator?: Navigator;
23 | };
24 |
25 | export type ConfigurableLocation = {
26 | /** Provide a custom `location` object to use in place of the global `location` object. */
27 | location?: Location;
28 | };
29 |
30 | export const defaultWindow = BROWSER && typeof window !== "undefined" ? window : undefined;
31 | export const defaultDocument =
32 | BROWSER && typeof window !== "undefined" ? window.document : undefined;
33 | export const defaultNavigator =
34 | BROWSER && typeof window !== "undefined" ? window.navigator : undefined;
35 | export const defaultLocation =
36 | BROWSER && typeof window !== "undefined" ? window.location : undefined;
37 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/internal/types.ts:
--------------------------------------------------------------------------------
1 | export type Getter = () => T;
2 | export type MaybeGetter = T | Getter;
3 | export type MaybeElementGetter = MaybeGetter;
4 | export type MaybeElement = HTMLElement | SVGElement | undefined | null;
5 |
6 | export type Setter = (value: T) => void;
7 | export type Expand = T extends infer U ? { [K in keyof U]: U[K] } : never;
8 | export type WritableProperties = {
9 | -readonly [P in keyof T]: T[P];
10 | };
11 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/internal/utils/array.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Get nth item of Array. Negative for backward
3 | */
4 | export function at(array: readonly T[], index: number): T | undefined {
5 | const len = array.length;
6 | if (!len) return undefined;
7 |
8 | if (index < 0) index += len;
9 |
10 | return array[index];
11 | }
12 |
13 | export function last(array: readonly T[]): T | undefined {
14 | return array[array.length - 1];
15 | }
16 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/internal/utils/browser.ts:
--------------------------------------------------------------------------------
1 | export { BROWSER as browser } from "esm-env";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/internal/utils/dom.ts:
--------------------------------------------------------------------------------
1 | import { defaultDocument } from "../configurable-globals.js";
2 |
3 | /**
4 | * Handles getting the active element in a document or shadow root.
5 | * If the active element is within a shadow root, it will traverse the shadow root
6 | * to find the active element.
7 | * If not, it will return the active element in the document.
8 | *
9 | * @param document A document or shadow root to get the active element from.
10 | * @returns The active element in the document or shadow root.
11 | */
12 | export function getActiveElement(document: DocumentOrShadowRoot): Element | null {
13 | let activeElement = document.activeElement;
14 |
15 | while (activeElement?.shadowRoot) {
16 | const node = activeElement.shadowRoot.activeElement;
17 | if (node === activeElement) break;
18 | else activeElement = node;
19 | }
20 |
21 | return activeElement;
22 | }
23 |
24 | /**
25 | * Returns the owner document of a given element.
26 | *
27 | * @param node The element to get the owner document from.
28 | * @returns
29 | */
30 | export function getOwnerDocument(
31 | node: Element | null | undefined,
32 | fallback = defaultDocument
33 | ): Document | undefined {
34 | return node?.ownerDocument ?? fallback;
35 | }
36 |
37 | /**
38 | * Checks if an element is or is contained by another element.
39 | *
40 | * @param node The element to check if it or its descendants contain the target element.
41 | * @param target The element to check if it is contained by the node.
42 | * @returns
43 | */
44 | export function isOrContainsTarget(node: Element, target: Element) {
45 | return node === target || node.contains(target);
46 | }
47 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/internal/utils/function.ts:
--------------------------------------------------------------------------------
1 | export function noop(): void {}
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/internal/utils/get.ts:
--------------------------------------------------------------------------------
1 | import type { MaybeGetter } from "../types.js";
2 | import { isFunction } from "./is.js";
3 |
4 | export function get(value: MaybeGetter): T {
5 | if (isFunction(value)) {
6 | return value();
7 | }
8 |
9 | return value;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/internal/utils/is.ts:
--------------------------------------------------------------------------------
1 | export function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
2 | return typeof value === "function";
3 | }
4 |
5 | export function isObject(value: unknown): value is Record {
6 | return value !== null && typeof value === "object";
7 | }
8 |
9 | export function isElement(value: unknown): value is Element {
10 | return value instanceof Element;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/internal/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | export async function sleep(ms = 0): Promise {
2 | return new Promise((resolve) => setTimeout(resolve, ms));
3 | }
4 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/test/util.svelte.ts:
--------------------------------------------------------------------------------
1 | import { test, vi } from "vitest";
2 |
3 | export function testWithEffect(name: string, fn: () => void | Promise): void {
4 | test(name, () => effectRootScope(fn));
5 | }
6 |
7 | export function effectRootScope(fn: () => void | Promise): void | Promise {
8 | let promise!: void | Promise;
9 | const cleanup = $effect.root(() => {
10 | promise = fn();
11 | });
12 |
13 | if (promise instanceof Promise) {
14 | return promise.finally(cleanup);
15 | } else {
16 | cleanup();
17 | }
18 | }
19 |
20 | export function vitestSetTimeoutWrapper(fn: () => void, timeout: number): void {
21 | setTimeout(() => {
22 | fn();
23 | }, timeout + 1);
24 |
25 | vi.advanceTimersByTime(timeout);
26 | }
27 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/active-element/active-element.svelte.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultWindow,
3 | type ConfigurableDocumentOrShadowRoot,
4 | type ConfigurableWindow,
5 | } from "$lib/internal/configurable-globals.js";
6 | import { getActiveElement } from "$lib/internal/utils/dom.js";
7 | import { on } from "svelte/events";
8 | import { createSubscriber } from "svelte/reactivity";
9 |
10 | export interface ActiveElementOptions
11 | extends ConfigurableDocumentOrShadowRoot,
12 | ConfigurableWindow {}
13 |
14 | export class ActiveElement {
15 | readonly #document?: DocumentOrShadowRoot;
16 | readonly #subscribe?: () => void;
17 |
18 | constructor(options: ActiveElementOptions = {}) {
19 | const { window = defaultWindow, document = window?.document } = options;
20 | if (window === undefined) return;
21 |
22 | this.#document = document;
23 | this.#subscribe = createSubscriber((update) => {
24 | const cleanupFocusIn = on(window, "focusin", update);
25 | const cleanupFocusOut = on(window, "focusout", update);
26 | return () => {
27 | cleanupFocusIn();
28 | cleanupFocusOut();
29 | };
30 | });
31 | }
32 |
33 | get current(): Element | null {
34 | this.#subscribe?.();
35 | if (!this.#document) return null;
36 | return getActiveElement(this.#document);
37 | }
38 | }
39 |
40 | /**
41 | * An object holding a reactive value that is equal to `document.activeElement`.
42 | * It automatically listens for changes, keeping the reference up to date.
43 | *
44 | * If you wish to use a custom document or shadowRoot, you should use
45 | * [useActiveElement](https://runed.dev/docs/utilities/active-element) instead.
46 | *
47 | * @see {@link https://runed.dev/docs/utilities/active-element}
48 | */
49 | export const activeElement = new ActiveElement();
50 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/active-element/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./active-element.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/animation-frames/animation-frames.svelte.ts:
--------------------------------------------------------------------------------
1 | import { untrack } from "svelte";
2 | import { extract } from "../extract/index.js";
3 | import type { MaybeGetter } from "$lib/internal/types.js";
4 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js";
5 |
6 | type RafCallbackParams = {
7 | /** The number of milliseconds since the last frame. */
8 | delta: number;
9 | /**
10 | * Time elapsed since the creation of the web page.
11 | * See {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin Time origin}.
12 | */
13 | timestamp: DOMHighResTimeStamp;
14 | };
15 |
16 | export type AnimationFramesOptions = ConfigurableWindow & {
17 | /**
18 | * Start calling requestAnimationFrame immediately.
19 | *
20 | * @default true
21 | */
22 | immediate?: boolean;
23 |
24 | /**
25 | * Limit the number of frames per second.
26 | * Set to `0` to disable
27 | *
28 | * @default 0
29 | */
30 | fpsLimit?: MaybeGetter;
31 | };
32 |
33 | /**
34 | * Wrapper over {@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame requestAnimationFrame},
35 | * with controls for pausing and resuming the animation, reactive tracking and optional limiting of fps, and utilities.
36 | */
37 | export class AnimationFrames {
38 | #callback: (params: RafCallbackParams) => void;
39 | #fpsLimitOption: AnimationFramesOptions["fpsLimit"] = 0;
40 | #fpsLimit = $derived(extract(this.#fpsLimitOption) ?? 0);
41 | #previousTimestamp: number | null = null;
42 | #frame: number | null = null;
43 | #fps = $state(0);
44 | #running = $state(false);
45 | #window = defaultWindow;
46 |
47 | constructor(callback: (params: RafCallbackParams) => void, options: AnimationFramesOptions = {}) {
48 | if (options.window) this.#window = options.window;
49 | this.#fpsLimitOption = options.fpsLimit;
50 | this.#callback = callback;
51 |
52 | this.start = this.start.bind(this);
53 | this.stop = this.stop.bind(this);
54 | this.toggle = this.toggle.bind(this);
55 |
56 | $effect(() => {
57 | if (options.immediate ?? true) {
58 | untrack(this.start);
59 | }
60 |
61 | return this.stop;
62 | });
63 | }
64 |
65 | #loop(timestamp: DOMHighResTimeStamp): void {
66 | if (!this.#running || !this.#window) return;
67 |
68 | if (this.#previousTimestamp === null) {
69 | this.#previousTimestamp = timestamp;
70 | }
71 |
72 | const delta = timestamp - this.#previousTimestamp;
73 | const fps = 1000 / delta;
74 | if (this.#fpsLimit && fps > this.#fpsLimit) {
75 | this.#frame = this.#window.requestAnimationFrame(this.#loop.bind(this));
76 | return;
77 | }
78 |
79 | this.#fps = fps;
80 | this.#previousTimestamp = timestamp;
81 | this.#callback({ delta, timestamp });
82 | this.#frame = this.#window.requestAnimationFrame(this.#loop.bind(this));
83 | }
84 |
85 | start(): void {
86 | if (!this.#window) return;
87 | this.#running = true;
88 | this.#previousTimestamp = 0;
89 | this.#frame = this.#window.requestAnimationFrame(this.#loop.bind(this));
90 | }
91 |
92 | stop(): void {
93 | if (!this.#frame || !this.#window) return;
94 | this.#running = false;
95 | this.#window.cancelAnimationFrame(this.#frame);
96 | this.#frame = null;
97 | }
98 |
99 | toggle(): void {
100 | this.#running ? this.stop() : this.start();
101 | }
102 |
103 | get fps(): number {
104 | return !this.#running ? 0 : this.#fps;
105 | }
106 |
107 | get running(): boolean {
108 | return this.#running;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/animation-frames/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./animation-frames.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/context/context.ts:
--------------------------------------------------------------------------------
1 | import { getContext, hasContext, setContext } from "svelte";
2 |
3 | export class Context {
4 | readonly #name: string;
5 | readonly #key: symbol;
6 |
7 | /**
8 | * @param name The name of the context.
9 | * This is used for generating the context key and error messages.
10 | */
11 | constructor(name: string) {
12 | this.#name = name;
13 | this.#key = Symbol(name);
14 | }
15 |
16 | /**
17 | * The key used to get and set the context.
18 | *
19 | * It is not recommended to use this value directly.
20 | * Instead, use the methods provided by this class.
21 | */
22 | get key(): symbol {
23 | return this.#key;
24 | }
25 |
26 | /**
27 | * Checks whether this has been set in the context of a parent component.
28 | *
29 | * Must be called during component initialisation.
30 | */
31 | exists(): boolean {
32 | return hasContext(this.#key);
33 | }
34 |
35 | /**
36 | * Retrieves the context that belongs to the closest parent component.
37 | *
38 | * Must be called during component initialisation.
39 | *
40 | * @throws An error if the context does not exist.
41 | */
42 | get(): TContext {
43 | const context: TContext | undefined = getContext(this.#key);
44 | if (context === undefined) {
45 | throw new Error(`Context "${this.#name}" not found`);
46 | }
47 | return context;
48 | }
49 |
50 | /**
51 | * Retrieves the context that belongs to the closest parent component,
52 | * or the given fallback value if the context does not exist.
53 | *
54 | * Must be called during component initialisation.
55 | */
56 | getOr(fallback: TFallback): TContext | TFallback {
57 | const context: TContext | undefined = getContext(this.#key);
58 | if (context === undefined) {
59 | return fallback;
60 | }
61 | return context;
62 | }
63 |
64 | /**
65 | * Associates the given value with the current component and returns it.
66 | *
67 | * Must be called during component initialisation.
68 | */
69 | set(context: TContext): TContext {
70 | return setContext(this.#key, context);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/context/index.ts:
--------------------------------------------------------------------------------
1 | export { Context } from "./context.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/debounced/debounced.svelte.ts:
--------------------------------------------------------------------------------
1 | import { useDebounce } from "../use-debounce/use-debounce.svelte.js";
2 | import { watch } from "../watch/watch.svelte.js";
3 | import type { Getter, MaybeGetter } from "$lib/internal/types.js";
4 | import { noop } from "$lib/internal/utils/function.js";
5 |
6 | /**
7 | * A wrapper over {@link useDebounce} that creates a debounced state.
8 | * It takes a "getter" function which returns the state you want to debounce.
9 | * Every time this state changes a timer (re)starts, the length of which is
10 | * configurable with the `wait` arg. When the timer ends the `current` value
11 | * is updated.
12 | *
13 | * @see https://runed.dev/docs/utilities/debounced
14 | *
15 | * @example
16 | *
17 | *
23 | *
24 | *
25 | *
26 | *
You searched for: {debounced.current}
27 | *
28 | */
29 | export class Debounced {
30 | #current: T = $state()!;
31 | #debounceFn: ReturnType;
32 |
33 | /**
34 | * @param getter A function that returns the state to watch.
35 | * @param wait The length of time to wait in ms, defaults to 250.
36 | */
37 | constructor(getter: Getter, wait: MaybeGetter = 250) {
38 | this.#current = getter(); // immediately set the initial value
39 | this.cancel = this.cancel.bind(this);
40 | this.setImmediately = this.setImmediately.bind(this);
41 | this.updateImmediately = this.updateImmediately.bind(this);
42 |
43 | this.#debounceFn = useDebounce(() => {
44 | this.#current = getter();
45 | }, wait);
46 |
47 | watch(getter, () => {
48 | this.#debounceFn().catch(noop);
49 | });
50 | }
51 |
52 | /**
53 | * Get the current value.
54 | */
55 | get current(): T {
56 | return this.#current;
57 | }
58 |
59 | /**
60 | * Cancel the latest timer.
61 | */
62 | cancel(): void {
63 | this.#debounceFn.cancel();
64 | }
65 |
66 | /**
67 | * Run the debounced function immediately.
68 | */
69 | updateImmediately(): Promise {
70 | return this.#debounceFn.runScheduledNow();
71 | }
72 |
73 | /**
74 | * Set the `current` value without waiting.
75 | */
76 | setImmediately(v: T): void {
77 | this.cancel();
78 | this.#current = v;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/debounced/debounced.test.svelte.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect } from "vitest";
2 | import { Debounced } from "./index.js";
3 | import { testWithEffect } from "$lib/test/util.svelte.js";
4 |
5 | describe("Debounced", () => {
6 | testWithEffect("Value does not get updated immediately", async () => {
7 | let value = $state(0);
8 | const debounced = new Debounced(() => value, 100);
9 |
10 | expect(debounced.current).toBe(0);
11 | value = 1;
12 | expect(debounced.current).toBe(0);
13 | await new Promise((resolve) => setTimeout(resolve, 200));
14 | expect(debounced.current).toBe(1);
15 | });
16 |
17 | testWithEffect("Can cancel debounced update", async () => {
18 | let value = $state(0);
19 | const debounced = new Debounced(() => value, 100);
20 |
21 | expect(debounced.current).toBe(0);
22 | value = 1;
23 | expect(debounced.current).toBe(0);
24 | debounced.cancel();
25 | await new Promise((resolve) => setTimeout(resolve, 200));
26 | expect(debounced.current).toBe(0);
27 | });
28 |
29 | testWithEffect("Can set value immediately", async () => {
30 | let value = $state(0);
31 | const debounced = new Debounced(() => value, 100);
32 |
33 | expect(debounced.current).toBe(0);
34 | value = 1;
35 | expect(debounced.current).toBe(0);
36 | await new Promise((resolve) => setTimeout(resolve, 200));
37 | expect(debounced.current).toBe(1);
38 |
39 | debounced.setImmediately(2);
40 | expect(debounced.current).toBe(2);
41 | });
42 |
43 | testWithEffect("Can run update immediately", async () => {
44 | let value = $state(0);
45 | const debounced = new Debounced(() => value * 2, 100);
46 |
47 | expect(debounced.current).toBe(0);
48 | value = 1;
49 | expect(debounced.current).toBe(0);
50 | await debounced.updateImmediately();
51 | expect(debounced.current).toBe(2);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/debounced/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./debounced.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/element-rect/element-rect.svelte.ts:
--------------------------------------------------------------------------------
1 | import { extract } from "../extract/extract.svelte.js";
2 | import { useMutationObserver } from "../use-mutation-observer/use-mutation-observer.svelte.js";
3 | import { useResizeObserver } from "../use-resize-observer/use-resize-observer.svelte.js";
4 | import type { MaybeElementGetter, WritableProperties } from "$lib/internal/types.js";
5 | import type { ConfigurableWindow } from "$lib/internal/configurable-globals.js";
6 |
7 | type Rect = WritableProperties>;
8 |
9 | export type ElementRectOptions = ConfigurableWindow & {
10 | initialRect?: DOMRect;
11 | };
12 |
13 | /**
14 | * Returns a reactive value holding the size of `node`.
15 | *
16 | * Accepts an `options` object with the following properties:
17 | * - `initialSize`: The initial size of the element. Defaults to `{ width: 0, height: 0 }`.
18 | * - `box`: The box model to use. Can be either `"content-box"` or `"border-box"`. Defaults to `"border-box"`.
19 | *
20 | * @returns an object with `width` and `height` properties.
21 | *
22 | * @see {@link https://runed.dev/docs/utilities/element-size}
23 | */
24 | export class ElementRect {
25 | #rect: Rect = $state({
26 | x: 0,
27 | y: 0,
28 | width: 0,
29 | height: 0,
30 | top: 0,
31 | right: 0,
32 | bottom: 0,
33 | left: 0,
34 | });
35 |
36 | constructor(node: MaybeElementGetter, options: ElementRectOptions = {}) {
37 | this.#rect = {
38 | width: options.initialRect?.width ?? 0,
39 | height: options.initialRect?.height ?? 0,
40 | x: options.initialRect?.x ?? 0,
41 | y: options.initialRect?.y ?? 0,
42 | top: options.initialRect?.top ?? 0,
43 | right: options.initialRect?.right ?? 0,
44 | bottom: options.initialRect?.bottom ?? 0,
45 | left: options.initialRect?.left ?? 0,
46 | };
47 |
48 | const el = $derived(extract(node));
49 | const update = () => {
50 | if (!el) return;
51 | const rect = el.getBoundingClientRect();
52 | this.#rect.width = rect.width;
53 | this.#rect.height = rect.height;
54 | this.#rect.x = rect.x;
55 | this.#rect.y = rect.y;
56 | this.#rect.top = rect.top;
57 | this.#rect.right = rect.right;
58 | this.#rect.bottom = rect.bottom;
59 | this.#rect.left = rect.left;
60 | };
61 |
62 | useResizeObserver(() => el, update, { window: options.window });
63 | $effect(update);
64 | useMutationObserver(() => el, update, {
65 | attributeFilter: ["style", "class"],
66 | window: options.window,
67 | });
68 | }
69 |
70 | get x(): number {
71 | return this.#rect.x;
72 | }
73 |
74 | get y(): number {
75 | return this.#rect.y;
76 | }
77 |
78 | get width(): number {
79 | return this.#rect.width;
80 | }
81 |
82 | get height(): number {
83 | return this.#rect.height;
84 | }
85 |
86 | get top(): number {
87 | return this.#rect.top;
88 | }
89 |
90 | get right(): number {
91 | return this.#rect.right;
92 | }
93 |
94 | get bottom(): number {
95 | return this.#rect.bottom;
96 | }
97 |
98 | get left(): number {
99 | return this.#rect.left;
100 | }
101 |
102 | get current(): Rect {
103 | return this.#rect;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/element-rect/index.ts:
--------------------------------------------------------------------------------
1 | export { ElementRect } from "./element-rect.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/element-size/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./element-size.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/extract/extract.svelte.ts:
--------------------------------------------------------------------------------
1 | import type { MaybeGetter } from "$lib/internal/types.js";
2 | import { isFunction } from "$lib/internal/utils/is.js";
3 |
4 | /**
5 | * Resolves a value that may be a getter function or a direct value.
6 | *
7 | * If the input is a function, it will be invoked to retrieve the actual value.
8 | * If the resolved value (or the input itself) is `undefined`, the optional
9 | * `defaultValue` is returned instead.
10 | *
11 | * @template T - The expected return type.
12 | * @param value - A value or a function that returns a value.
13 | * @param defaultValue - A fallback value returned if the resolved value is `undefined`.
14 | * @returns The resolved value or the default.
15 | */
16 | export function extract(value: MaybeGetter): T;
17 | export function extract(value: MaybeGetter, defaultValue: T): T;
18 |
19 | export function extract(value: unknown, defaultValue?: unknown) {
20 | if (isFunction(value)) {
21 | const getter = value;
22 | const gotten = getter();
23 | if (gotten === undefined) return defaultValue;
24 | return gotten;
25 | }
26 |
27 | if (value === undefined) return defaultValue;
28 | return value;
29 | }
30 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/extract/index.ts:
--------------------------------------------------------------------------------
1 | export { extract } from "./extract.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/finite-state-machine/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./finite-state-machine.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./active-element/index.js";
2 | export * from "./animation-frames/index.js";
3 | export * from "./context/index.js";
4 | export * from "./debounced/index.js";
5 | export * from "./element-rect/index.js";
6 | export * from "./element-size/index.js";
7 | export * from "./extract/index.js";
8 | export * from "./finite-state-machine/index.js";
9 | export * from "./is-focus-within/index.js";
10 | export * from "./is-idle/index.js";
11 | export * from "./is-in-viewport/index.js";
12 | export * from "./is-mounted/index.js";
13 | export * from "./on-click-outside/index.js";
14 | export * from "./persisted-state/index.js";
15 | export * from "./pressed-keys/index.js";
16 | export * from "./previous/index.js";
17 | export * from "./resource/index.js";
18 | export * from "./state-history/index.js";
19 | export * from "./textarea-autosize/index.js";
20 | export * from "./use-debounce/index.js";
21 | export * from "./use-event-listener/index.js";
22 | export * from "./use-geolocation/index.js";
23 | export * from "./use-intersection-observer/index.js";
24 | export * from "./use-mutation-observer/index.js";
25 | export * from "./use-resize-observer/index.js";
26 | export * from "./watch/index.js";
27 |
28 | export * from "./scroll-state/index.js";
29 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/is-focus-within/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./is-focus-within.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/is-focus-within/is-focus-within.svelte.ts:
--------------------------------------------------------------------------------
1 | import type { MaybeElementGetter } from "$lib/internal/types.js";
2 | import {
3 | ActiveElement,
4 | type ActiveElementOptions,
5 | } from "../active-element/active-element.svelte.js";
6 | import { extract } from "../extract/extract.svelte.js";
7 |
8 | export interface IsFocusWithinOptions extends ActiveElementOptions {}
9 |
10 | /**
11 | * Tracks whether the focus is within a target element.
12 | * @see {@link https://runed.dev/docs/utilities/is-focus-within}
13 | */
14 | export class IsFocusWithin {
15 | readonly #node: MaybeElementGetter;
16 | readonly #activeElement: ActiveElement;
17 |
18 | constructor(node: MaybeElementGetter, options: IsFocusWithinOptions = {}) {
19 | this.#node = node;
20 | this.#activeElement = new ActiveElement(options);
21 | }
22 |
23 | readonly current = $derived.by(() => {
24 | const node = extract(this.#node);
25 | if (node == null) return false;
26 | return node.contains(this.#activeElement.current);
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/is-idle/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./is-idle.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/is-idle/is-idle.svelte.ts:
--------------------------------------------------------------------------------
1 | import { extract } from "../extract/index.js";
2 | import { useDebounce } from "../use-debounce/index.js";
3 | import type { MaybeGetter } from "$lib/internal/types.js";
4 | import { useEventListener } from "$lib/utilities/use-event-listener/use-event-listener.svelte.js";
5 | import {
6 | defaultWindow,
7 | type ConfigurableDocument,
8 | type ConfigurableWindow,
9 | } from "$lib/internal/configurable-globals.js";
10 |
11 | type WindowEvent = keyof WindowEventMap;
12 |
13 | export type IsIdleOptions = ConfigurableDocument &
14 | ConfigurableWindow & {
15 | /**
16 | * The events that should set the idle state to `true`
17 | *
18 | * @default ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel']
19 | */
20 | events?: MaybeGetter<(keyof WindowEventMap)[]>;
21 | /**
22 | * The timeout in milliseconds before the idle state is set to `true`. Defaults to 60 seconds.
23 | *
24 | * @default 60000
25 | */
26 | timeout?: MaybeGetter;
27 | /**
28 | * Detect document visibility changes
29 | *
30 | * @default false
31 | */
32 | detectVisibilityChanges?: MaybeGetter;
33 | /**
34 | * The initial state of the idle property
35 | *
36 | * @default false
37 | */
38 | initialState?: boolean;
39 | };
40 |
41 | const DEFAULT_EVENTS = [
42 | "keypress",
43 | "mousemove",
44 | "touchmove",
45 | "click",
46 | "scroll",
47 | ] satisfies WindowEvent[];
48 |
49 | const DEFAULT_OPTIONS = {
50 | events: DEFAULT_EVENTS,
51 | initialState: false,
52 | timeout: 60000,
53 | } satisfies IsIdleOptions;
54 |
55 | /**
56 | * Tracks whether the user is being inactive.
57 | * @see {@link https://runed.dev/docs/utilities/is-idle}
58 | */
59 | export class IsIdle {
60 | #current: boolean = $state(false);
61 | #lastActive = $state(Date.now());
62 |
63 | constructor(_options?: IsIdleOptions) {
64 | const opts = {
65 | ...DEFAULT_OPTIONS,
66 | ..._options,
67 | };
68 | const window = opts.window ?? defaultWindow;
69 | const document = opts.document ?? window?.document;
70 |
71 | const timeout = $derived(extract(opts.timeout));
72 | const events = $derived(extract(opts.events));
73 | const detectVisibilityChanges = $derived(extract(opts.detectVisibilityChanges));
74 | this.#current = opts.initialState;
75 |
76 | const debouncedReset = useDebounce(
77 | () => {
78 | this.#current = true;
79 | },
80 | () => timeout
81 | );
82 |
83 | debouncedReset();
84 |
85 | const handleActivity = () => {
86 | this.#current = false;
87 | this.#lastActive = Date.now();
88 | debouncedReset();
89 | };
90 |
91 | useEventListener(
92 | () => window,
93 | events,
94 | () => {
95 | handleActivity();
96 | },
97 | { passive: true }
98 | );
99 |
100 | $effect(() => {
101 | if (!detectVisibilityChanges || !document) return;
102 | useEventListener(document, ["visibilitychange"], () => {
103 | if (document.hidden) return;
104 | handleActivity();
105 | });
106 | });
107 | }
108 |
109 | get lastActive(): number {
110 | return this.#lastActive;
111 | }
112 |
113 | get current(): boolean {
114 | return this.#current;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/is-idle/is-idle.test.svelte.ts:
--------------------------------------------------------------------------------
1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2 | import { IsIdle } from "./is-idle.svelte.js";
3 | import { testWithEffect, vitestSetTimeoutWrapper } from "$lib/test/util.svelte.js";
4 |
5 | describe("IsIdle", () => {
6 | beforeEach(() => {
7 | vi.useFakeTimers();
8 | });
9 | afterEach(() => {
10 | vi.clearAllTimers();
11 | });
12 |
13 | const DEFAULT_IDLE_TIME = 500;
14 | describe("Default behaviors", () => {
15 | testWithEffect("Initially set to false", async () => {
16 | const idleState = new IsIdle();
17 | expect(idleState.current).toBe(false);
18 | });
19 |
20 | testWithEffect("IsIdle is set to true when no activity occurs", async () => {
21 | const idleState = new IsIdle();
22 |
23 | vitestSetTimeoutWrapper(() => {
24 | expect(idleState.current).toBe(true);
25 | }, DEFAULT_IDLE_TIME);
26 | });
27 |
28 | testWithEffect("IsIdle is set to false on click event", async () => {
29 | const idleState = new IsIdle();
30 |
31 | vitestSetTimeoutWrapper(() => {
32 | expect(idleState.current).toBe(true);
33 | const input = document.createElement("input");
34 | document.body.appendChild(input);
35 | input.click();
36 | expect(idleState.current).toBe(false);
37 | }, DEFAULT_IDLE_TIME);
38 | });
39 | });
40 |
41 | describe("Args", () => {
42 | testWithEffect("IsIdle timer arg", async () => {
43 | const idleState = new IsIdle({ timeout: 300 });
44 | vitestSetTimeoutWrapper(() => {
45 | expect(idleState.current).toBe(false);
46 | }, 200);
47 | vitestSetTimeoutWrapper(() => {
48 | expect(idleState.current).toBe(true);
49 | }, 300);
50 | });
51 |
52 | testWithEffect("Initial state option gets overwritten when passed in", async () => {
53 | const idleState = new IsIdle({ initialState: true });
54 | expect(idleState.current).toBe(true);
55 | });
56 |
57 | it.skip("Events args work gets overwritten when passed in", async () => {
58 | const idleState = new IsIdle({ events: ["keypress"] });
59 |
60 | vitestSetTimeoutWrapper(() => {
61 | const input = document.createElement("input");
62 | document.body.appendChild(input);
63 | input.click();
64 | expect(idleState.current).toBe(true);
65 | // TODO: add jest-dom to handle events
66 | }, DEFAULT_IDLE_TIME);
67 | });
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/is-in-viewport/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./is-in-viewport.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/is-in-viewport/is-in-viewport.svelte.ts:
--------------------------------------------------------------------------------
1 | import type { ConfigurableWindow } from "$lib/internal/configurable-globals.js";
2 | import type { MaybeElementGetter } from "$lib/internal/types.js";
3 | import {
4 | useIntersectionObserver,
5 | type UseIntersectionObserverOptions,
6 | } from "../use-intersection-observer/use-intersection-observer.svelte.js";
7 |
8 | export type IsInViewportOptions = ConfigurableWindow & UseIntersectionObserverOptions;
9 |
10 | /**
11 | * Tracks if an element is visible within the current viewport.
12 | *
13 | * @see {@link https://runed.dev/docs/utilities/is-in-viewport}
14 | */
15 | export class IsInViewport {
16 | #isInViewport = $state(false);
17 |
18 | constructor(node: MaybeElementGetter, options?: IsInViewportOptions) {
19 | useIntersectionObserver(
20 | node,
21 | (intersectionObserverEntries) => {
22 | let isIntersecting = this.#isInViewport;
23 | let latestTime = 0;
24 | for (const entry of intersectionObserverEntries) {
25 | if (entry.time >= latestTime) {
26 | latestTime = entry.time;
27 | isIntersecting = entry.isIntersecting;
28 | }
29 | }
30 | this.#isInViewport = isIntersecting;
31 | },
32 | options
33 | );
34 | }
35 |
36 | get current() {
37 | return this.#isInViewport;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/is-mounted/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./is-mounted.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/is-mounted/is-mounted.svelte.ts:
--------------------------------------------------------------------------------
1 | import { untrack } from "svelte";
2 |
3 | /**
4 | * Returns an object with the mounted state of the component
5 | * that invokes this function.
6 | *
7 | * @see {@link https://runed.dev/docs/utilities/is-mounted}
8 | */
9 | export class IsMounted {
10 | #isMounted: boolean = $state(false);
11 |
12 | constructor() {
13 | $effect(() => {
14 | untrack(() => (this.#isMounted = true));
15 |
16 | return () => {
17 | this.#isMounted = false;
18 | };
19 | });
20 | }
21 |
22 | get current(): boolean {
23 | return this.#isMounted;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/on-click-outside/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./on-click-outside.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/persisted-state/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./persisted-state.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/pressed-keys/index.ts:
--------------------------------------------------------------------------------
1 | export { PressedKeys } from "./pressed-keys.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/pressed-keys/pressed-keys.svelte.ts:
--------------------------------------------------------------------------------
1 | import { on } from "svelte/events";
2 | import { createSubscriber } from "svelte/reactivity";
3 | import { watch } from "$lib/utilities/watch/index.js";
4 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js";
5 |
6 | export type PressedKeysOptions = ConfigurableWindow;
7 | /**
8 | * Tracks which keys are currently pressed.
9 | *
10 | * @see {@link https://runed.dev/docs/utilities/pressed-keys}
11 | */
12 | export class PressedKeys {
13 | #pressedKeys = $state([]);
14 | readonly #subscribe?: () => void;
15 |
16 | constructor(options: PressedKeysOptions = {}) {
17 | const { window = defaultWindow } = options;
18 | this.has = this.has.bind(this);
19 |
20 | if (!window) return;
21 |
22 | this.#subscribe = createSubscriber((update) => {
23 | const keydown = on(window, "keydown", (e) => {
24 | const key = e.key.toLowerCase();
25 | if (!this.#pressedKeys.includes(key)) {
26 | this.#pressedKeys.push(key);
27 | }
28 | update();
29 | });
30 |
31 | const keyup = on(window, "keyup", (e) => {
32 | const key = e.key.toLowerCase();
33 |
34 | // Special handling for modifier keys (meta, control, alt, shift)
35 | // This addresses issues with OS/browser intercepting certain key combinations
36 | // where non-modifier keyup events might not fire properly
37 | if (["meta", "control", "alt", "shift"].includes(key)) {
38 | // When a modifier key is released, clear all non-modifier keys
39 | // but keep other modifier keys that might still be pressed
40 | // This prevents keys from getting "stuck" in the pressed state
41 | this.#pressedKeys = this.#pressedKeys.filter((k) =>
42 | ["meta", "control", "alt", "shift"].includes(k)
43 | );
44 | }
45 |
46 | // Regular key removal
47 | this.#pressedKeys = this.#pressedKeys.filter((k) => k !== key);
48 | update();
49 | });
50 |
51 | // Handle window blur events (switching applications, clicking outside browser)
52 | // Reset all keys when user shifts focus away from the window
53 | const blur = on(window, "blur", () => {
54 | this.#pressedKeys = [];
55 | update();
56 | });
57 |
58 | // Handle tab visibility changes (switching browser tabs)
59 | // This catches cases where the window doesn't lose focus but the tab is hidden
60 | const visibilityChange = on(document, "visibilitychange", () => {
61 | if (document.visibilityState === "hidden") {
62 | this.#pressedKeys = [];
63 | update();
64 | }
65 | });
66 |
67 | return () => {
68 | keydown();
69 | keyup();
70 | blur();
71 | visibilityChange();
72 | };
73 | });
74 | }
75 |
76 | has(...keys: string[]): boolean {
77 | this.#subscribe?.();
78 | const normalizedKeys = keys.map((key) => key.toLowerCase());
79 | return normalizedKeys.every((key) => this.#pressedKeys.includes(key));
80 | }
81 |
82 | get all(): string[] {
83 | this.#subscribe?.();
84 | return this.#pressedKeys;
85 | }
86 |
87 | /**
88 | * Registers a callback to execute when specified key combination is pressed.
89 | *
90 | * @param keys - Array or single string of keys to monitor
91 | * @param callback - Function to execute when the key combination is matched
92 | */
93 | onKeys(keys: string | string[], callback: () => void) {
94 | this.#subscribe?.();
95 |
96 | const keysToMonitor = Array.isArray(keys) ? keys : [keys];
97 |
98 | watch(
99 | () => this.all,
100 | () => {
101 | if (this.has(...keysToMonitor)) {
102 | callback();
103 | }
104 | }
105 | );
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/previous/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./previous.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/previous/previous.svelte.ts:
--------------------------------------------------------------------------------
1 | import type { Getter } from "$lib/internal/types.js";
2 | import { watch } from "../watch/watch.svelte.js";
3 |
4 | /**
5 | * Holds the previous value of a getter.
6 | *
7 | * @see {@link https://runed.dev/docs/utilities/previous}
8 | */
9 | export class Previous {
10 | #previous: T | undefined = $state(undefined);
11 |
12 | constructor(getter: Getter, initialValue?: T) {
13 | if (initialValue !== undefined) this.#previous = initialValue;
14 |
15 | watch(
16 | () => getter(),
17 | (_, v) => {
18 | this.#previous = v;
19 | }
20 | );
21 | }
22 |
23 | get current(): T | undefined {
24 | return this.#previous;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/previous/previous.test.svelte.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from "$lib/internal/utils/sleep.js";
2 | import { testWithEffect } from "$lib/test/util.svelte.js";
3 | import { describe } from "node:test";
4 | import { expect } from "vitest";
5 | import { Previous } from "./previous.svelte.js";
6 |
7 | describe("usePrevious", () => {
8 | testWithEffect("Should return undefined initially", () => {
9 | const previous = new Previous(() => 0);
10 | expect(previous.current).toBe(undefined);
11 | });
12 |
13 | testWithEffect("Should return initialValue initially, when passed", () => {
14 | const previous = new Previous(() => 1, 0);
15 | expect(previous.current).toBe(0);
16 | });
17 |
18 | testWithEffect("Should return previous value", async () => {
19 | let count = $state(0);
20 | const previous = new Previous(() => count);
21 |
22 | await sleep(10);
23 | expect(previous.current).toBe(undefined);
24 | count = 1;
25 | await sleep(10);
26 | expect(previous.current).toBe(0);
27 | count = 2;
28 | await sleep(10);
29 | expect(previous.current).toBe(1);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/resource/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./resource.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/resource/msw-handlers.ts:
--------------------------------------------------------------------------------
1 | import { http, delay, HttpResponse } from "msw";
2 |
3 | export type ResponseData = {
4 | id: number;
5 | name: string;
6 | email: string;
7 | };
8 |
9 | export type SearchResponseData = {
10 | results: { id: number; title: string }[];
11 | page: number;
12 | total: number;
13 | };
14 |
15 | export const handlers = [
16 | // Basic user endpoint
17 | http.get("https://api.example.com/users/:id", async ({ params }) => {
18 | await delay(50);
19 | return HttpResponse.json({
20 | id: Number(params.id),
21 | name: `User ${params.id}`,
22 | email: `user${params.id}@example.com`,
23 | });
24 | }),
25 |
26 | // Search endpoint with query params
27 | http.get("https://api.example.com/search", ({ request }) => {
28 | const url = new URL(request.url);
29 | const query = url.searchParams.get("q");
30 | const page = Number(url.searchParams.get("page")) || 1;
31 |
32 | return HttpResponse.json({
33 | results: [
34 | { id: page * 1, title: `Result 1 for ${query}` },
35 | { id: page * 2, title: `Result 2 for ${query}` },
36 | ],
37 | page,
38 | total: 10,
39 | });
40 | }),
41 |
42 | // Endpoint that can fail
43 | http.get("https://api.example.com/error-prone", () => {
44 | return new HttpResponse(null, { status: 500 });
45 | }),
46 | ];
47 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/scroll-state/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./scroll-state.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/state-history/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./state-history.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/state-history/state-history.svelte.ts:
--------------------------------------------------------------------------------
1 | import { watch } from "../watch/watch.svelte.js";
2 | import type { MaybeGetter, Setter } from "$lib/internal/types.js";
3 | import { get } from "$lib/internal/utils/get.js";
4 |
5 | type LogEvent = {
6 | snapshot: T;
7 | timestamp: number;
8 | };
9 |
10 | type StateHistoryOptions = {
11 | capacity?: MaybeGetter;
12 | };
13 |
14 | /**
15 | * Tracks the change history of a value, providing undo and redo capabilities.
16 | *
17 | * @see {@link https://runed.dev/docs/utilities/state-history}
18 | */
19 | export class StateHistory {
20 | #redoStack: LogEvent[] = $state([]);
21 | #ignoreUpdate: boolean = false;
22 | #set: Setter;
23 | log: LogEvent[] = $state([]);
24 | readonly canUndo = $derived(this.log.length > 1);
25 | readonly canRedo = $derived(this.#redoStack.length > 0);
26 |
27 | constructor(value: MaybeGetter, set: Setter, options?: StateHistoryOptions) {
28 | this.#redoStack = [];
29 | this.#set = set;
30 | this.undo = this.undo.bind(this);
31 | this.redo = this.redo.bind(this);
32 |
33 | const addEvent = (event: LogEvent): void => {
34 | this.log.push(event);
35 | const capacity$ = get(options?.capacity);
36 | if (capacity$ && this.log.length > capacity$) {
37 | this.log = this.log.slice(-capacity$);
38 | }
39 | };
40 |
41 | watch(
42 | () => get(value),
43 | (v) => {
44 | if (this.#ignoreUpdate) {
45 | this.#ignoreUpdate = false;
46 | return;
47 | }
48 |
49 | addEvent({ snapshot: v, timestamp: new Date().getTime() });
50 | this.#redoStack = [];
51 | }
52 | );
53 |
54 | watch(
55 | () => get(options?.capacity),
56 | (c) => {
57 | if (!c) return;
58 | this.log = this.log.slice(-c);
59 | }
60 | );
61 | }
62 |
63 | undo(): void {
64 | const [prev, curr] = this.log.slice(-2);
65 | if (!curr || !prev) return;
66 | this.#ignoreUpdate = true;
67 | this.#redoStack.push(curr);
68 | this.log.pop();
69 | this.#set(prev.snapshot);
70 | }
71 |
72 | redo(): void {
73 | const nextEvent = this.#redoStack.pop();
74 | if (!nextEvent) return;
75 | this.#ignoreUpdate = true;
76 | this.log.push(nextEvent);
77 | this.#set(nextEvent.snapshot);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/state-history/state-history.test.svelte.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 |
3 | describe("StateHistory", () => {
4 | test("dummy test", () => {
5 | expect(true).toBe(true);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/textarea-autosize/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./textarea-autosize.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-debounce/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./use-debounce.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-debounce/use-debounce.svelte.ts:
--------------------------------------------------------------------------------
1 | import type { MaybeGetter } from "$lib/internal/types.js";
2 | import { extract } from "../extract/extract.svelte.js";
3 |
4 | type UseDebounceReturn = ((
5 | this: unknown,
6 | ...args: Args
7 | ) => Promise) & {
8 | cancel: () => void;
9 | runScheduledNow: () => Promise;
10 | pending: boolean;
11 | };
12 |
13 | type DebounceContext = {
14 | timeout: ReturnType | null;
15 | runner: (() => Promise) | null;
16 | resolve: (value: Return) => void;
17 | reject: (reason: unknown) => void;
18 | promise: Promise;
19 | };
20 |
21 | /**
22 | * Function that takes a callback, and returns a debounced version of it.
23 | * When calling the debounced function, it will wait for the specified time
24 | * before calling the original callback. If the debounced function is called
25 | * again before the time has passed, the timer will be reset.
26 | *
27 | * You can await the debounced function to get the value when it is eventually
28 | * called.
29 | *
30 | * The second parameter is the time to wait before calling the original callback.
31 | * Alternatively, it can also be a getter function that returns the time to wait.
32 | *
33 | * @see {@link https://runed.dev/docs/utilities/use-debounce}
34 | *
35 | * @param callback The callback to call when the time has passed.
36 | * @param wait The length of time to wait in ms, defaults to 250.
37 | */
38 | export function useDebounce(
39 | callback: (...args: Args) => Return,
40 | wait?: MaybeGetter
41 | ): UseDebounceReturn {
42 | let context = $state | null>(null);
43 | const wait$ = $derived(extract(wait, 250));
44 |
45 | function debounced(this: unknown, ...args: Args) {
46 | if (context) {
47 | // Old context will be reused so callers awaiting the promise will get the
48 | // new value
49 | if (context.timeout) {
50 | clearTimeout(context.timeout);
51 | }
52 | } else {
53 | // No old context, create a new one
54 | let resolve: (value: Return) => void;
55 | let reject: (reason: unknown) => void;
56 | const promise = new Promise((res, rej) => {
57 | resolve = res;
58 | reject = rej;
59 | });
60 |
61 | context = {
62 | timeout: null,
63 | runner: null,
64 | promise,
65 | resolve: resolve!,
66 | reject: reject!,
67 | };
68 | }
69 |
70 | context.runner = async () => {
71 | // Grab the context and reset it
72 | // -> new debounced calls will create a new context
73 | if (!context) return;
74 | const ctx = context;
75 | context = null;
76 |
77 | try {
78 | ctx.resolve(await callback.apply(this, args));
79 | } catch (error) {
80 | ctx.reject(error);
81 | }
82 | };
83 |
84 | context.timeout = setTimeout(context.runner, wait$);
85 |
86 | return context.promise;
87 | }
88 |
89 | debounced.cancel = async () => {
90 | if (!context || context.timeout === null) {
91 | // Wait one event loop to see if something triggered the debounced function
92 | await new Promise((resolve) => setTimeout(resolve, 0));
93 | if (!context || context.timeout === null) return;
94 | }
95 |
96 | clearTimeout(context.timeout);
97 | context.reject("Cancelled");
98 | context = null;
99 | };
100 |
101 | debounced.runScheduledNow = async () => {
102 | if (!context || !context.timeout) {
103 | // Wait one event loop to see if something triggered the debounced function
104 | await new Promise((resolve) => setTimeout(resolve, 0));
105 | if (!context || !context.timeout) return;
106 | }
107 |
108 | clearTimeout(context.timeout);
109 | context.timeout = null;
110 |
111 | await context.runner?.();
112 | };
113 |
114 | Object.defineProperty(debounced, "pending", {
115 | enumerable: true,
116 | get() {
117 | return !!context?.timeout;
118 | },
119 | });
120 |
121 | return debounced as unknown as UseDebounceReturn;
122 | }
123 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-event-listener/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./use-event-listener.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-geolocation/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./use-geolocation.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-geolocation/use-geolocation.svelte.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultNavigator,
3 | type ConfigurableNavigator,
4 | } from "$lib/internal/configurable-globals.js";
5 | import type { WritableProperties } from "$lib/internal/types.js";
6 |
7 | export type UseGeolocationOptions = Partial & {
8 | /**
9 | * Whether to start the watcher immediately upon creation. If set to `false`, the watcher
10 | * will only start tracking the position when `resume()` is called.
11 | *
12 | * @defaultValue true
13 | */
14 | immediate?: boolean;
15 | } & ConfigurableNavigator;
16 |
17 | type WritableGeolocationPosition = WritableProperties<
18 | Omit
19 | > & {
20 | coords: WritableProperties>;
21 | };
22 |
23 | export type UseGeolocationPosition = Omit & {
24 | coords: Omit;
25 | };
26 |
27 | export type UseGeolocationReturn = {
28 | readonly isSupported: boolean;
29 | readonly position: UseGeolocationPosition;
30 | readonly error: GeolocationPositionError | null;
31 | readonly isPaused: boolean;
32 | resume: () => void;
33 | pause: () => void;
34 | };
35 |
36 | /**
37 | * Reactive access to the browser's [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).
38 | *
39 | * @see https://runed.dev/docs/utilities/use-geolocation
40 | */
41 | export function useGeolocation(options: UseGeolocationOptions = {}): UseGeolocationReturn {
42 | const {
43 | enableHighAccuracy = true,
44 | maximumAge = 30000,
45 | timeout = 27000,
46 | immediate = true,
47 | navigator = defaultNavigator,
48 | } = options;
49 |
50 | const isSupported = Boolean(navigator);
51 |
52 | let error = $state.raw(null);
53 | let position = $state({
54 | timestamp: 0,
55 | coords: {
56 | accuracy: 0,
57 | latitude: Number.POSITIVE_INFINITY,
58 | longitude: Number.POSITIVE_INFINITY,
59 | altitude: null,
60 | altitudeAccuracy: null,
61 | heading: null,
62 | speed: null,
63 | },
64 | });
65 | let isPaused = $state(false);
66 |
67 | function updatePosition(_position: GeolocationPosition) {
68 | error = null;
69 | position.timestamp = _position.timestamp;
70 | position.coords.accuracy = _position.coords.accuracy;
71 | position.coords.altitude = _position.coords.altitude;
72 | position.coords.altitudeAccuracy = _position.coords.altitudeAccuracy;
73 | position.coords.heading = _position.coords.heading;
74 | position.coords.latitude = _position.coords.latitude;
75 | position.coords.longitude = _position.coords.longitude;
76 | position.coords.speed = _position.coords.speed;
77 | }
78 |
79 | let watcher: number;
80 |
81 | function resume() {
82 | if (!navigator) return;
83 | watcher = navigator.geolocation.watchPosition(updatePosition, (err) => (error = err), {
84 | enableHighAccuracy,
85 | maximumAge,
86 | timeout,
87 | });
88 | isPaused = false;
89 | }
90 |
91 | function pause() {
92 | if (watcher && navigator) {
93 | navigator.geolocation.clearWatch(watcher);
94 | }
95 | isPaused = true;
96 | }
97 |
98 | $effect(() => {
99 | if (immediate) resume();
100 | return () => pause();
101 | });
102 |
103 | return {
104 | get isSupported() {
105 | return isSupported;
106 | },
107 | position,
108 | get error() {
109 | return error;
110 | },
111 | get isPaused() {
112 | return isPaused;
113 | },
114 | resume,
115 | pause,
116 | };
117 | }
118 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-intersection-observer/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./use-intersection-observer.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-intersection-observer/use-intersection-observer.svelte.ts:
--------------------------------------------------------------------------------
1 | import { extract } from "../extract/extract.svelte.js";
2 | import type { MaybeElementGetter, MaybeGetter } from "$lib/internal/types.js";
3 | import { get } from "$lib/internal/utils/get.js";
4 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js";
5 |
6 | export interface UseIntersectionObserverOptions
7 | extends Omit,
8 | ConfigurableWindow {
9 | /**
10 | * Whether to start the observer immediately upon creation. If set to `false`, the observer
11 | * will only start observing when `resume()` is called.
12 | *
13 | * @defaultValue true
14 | */
15 | immediate?: boolean;
16 |
17 | /**
18 | * The root document/element to use as the bounding box for the intersection.
19 | */
20 | root?: MaybeElementGetter;
21 | }
22 |
23 | /**
24 | * Watch for intersection changes of a target element.
25 | *
26 | * @see https://runed.dev/docs/utilities/useIntersectionObserver
27 | * @see https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver IntersectionObserver MDN
28 | */
29 | export function useIntersectionObserver(
30 | target: MaybeGetter,
31 | callback: IntersectionObserverCallback,
32 | options: UseIntersectionObserverOptions = {}
33 | ) {
34 | const {
35 | root,
36 | rootMargin = "0px",
37 | threshold = 0.1,
38 | immediate = true,
39 | window = defaultWindow,
40 | } = options;
41 |
42 | let isActive = $state(immediate);
43 | let observer: IntersectionObserver | undefined;
44 |
45 | const targets = $derived.by(() => {
46 | const value = extract(target);
47 | return new Set(value ? (Array.isArray(value) ? value : [value]) : []);
48 | });
49 |
50 | const stop = $effect.root(() => {
51 | $effect(() => {
52 | if (!targets.size || !isActive || !window) return;
53 | observer = new window.IntersectionObserver(callback, {
54 | rootMargin,
55 | root: get(root),
56 | threshold,
57 | });
58 | for (const el of targets) observer.observe(el);
59 |
60 | return () => {
61 | observer?.disconnect();
62 | };
63 | });
64 | });
65 |
66 | $effect(() => {
67 | return stop;
68 | });
69 |
70 | return {
71 | get isActive() {
72 | return isActive;
73 | },
74 | stop,
75 | pause() {
76 | isActive = false;
77 | },
78 | resume() {
79 | isActive = true;
80 | },
81 | };
82 | }
83 |
84 | export type UseIntersectionObserverReturn = ReturnType;
85 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-mutation-observer/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./use-mutation-observer.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-mutation-observer/use-mutation-observer.svelte.ts:
--------------------------------------------------------------------------------
1 | import { extract } from "../extract/extract.svelte.js";
2 | import type { MaybeGetter } from "$lib/internal/types.js";
3 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js";
4 |
5 | export interface UseMutationObserverOptions extends MutationObserverInit, ConfigurableWindow {}
6 |
7 | /**
8 | * Watch for changes being made to the DOM tree.
9 | *
10 | * @see https://runed.dev/docs/utilities/useMutationObserver
11 | * @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver MutationObserver MDN
12 | */
13 | export function useMutationObserver(
14 | target: MaybeGetter,
15 | callback: MutationCallback,
16 | options: UseMutationObserverOptions = {}
17 | ) {
18 | const { window = defaultWindow } = options;
19 | let observer: MutationObserver | undefined;
20 |
21 | const targets = $derived.by(() => {
22 | const value = extract(target);
23 | return new Set(value ? (Array.isArray(value) ? value : [value]) : []);
24 | });
25 |
26 | const stop = $effect.root(() => {
27 | $effect(() => {
28 | if (!targets.size || !window) return;
29 | observer = new window.MutationObserver(callback);
30 | for (const el of targets) observer.observe(el, options);
31 |
32 | return () => {
33 | observer?.disconnect();
34 | observer = undefined;
35 | };
36 | });
37 | });
38 |
39 | $effect(() => {
40 | return stop;
41 | });
42 |
43 | return {
44 | stop,
45 | takeRecords() {
46 | return observer?.takeRecords();
47 | },
48 | };
49 | }
50 |
51 | export type UseMutationObserverReturn = ReturnType;
52 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-resize-observer/index.ts:
--------------------------------------------------------------------------------
1 | export { useResizeObserver } from "./use-resize-observer.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/use-resize-observer/use-resize-observer.svelte.ts:
--------------------------------------------------------------------------------
1 | import { extract } from "../extract/extract.svelte.js";
2 | import type { MaybeGetter } from "$lib/internal/types.js";
3 | import { defaultWindow, type ConfigurableWindow } from "$lib/internal/configurable-globals.js";
4 |
5 | export interface ResizeObserverSize {
6 | readonly inlineSize: number;
7 | readonly blockSize: number;
8 | }
9 |
10 | export interface ResizeObserverEntry {
11 | readonly target: Element;
12 | readonly contentRect: DOMRectReadOnly;
13 | readonly borderBoxSize?: ReadonlyArray;
14 | readonly contentBoxSize?: ReadonlyArray;
15 | readonly devicePixelContentBoxSize?: ReadonlyArray;
16 | }
17 |
18 | export type ResizeObserverCallback = (
19 | entries: ReadonlyArray,
20 | observer: ResizeObserver
21 | ) => void;
22 |
23 | export interface UseResizeObserverOptions extends ConfigurableWindow {
24 | /**
25 | * Sets which box model the observer will observe changes to. Possible values
26 | * are `content-box` (the default), `border-box` and `device-pixel-content-box`.
27 | *
28 | * @default 'content-box'
29 | */
30 | box?: ResizeObserverBoxOptions;
31 | }
32 |
33 | declare class ResizeObserver {
34 | constructor(callback: ResizeObserverCallback);
35 | disconnect(): void;
36 | observe(target: Element, options?: UseResizeObserverOptions): void;
37 | unobserve(target: Element): void;
38 | }
39 |
40 | /**
41 | * Reports changes to the dimensions of an Element's content or the border-box
42 | *
43 | * @see https://runed.dev/docs/utilities/useResizeObserver
44 | */
45 | export function useResizeObserver(
46 | target: MaybeGetter,
47 | callback: ResizeObserverCallback,
48 | options: UseResizeObserverOptions = {}
49 | ) {
50 | const { window = defaultWindow } = options;
51 | let observer: ResizeObserver | undefined;
52 |
53 | const targets = $derived.by(() => {
54 | const value = extract(target);
55 | return new Set(value ? (Array.isArray(value) ? value : [value]) : []);
56 | });
57 |
58 | const stop = $effect.root(() => {
59 | $effect(() => {
60 | if (!targets.size || !window) return;
61 | observer = new window.ResizeObserver(callback);
62 | for (const el of targets) observer.observe(el, options);
63 |
64 | return () => {
65 | observer?.disconnect();
66 | observer = undefined;
67 | };
68 | });
69 | });
70 |
71 | $effect(() => {
72 | return stop;
73 | });
74 |
75 | return {
76 | stop,
77 | };
78 | }
79 |
80 | export type UseResizeObserverReturn = ReturnType;
81 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/watch/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./watch.svelte.js";
2 |
--------------------------------------------------------------------------------
/packages/runed/src/lib/utilities/watch/watch.test.svelte.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect } from "vitest";
2 | import { watch, watchOnce } from "./watch.svelte.js";
3 | import { testWithEffect } from "$lib/test/util.svelte.js";
4 | import { sleep } from "$lib/internal/utils/sleep.js";
5 |
6 | describe("watch", () => {
7 | testWithEffect("watchers only track their dependencies", async () => {
8 | let count = $state(0);
9 | let runs = $state(0);
10 |
11 | watch(
12 | () => count,
13 | () => {
14 | runs = runs + 1;
15 | }
16 | );
17 |
18 | // Watchers run immediately by default
19 | await sleep(0);
20 | expect(runs).toBe(1);
21 |
22 | count++;
23 | await sleep(0);
24 | expect(runs).toBe(2);
25 | });
26 |
27 | testWithEffect("watchers initially pass `undefined` as the previous value", () => {
28 | return new Promise((resolve) => {
29 | const count = $state(0);
30 |
31 | watch(
32 | () => count,
33 | (count, prevCount) => {
34 | expect(count).toBe(0);
35 | expect(prevCount).toBe(undefined);
36 | resolve();
37 | }
38 | );
39 | });
40 | });
41 |
42 | testWithEffect(
43 | "watchers with an array of sources initially pass an empty array as the previous value",
44 | () => {
45 | return new Promise((resolve) => {
46 | const count = $state(1);
47 | const doubled = $derived(count * 2);
48 |
49 | watch([() => count, () => doubled], ([count, doubled], [prevCount, prevDoubled]) => {
50 | expect(count).toBe(1);
51 | expect(prevCount).toBe(undefined);
52 | expect(doubled).toBe(2);
53 | expect(prevDoubled).toBe(undefined);
54 | resolve();
55 | });
56 | });
57 | }
58 | );
59 |
60 | testWithEffect("lazy watchers pass the initial value as the previous value", () => {
61 | return new Promise((resolve) => {
62 | let count = $state(0);
63 |
64 | watch(
65 | () => count,
66 | (count, prevCount) => {
67 | expect(count).toBe(1);
68 | expect(prevCount).toBe(0);
69 | resolve();
70 | },
71 | { lazy: true }
72 | );
73 |
74 | // Wait for the watcher's initial run to determine its dependencies.
75 | sleep(0).then(() => {
76 | count = 1;
77 | });
78 | });
79 | });
80 |
81 | testWithEffect("once watchers only run once", async () => {
82 | let count = $state(0);
83 | let runs = 0;
84 |
85 | watchOnce(
86 | () => count,
87 | () => {
88 | runs++;
89 | }
90 | );
91 |
92 | // Wait for the watcher's initial run to determine its dependencies.
93 | await sleep(0);
94 |
95 | count++;
96 | await sleep(0);
97 | expect(runs).toBe(1);
98 |
99 | count++;
100 | await sleep(0);
101 | expect(runs).toBe(1);
102 | });
103 |
104 | testWithEffect("once watchers pass the initial value as the previous value", () => {
105 | return new Promise((resolve) => {
106 | let count = $state(0);
107 |
108 | watchOnce(
109 | () => count,
110 | (count, prevCount) => {
111 | expect(count).toBe(1);
112 | expect(prevCount).toBe(0);
113 | resolve();
114 | }
115 | );
116 |
117 | // Wait for the watcher's initial run to determine its dependencies.
118 | sleep(0).then(() => {
119 | count = 1;
120 | });
121 | });
122 | });
123 | });
124 |
--------------------------------------------------------------------------------
/packages/runed/svelte.config.js:
--------------------------------------------------------------------------------
1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
2 |
3 | /** @type {import('@sveltejs/kit').Config} */
4 | const config = {
5 | preprocess: [vitePreprocess()],
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/packages/runed/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "module": "NodeNext",
13 | "moduleResolution": "NodeNext",
14 | "noUncheckedIndexedAccess": true,
15 | "types": ["vitest/globals"],
16 | "experimentalDecorators": true
17 | },
18 | "include": [
19 | "./.svelte-kit/ambient.d.ts",
20 | "./.svelte-kit/non-ambient.d.ts",
21 | "./.svelte-kit/types/**/$types.d.ts",
22 | "./src/**/*.js",
23 | "./src/**/*.ts",
24 | "./src/**/*.svelte",
25 | "./vite.config.ts",
26 | "./svelte.config.js",
27 | "./setupTest.ts"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/packages/runed/vite.config.ts:
--------------------------------------------------------------------------------
1 | import process from "node:process";
2 | import { sveltekit } from "@sveltejs/kit/vite";
3 | import { defineConfig } from "vitest/config";
4 | import { svelteTesting } from "@testing-library/svelte/vite";
5 | import type { Plugin } from "vite";
6 |
7 | const vitestBrowserConditionPlugin: Plugin = {
8 | name: "vite-plugin-vitest-browser-condition",
9 | configResolved({ resolve }: { resolve: { conditions: string[] } }) {
10 | if (process.env.VITEST) {
11 | resolve.conditions.unshift("browser");
12 | }
13 | },
14 | };
15 |
16 | export default defineConfig({
17 | plugins: [vitestBrowserConditionPlugin, sveltekit(), svelteTesting()],
18 | test: {
19 | includeSource: ["src/**/*.{js,ts,svelte}"],
20 | globals: true,
21 | coverage: {
22 | exclude: ["./setupTest.ts"],
23 | },
24 | workspace: [
25 | {
26 | extends: true,
27 | test: {
28 | setupFiles: ["./setupTest.ts"],
29 | include: ["src/**/*.{test,test.svelte,spec}.{js,ts}"],
30 | exclude: ["src/**/*.browser.{test,test.svelte,spec}.{js,ts}"],
31 | name: "unit",
32 | environment: "jsdom",
33 | },
34 | },
35 | {
36 | plugins: [sveltekit(), svelteTesting()],
37 | test: {
38 | include: ["src/**/*.browser.{test,test.svelte,spec}.{js,ts}"],
39 | name: "browser",
40 | browser: {
41 | instances: [
42 | {
43 | browser: "chromium",
44 | },
45 | ],
46 | enabled: true,
47 | provider: "playwright",
48 | headless: true,
49 | },
50 | },
51 | },
52 | ],
53 | },
54 | });
55 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
3 | - "sites/*"
4 |
--------------------------------------------------------------------------------
/scripts/add-utility.mjs:
--------------------------------------------------------------------------------
1 | /* CLI tool to add a new utility. It asks for the utility name and then creates the respective files. */
2 | import fs from "node:fs";
3 | import readlineSync from "readline-sync";
4 |
5 | function toKebabCase(str) {
6 | return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
7 | }
8 |
9 | const utilsDir = "./packages/runed/src/lib/utilities";
10 | const contentDir = "./sites/docs/src/content/utilities";
11 | const demosDir = "./sites/docs/src/lib/components/demos";
12 |
13 | const utilName = readlineSync.question("What is the name of the utility? ");
14 | const kebabUtil = toKebabCase(utilName);
15 | const utilDir = `${utilsDir}/${kebabUtil}`;
16 | const utilIndexFile = `${utilDir}/index.ts`;
17 | const utilMainFile = `${utilDir}/${kebabUtil}.svelte.ts`;
18 | const utilsBarrelFile = `${utilsDir}/index.ts`;
19 | const contentFile = `${contentDir}/${kebabUtil}.md`;
20 | const demoFile = `${demosDir}/${kebabUtil}.svelte`;
21 |
22 | fs.mkdirSync(utilDir, { recursive: true });
23 | fs.writeFileSync(utilIndexFile, `export * from "./${kebabUtil}.svelte.js";`);
24 | fs.writeFileSync(utilMainFile, "");
25 | fs.appendFileSync(utilsBarrelFile, `export * from "./${kebabUtil}/index.js";`);
26 |
27 | // Write the boilerplate code for the docs content file
28 | fs.writeFileSync(
29 | contentFile,
30 | `---
31 | title: ${utilName}
32 | description: N/A
33 | category: New
34 | ---
35 |
36 |
39 |
40 | ## Demo
41 |
42 |
43 |
44 | ## Usage
45 | `
46 | );
47 |
48 | // Write the boilerplate code for the demo file
49 | fs.writeFileSync(
50 | demoFile,
51 | `
52 |
56 |
57 |
58 |
59 |
60 | `
61 | );
62 |
--------------------------------------------------------------------------------
/sites/docs/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 | vite.config.js.timestamp-*
10 | vite.config.ts.timestamp-*
11 | .contentlayer
12 | .contentlayer/
13 | /.contentlayer
14 | .vercel
--------------------------------------------------------------------------------
/sites/docs/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Hunter Johnston
4 | Copyright (c) 2024 Thomas G. Lopes
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/sites/docs/README.md:
--------------------------------------------------------------------------------
1 | # Runed Documentation
2 |
--------------------------------------------------------------------------------
/sites/docs/mdsx.config.js:
--------------------------------------------------------------------------------
1 | import { resolve } from "node:path";
2 | import { fileURLToPath } from "node:url";
3 | import { defineConfig } from "mdsx";
4 | import { baseRehypePlugins, baseRemarkPlugins } from "@svecodocs/kit/mdsxConfig";
5 |
6 | const __dirname = fileURLToPath(new URL(".", import.meta.url));
7 |
8 | export default defineConfig({
9 | remarkPlugins: [...baseRemarkPlugins],
10 | // @ts-expect-error shh
11 | rehypePlugins: [...baseRehypePlugins],
12 | blueprints: {
13 | default: {
14 | path: resolve(__dirname, "./src/lib/components/blueprint.svelte"),
15 | },
16 | },
17 | extensions: [".md"],
18 | });
19 |
--------------------------------------------------------------------------------
/sites/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "description": "Docs for Runed",
4 | "version": "0.0.0",
5 | "private": true,
6 | "scripts": {
7 | "dev": "pnpm \"/dev:/\"",
8 | "dev:content": "velite dev --watch",
9 | "dev:svelte": "pnpm build:search && vite dev",
10 | "build": "velite && pnpm build:search && vite build",
11 | "build:search": "node ./scripts/update-velite-output.js && node ./scripts/build-search-data.js",
12 | "preview": "vite preview",
13 | "check": "velite && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
14 | "check:watch": "pnpm build:content && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
15 | },
16 | "license": "MIT",
17 | "contributors": [
18 | {
19 | "name": "Thomas G. Lopes",
20 | "url": "https://thomasglopes.com"
21 | },
22 | {
23 | "name": "Hunter Johnston",
24 | "url": "https://github.com/huntabyte"
25 | }
26 | ],
27 | "repository": {
28 | "type": "git",
29 | "url": "https://github.com/svecosystem/runed.git"
30 | },
31 | "devDependencies": {
32 | "@svecodocs/kit": "^0.2.1",
33 | "@sveltejs/adapter-auto": "^6.0.1",
34 | "@sveltejs/adapter-cloudflare": "^7.0.3",
35 | "@sveltejs/kit": "^2.20.7",
36 | "@sveltejs/vite-plugin-svelte": "^5.0.3",
37 | "@tailwindcss/vite": "4.1.4",
38 | "mdsx": "^0.0.6",
39 | "phosphor-svelte": "^3.0.1",
40 | "runed": "workspace:^",
41 | "svelte": "^5.28.6",
42 | "svelte-check": "^4.1.6",
43 | "tailwindcss": "4.1.4",
44 | "typescript": "^5.7.2",
45 | "velite": "^0.2.1",
46 | "vite": "^6.3.5",
47 | "vitest": "^3.1.3"
48 | },
49 | "type": "module"
50 | }
51 |
--------------------------------------------------------------------------------
/sites/docs/scripts/build-search-data.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "node:url";
2 | import { writeFileSync } from "node:fs";
3 | import { resolve } from "node:path";
4 | import { docs } from "../.velite/index.js";
5 | import { cleanMarkdown } from "../node_modules/@svecodocs/kit/dist/utils.js";
6 |
7 | const __dirname = fileURLToPath(new URL(".", import.meta.url));
8 |
9 | export function buildDocsSearchIndex() {
10 | return docs.map((doc) => ({
11 | title: doc.title,
12 | href: `/docs/${doc.slug}`,
13 | description: doc.description,
14 | content: cleanMarkdown(doc.raw),
15 | }));
16 | }
17 |
18 | const searchData = buildDocsSearchIndex();
19 |
20 | writeFileSync(
21 | resolve(__dirname, "../src/routes/api/search.json/search.json"),
22 | JSON.stringify(searchData),
23 | { flag: "w" }
24 | );
25 |
--------------------------------------------------------------------------------
/sites/docs/scripts/update-velite-output.js:
--------------------------------------------------------------------------------
1 | import { readFile, writeFile } from "node:fs/promises";
2 | import { join } from "node:path";
3 | import { fileURLToPath } from "node:url";
4 |
5 | const __dirname = fileURLToPath(new URL(".", import.meta.url));
6 | const dtsPath = join(__dirname, "../.velite/index.d.ts");
7 | const indexPath = join(__dirname, "../.velite/index.js");
8 |
9 | async function replaceContents() {
10 | const data = await readFile(dtsPath, "utf8").catch((err) => {
11 | console.error("Error reading file:", err);
12 | });
13 | if (!data) return;
14 |
15 | const updatedContent = data.replace("'../velite.config'", "'../velite.config.js'");
16 | if (updatedContent === data) return;
17 |
18 | await writeFile(dtsPath, updatedContent, "utf8").catch((err) => {
19 | console.error("Error writing file:", err);
20 | });
21 | }
22 |
23 | async function replaceIndexContents() {
24 | const data = await readFile(indexPath, "utf8").catch((err) => {
25 | console.error("Error reading file:", err);
26 | });
27 | if (!data) return;
28 |
29 | const updatedContent = data.replaceAll(".json'", ".json' with { type: 'json' }");
30 | if (updatedContent === data) return;
31 |
32 | await writeFile(indexPath, updatedContent, "utf8").catch((err) => {
33 | console.error("Error writing file:", err);
34 | });
35 | }
36 |
37 | await replaceContents();
38 | await replaceIndexContents();
39 |
--------------------------------------------------------------------------------
/sites/docs/src/app.css:
--------------------------------------------------------------------------------
1 | @import "@svecodocs/kit/theme-orange.css";
2 | @import "@svecodocs/kit/globals.css";
3 | @source "../node_modules/@svecodocs/kit";
4 |
--------------------------------------------------------------------------------
/sites/docs/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface PageState {}
9 | // interface Platform {}
10 | }
11 | }
12 |
13 | export {};
14 |
--------------------------------------------------------------------------------
/sites/docs/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 |
%sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/sites/docs/src/content/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | description: Learn how to install and use Runed in your projects.
4 | category: Anchor
5 | ---
6 |
7 | ## Installation
8 |
9 | Install Runed using your favorite package manager:
10 |
11 | ```bash
12 | npm install runed
13 | ```
14 |
15 | ## Usage
16 |
17 | Import one of the utilities you need to either a `.svelte` or `.svelte.js|ts` file and start using
18 | it:
19 |
20 | ```svelte title="component.svelte"
21 |
26 |
27 |
28 |
29 | {#if activeElement.current === inputElement}
30 | The input element is active!
31 | {/if}
32 | ```
33 |
34 | or
35 |
36 | ```ts title="some-module.svelte.ts"
37 | import { activeElement } from "runed";
38 |
39 | function logActiveElement() {
40 | $effect(() => {
41 | console.log("Active element is ", activeElement.current);
42 | });
43 | }
44 |
45 | logActiveElement();
46 | ```
47 |
--------------------------------------------------------------------------------
/sites/docs/src/content/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | description: Runes are magic, but what good is magic if you don't have a wand?
4 | category: Anchor
5 | ---
6 |
7 | Runed is a collection of utilities for Svelte 5 that make composing powerful applications and
8 | libraries a breeze, leveraging the power of [Svelte Runes](https://svelte.dev/blog/runes).
9 |
10 | ## Why Runed?
11 |
12 | Svelte 5 Runes unlock immense power by providing a set of primitives that allow us to build
13 | impressive applications and libraries with ease. However, building complex applications often
14 | requires more than just the primitives provided by Svelte Runes.
15 |
16 | Runed takes those primitives to the next level by providing:
17 |
18 | - **Powerful Utilities**: A set of carefully crafted utility functions and classes that simplify
19 | common tasks and reduce boilerplate.
20 | - **Collective Efforts**: We often find ourselves writing the same utility functions over and over
21 | again. Runed aims to provide a single source of truth for these utilities, allowing the community
22 | to contribute, test, and benefit from them.
23 | - **Consistency**: A consistent set of APIs and behaviors across all utilities, so you can focus on
24 | building your projects instead of constantly learning new APIs.
25 | - **Reactivity First**: Powered by Svelte 5's new reactivity system, Runed utilities are designed to
26 | handle reactive state and side effects with ease.
27 | - **Type Safety**: Full TypeScript support to catch errors early and provide a better developer
28 | experience.
29 |
30 | ## Ideas and Principles
31 |
32 | #### Embrace the Magic of Runes
33 |
34 | Svelte Runes are a powerful new paradigm. Runed fully embraces this concept and explores its
35 | potential. Our goal is to make working with Runes feel as natural and intuitive as possible.
36 |
37 | #### Enhance, Don't Replace
38 |
39 | Runed is not here to replace Svelte's core functionality, but to enhance and extend it. Our
40 | utilities should feel like a natural extension of Svelte, not a separate framework.
41 |
42 | #### Progressive Complexity
43 |
44 | Simple things should be simple, complex things should be possible. Runed provides easy-to-use
45 | defaults while allowing for advanced customization when needed.
46 |
47 | #### Open Source and Community Collaboration
48 |
49 | Runed is an open-source, MIT licensed project that welcomes all forms of contributions from the
50 | community. Whether it's bug reports, feature requests, or code contributions, your input will help
51 | make Runed the best it can be.
52 |
--------------------------------------------------------------------------------
/sites/docs/src/content/utilities/active-element.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: activeElement
3 | description: Track and access the currently focused DOM element
4 | category: Elements
5 | ---
6 |
7 |
10 |
11 | `activeElement` provides reactive access to the currently focused DOM element in your application,
12 | similar to `document.activeElement` but with reactive updates.
13 |
14 | - Updates synchronously with DOM focus changes
15 | - Returns `null` when no element is focused
16 | - Safe to use with SSR (Server-Side Rendering)
17 | - Lightweight alternative to manual focus tracking
18 | - Searches through Shadow DOM boundaries for the true active element
19 |
20 | ## Demo
21 |
22 |
23 |
24 | ## Usage
25 |
26 | ```svelte
27 |
30 |
31 |
32 | Currently active element:
33 | {activeElement.current?.localName ?? "No active element found"}
34 |
35 | ```
36 |
37 | ## Custom Document
38 |
39 | If you wish to scope the focus tracking within a custom document or shadow root, you can pass a
40 | `DocumentOrShadowRoot` to the `ActiveElement` options:
41 |
42 | ```svelte
43 |
50 | ```
51 |
52 | ## Type Definition
53 |
54 | ```ts
55 | interface ActiveElement {
56 | readonly current: Element | null;
57 | }
58 | ```
59 |
--------------------------------------------------------------------------------
/sites/docs/src/content/utilities/animation-frames.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: AnimationFrames
3 | description: A wrapper for requestAnimationFrame with FPS control and frame metrics
4 | category: Animation
5 | ---
6 |
7 |
10 |
11 | `AnimationFrames` provides a declarative API over the browser's
12 | [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame),
13 | offering FPS limiting capabilities and frame metrics while handling cleanup automatically.
14 |
15 | ## Demo
16 |
17 |
18 |
19 | ## Usage
20 |
21 | ```svelte
22 |
41 |
42 |
49 | (fpsLimit = value[0] ?? 0)}
52 | min={0}
53 | max={144} />
54 | ```
55 |
--------------------------------------------------------------------------------
/sites/docs/src/content/utilities/context.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Context
3 | description:
4 | A wrapper around Svelte's Context API that provides type safety and improved ergonomics for
5 | sharing data between components.
6 | category: State
7 | ---
8 |
9 |
12 |
13 | Context allows you to pass data through the component tree without explicitly passing props through
14 | every level. It's useful for sharing data that many components need, like themes, authentication
15 | state, or localization preferences.
16 |
17 | The `Context` class provides a type-safe way to define, set, and retrieve context values.
18 |
19 | ## Usage
20 |
21 |
22 |
23 | Creating a Context
24 |
25 | First, create a `Context` instance with the type of value it will hold:
26 |
27 | ```ts title="context.ts"
28 | import { Context } from "runed";
29 |
30 | export const myTheme = new Context<"light" | "dark">("theme");
31 | ```
32 |
33 | Creating a `Context` instance only defines the context - it doesn't actually set any value. The
34 | value passed to the constructor (`"theme"` in this example) is just an identifier used for debugging
35 | and error messages.
36 |
37 | Think of this step as creating a "container" that will later hold your context value. The container
38 | is typed (in this case to only accept `"light"` or `"dark"` as values) but remains empty until you
39 | explicitly call `myTheme.set()` during component initialization.
40 |
41 | This separation between defining and setting context allows you to:
42 |
43 | - Keep context definitions in separate files
44 | - Reuse the same context definition across different parts of your app
45 | - Maintain type safety throughout your application
46 | - Set different values for the same context in different component trees
47 |
48 | Setting Context Values
49 |
50 | Set the context value in a parent component during initialization.
51 |
52 | ```svelte title="+layout.svelte"
53 |
59 |
60 | {@render children?.()}
61 | ```
62 |
63 |
64 |
65 | Context must be set during component initialization, similar to lifecycle functions like `onMount`.
66 | You cannot set context inside event handlers or callbacks.
67 |
68 |
69 |
70 | Reading Context Values
71 |
72 | Child components can access the context using `get()` or `getOr()`
73 |
74 | ```svelte title="+page.svelte"
75 |
82 | ```
83 |
84 |
85 |
86 | ## Type Definition
87 |
88 | ```ts
89 | class Context {
90 | /**
91 | * @param name The name of the context.
92 | * This is used for generating the context key and error messages.
93 | */
94 | constructor(name: string) {}
95 |
96 | /**
97 | * The key used to get and set the context.
98 | *
99 | * It is not recommended to use this value directly.
100 | * Instead, use the methods provided by this class.
101 | */
102 | get key(): symbol;
103 |
104 | /**
105 | * Checks whether this has been set in the context of a parent component.
106 | *
107 | * Must be called during component initialization.
108 | */
109 | exists(): boolean;
110 |
111 | /**
112 | * Retrieves the context that belongs to the closest parent component.
113 | *
114 | * Must be called during component initialization.
115 | *
116 | * @throws An error if the context does not exist.
117 | */
118 | get(): TContext;
119 |
120 | /**
121 | * Retrieves the context that belongs to the closest parent component,
122 | * or the given fallback value if the context does not exist.
123 | *
124 | * Must be called during component initialization.
125 | */
126 | getOr(fallback: TFallback): TContext | TFallback;
127 |
128 | /**
129 | * Associates the given value with the current component and returns it.
130 | *
131 | * Must be called during component initialization.
132 | */
133 | set(context: TContext): TContext;
134 | }
135 | ```
136 |
--------------------------------------------------------------------------------
/sites/docs/src/content/utilities/debounced.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Debounced
3 | description: A wrapper over `useDebounce` that returns a debounced state.
4 | category: State
5 | ---
6 |
7 |
10 |
11 | ## Demo
12 |
13 |
14 |
15 | ## Usage
16 |
17 | This is a simple wrapper over [`useDebounce`](https://runed.dev/docs/utilities/use-debounce) that
18 | returns a debounced state.
19 |
20 | ```svelte
21 |
27 |
28 |
29 |
30 |
You searched for: {debounced.current}
31 |
32 | ```
33 |
34 | You may cancel the pending update, run it immediately, or set a new value. Setting a new value
35 | immediately also cancels any pending updates.
36 |
37 | ```ts
38 | let count = $state(0);
39 | const debounced = new Debounced(() => count, 500);
40 | count = 1;
41 | debounced.cancel();
42 | // after a while...
43 | console.log(debounced.current); // Still 0!
44 |
45 | count = 2;
46 | console.log(debounced.current); // Still 0!
47 | debounced.setImmediately(count);
48 | console.log(debounced.current); // 2
49 |
50 | count = 3;
51 | console.log(debounced.current); // 2
52 | await debounced.updateImmediately();
53 | console.log(debounced.current); // 3
54 | ```
55 |
--------------------------------------------------------------------------------
/sites/docs/src/content/utilities/element-rect.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: ElementRect
3 | description: Track element dimensions and position reactively
4 | category: Elements
5 | ---
6 |
7 |
10 |
11 | `ElementRect` provides reactive access to an element's dimensions and position information,
12 | automatically updating when the element's size or position changes.
13 |
14 | ## Demo
15 |
16 |
17 |
18 | ## Usage
19 |
20 | ```svelte
21 |
27 |
28 |
29 |
30 |