diff --git a/packages/v4/docs/utils/css.md b/packages/v4/docs/utils/css.md index beda7708..0997d7ef 100644 --- a/packages/v4/docs/utils/css.md +++ b/packages/v4/docs/utils/css.md @@ -7,14 +7,14 @@ import { matrix, transform } from '@studiometa/js-toolkit-v4/utils'; [[toc]] -## `transform` +## Transforms + +### transform ```ts transform(props: TransformProps): string ``` -Builds a `transform` value from named parts, so a component composes a transform instead of assembling a string: - ```js twoslash // @twoslash-cache: {"v":1,"hash":"9a8e004351703f9dffdc3f904dd9d23844d07bb9b49a501a38f6f2dbd5cb7a95","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvNKWZg4/CKQC2jLKQhY4iXgBVZ8xSoAKGrdx1wZ7MAHMAOmHbKsStNIMKlyyiCgQRBEQQADFvZndmXgBhAGVYjzkvFQA6R0ddfBhpfFIYbOZaeF4Ifl4omSTWCIkpZjySsGyAA0r5apoAZihminKwKBzsuEJSNHw5QdLHKI00Gsk+qLgRNmylct44AGsYAHdePZs4EpJSXnY0E5xSAFpC9muzMnF4FN952yDkZBA6ZhcrFwVGaoIAVnBHG1kqpgLxaDoAIwABmRfRWax0yJSAFZeABfbgAbkcAHpSYl2hEYN1GCjkVhaH0GUzeCy+BigYxsTjuI5Qc0QABdIVUKz1BiIACcVCBdnGSBl1HqthgkuoniMPllNlwiDRIBEE1kYjIivxFHQ2D1BGIZqoNHoTDYnB4lJhajM2j0mu8pk0PEs1jsjmcrjG7q1vn8gTwYRUEU2cQS0K1aTAGSyOTyBSKJ1Km2hHVq5QakhaRep3V6/UG42Go3GkxK/BmvDmCzASy2qyBJXOy12ByO8lOZAuV14N3utEeU+eY3Ybw+zC+SB+f1oAKwQN8AohUN9KkYcIRvHp6N7MCxuIJxLJFMrXSgdNRjOZ77ZjI5V+5uL5YACsKoogOKYxIAAbAATLKMDyvgioOiqap4Km3i+BwTRIDBhrGswprkPqFpWjgeCEGcHx0OqnqaC8GB8PoSRav6WgpLQAD8OhgIIygAEbjgAPrwwiwPwupQL4YGSgA7AALLB8FIIiSGkKq6q0BhurYVQRr1PhNCEYiUHEdQ1pkXa5AOlRTDqLRYz0T6TF+l6KQYJxvDcXxgnCQMMBiU0ElivM4GIAAHMpIByrYCqIBFwVqXgGCaVhiA4bpJoGUpUomZgpHBOR9rUNZwQ0TcmAMUeygsXAKScjA7mefx5xCSJfniZJwWSqFoUKdFCGIDiKkJcEdXJXqaV4QR2EQTlZn5RZlFOiVtllQ5jGGM5AYpB2NANTxTW8C1vn+TAgWgZ1Smor1MUQUNKHBDtwKRVpqU6ZNmWIJ0oX4iBIjQDaYZuLwcJoSoBICBoyi8AA5AAAlYghQBIyhqswpIQrcaAQBArDbJctxELJpKCOIrBwNDJIZmAoOwvCSJorwGA6FBDN1UiuJ9I9OiyXihJEr4KPzEgoBUXBcC1HgEIgPi+JAA"} import { transform } from '@studiometa/js-toolkit-v4/utils'; @@ -22,11 +22,11 @@ import { transform } from '@studiometa/js-toolkit-v4/utils'; transform({ x: 10, y: 20, scale: 1.5, rotate: 45 }); ``` -`TransformProps` takes `x`, `y`, `z`, `rotate`, `rotateX`, `rotateY`, `rotateZ`, `scale`, `scaleX`, `scaleY`, `scaleZ`, `skew` and the rest of the family. `TRANSFORM_PROPS` is the ordered list of the keys it reads. +Builds a `transform` value from named parts, so a component composes a transform instead of assembling a string. `TransformProps` takes `x`, `y`, `z`, `rotate`, `rotateX`, `rotateY`, `rotateZ`, `scale`, `scaleX`, `scaleY`, `scaleZ`, `skew` and the rest of the family. **The order is fixed by `TRANSFORM_PROPS`, not by the object.** Transform functions do not commute, so two components building "the same" transform from differently-ordered literals must still get the same matrix. -## `matrix` +### matrix ```ts matrix(props?: MatrixProps): string @@ -41,7 +41,17 @@ matrix({ scaleX: 2, translateX: 10 }); A `matrix()` string. Reach for it when a value has to be interpolated as a matrix rather than as separate functions. -## `getOffsetSizes` +### TRANSFORM_PROPS + +```ts +const TRANSFORM_PROPS: readonly (keyof TransformProps)[]; +``` + +The ordered list of keys `transform()` reads. + +## Measuring + +### getOffsetSizes ```ts getOffsetSizes(element: HTMLElement): { x, y, width, height, top, right, bottom, left } @@ -62,14 +72,18 @@ That is exactly what a drag or a tilt needs: the layout box is the frame of refe Call it from the `read` phase — [`$read()`](/api/instance-methods.html#read-and-write) — so it batches with every other measurement of the frame. ::: -## `setClassesOrStyles` +## Applying + +### setClassesOrStyles ```ts -setClassesOrStyles(el: HTMLElement, value: string | string[] | Partial | undefined, method?: 'add' | 'remove'): void +setClassesOrStyles( + el: HTMLElement, + value: string | string[] | Partial | undefined, + method?: 'add' | 'remove', +): void ``` -Applies a value that may be **either** classes or inline styles, which is what lets [`transition()`](./transitions.html) take one option in both forms: - ```js twoslash // @twoslash-cache: {"v":1,"hash":"922eff9993f504dc1ead2fb24a0c4110ba1104c8ee56fa19ef3fe0d7208624ab","data":"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="} import { setClassesOrStyles } from '@studiometa/js-toolkit-v4/utils'; @@ -82,6 +96,8 @@ setClassesOrStyles(el, { opacity: '0' }); // inline styles setClassesOrStyles(el, 'is-active', 'remove'); // undo it ``` +Applies a value that may be **either** classes or inline styles, which is what lets [`transition()`](./transitions.html) take one option in both forms. + An `undefined` value does nothing, so a caller with an optional state does not have to branch. ## What is not here diff --git a/packages/v4/docs/utils/dom.md b/packages/v4/docs/utils/dom.md index 1e0ccee7..8cf567ab 100644 --- a/packages/v4/docs/utils/dom.md +++ b/packages/v4/docs/utils/dom.md @@ -7,7 +7,7 @@ import { createElement, selectorFor } from '@studiometa/js-toolkit-v4/utils'; [[toc]] -## `createElement` +## createElement ```ts createElement(tag?, children?): HTMLElement @@ -32,7 +32,7 @@ createElement('button', { class: 'btn', data: { component: 'Action' } }, 'Send') The `data` key is why this exists rather than three lines of `document.createElement`: building an element that carries `data-component` and a few `data-option-*` is the common case, and spelling each `setAttribute()` out loses the shape. -## `selectorFor` +## selectorFor ```ts selectorFor(name: string): string diff --git a/packages/v4/docs/utils/easings.md b/packages/v4/docs/utils/easings.md index 1db4e601..fbf368fe 100644 --- a/packages/v4/docs/utils/easings.md +++ b/packages/v4/docs/utils/easings.md @@ -11,22 +11,20 @@ An `EasingFunction` takes a `0 → 1` progress and returns a shaped `0 → 1` va [[toc]] -## The 24 functions +## Choosing one -Eight curves, three directions each: +Eight curves, three directions each. Every `out` and `in-out` is derived from its `in`. -| Curve | In | Out | In-out | -| ------ | ------------- | -------------- | ---------------- | -| linear | `easeLinear` | — | — | -| quad | `easeInQuad` | `easeOutQuad` | `easeInOutQuad` | -| cubic | `easeInCubic` | `easeOutCubic` | `easeInOutCubic` | -| quart | `easeInQuart` | `easeOutQuart` | `easeInOutQuart` | -| quint | `easeInQuint` | `easeOutQuint` | `easeInOutQuint` | -| sine | `easeInSine` | `easeOutSine` | `easeInOutSine` | -| circ | `easeInCirc` | `easeOutCirc` | `easeInOutCirc` | -| expo | `easeInExpo` | `easeOutExpo` | `easeInOutExpo` | - -`easeLinear` is the identity, and it exists so a call site can name "no easing" rather than branch on `undefined`. +| Curve | In | Out | In-out | +| ------ | ----------------------------- | ------------------------------- | ----------------------------------- | +| linear | [`easeLinear`](#easelinear) | — | — | +| quad | [`easeInQuad`](#easeinquad) | [`easeOutQuad`](#easeoutquad) | [`easeInOutQuad`](#easeinoutquad) | +| cubic | [`easeInCubic`](#easeincubic) | [`easeOutCubic`](#easeoutcubic) | [`easeInOutCubic`](#easeinoutcubic) | +| quart | [`easeInQuart`](#easeinquart) | [`easeOutQuart`](#easeoutquart) | [`easeInOutQuart`](#easeinoutquart) | +| quint | [`easeInQuint`](#easeinquint) | [`easeOutQuint`](#easeoutquint) | [`easeInOutQuint`](#easeinoutquint) | +| sine | [`easeInSine`](#easeinsine) | [`easeOutSine`](#easeoutsine) | [`easeInOutSine`](#easeinoutsine) | +| circ | [`easeInCirc`](#easeincirc) | [`easeOutCirc`](#easeoutcirc) | [`easeInOutCirc`](#easeinoutcirc) | +| expo | [`easeInExpo`](#easeinexpo) | [`easeOutExpo`](#easeoutexpo) | [`easeInOutExpo`](#easeinoutexpo) | ## Deriving one @@ -53,7 +51,23 @@ const easeInOutBack = createEaseInOut(easeInBack); That is why only the eight `in` functions are written out and the other sixteen are derived: an `out` is an `in` run backwards, and writing it twice is how the two drift apart. -## Usage +### createEaseOut + +```ts +createEaseOut(easeIn: EasingFunction): EasingFunction +``` + +Mirror an `in` curve into its `out`. + +### createEaseInOut + +```ts +createEaseInOut(easeIn: EasingFunction): EasingFunction +``` + +Compose an `in` curve into its symmetric `in-out`. + +## Using one ```js twoslash // @twoslash-cache: {"v":1,"hash":"09933edf6d2a922b7f27e4d7327afd8598ff86255acb641b0754c36ba307675c","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIBjCGDhpeMLjADyAVzQBhaQCN2/RLwCiXdmADmAMWlh+adkIA6YdgFssEUqPFwpshcv6UQI5vaQBOKqwwumj4SACMAExUaN46MAyIII7O8koqHhxguIgADFT8+N7MxmR+AL4U6NjZBMSl0XQJICwcXHwAZobGpmC8gaRYjFbaamDSVopkFLxWzLSj45Ok06TMJhALE2Tcm0sW1rb2fWRYHlAQ/AiJAJJgNAMQrGswvMy8q+u8k2gA7jBBvF+EC+EEMUDgADoPF4fIgIgBmAJBHQhJAAFmisXieH6pwC2myeRABSKJXIcPhFSqODwhBI5Aa9DwnSM616tjg7DZAEE0IwsKQIDpSPA4GpmGAMDteGMtgzPDFYQAOImBYKhRBhImKuJNDlcnq8jIEpBRYmFVZk8qVajVWl1eU0JmJflFKzxba8AVCkVwMWvSXQxUJXxhJHqpBKzGkXV4b3C0XGrKm/IW4r3U05Km2mmJOn1aiNJhsTg8Y4DIYjGWLKYzOa7WsfUwN0jS2V7Sw2OyiXFnC5XEC3e62J40V7vNamL7xP4AoEgsGQoPeBJhSLhlEajHULFNXv45OIRHm0kZxC+bOYXO1ekeJ1NFqlvjJGSpNz8wUJv0tts10j7LsjhfFw0ncKgYVXCIAFYN1RTUzR1bFEmAt90gPbJjxJS0zwiCJLztPMHTvIsXSwN0PVbL1P19f0JQwZdYTCeEoxANVN0jaNY0SeMaKTDDU1PUojygsoAF18mgGoDm7XhgDECRX1cFRplxXgyl4dpBSsXgAHIAAERGkKBTHdGIAHoACs4AAWjQCBHgAay5ayiDRMzZHYVg4B0gBuCwLBZbohC9CBOR5PkeNFPhgAsXh3niaRSF6XFGDyXg0RyNKUKU/gPx9KLuD8sAyg8UzmCQUBGiCTkhDwKyQDKMogA="} @@ -66,9 +80,147 @@ function positionAt(progress) { An easing shapes a **progress**, so it pairs with a value that already runs `0 → 1`: a [`useScrollProgress()`](/api/services/useScrollProgress.html) subscriber, or a `map()` of anything else. -## What they are not for +## They do not animate -They do not animate. There is no clock here — an easing is a pure function of a progress you already have. +There is no clock here — an easing is a pure function of a progress you already have. - For a value chasing a target frame by frame, use [`damp()` or `smoothTo()`](./motion.html). - For time-based playback, stagger and sequencing, that is the separate `ui-animation` package. `tween` and `animate` are not shipped. + +## The functions + +Each takes a `0 → 1` progress and returns the shaped value. + +### easeLinear + +```ts +easeLinear(progress: number): number +``` + +The identity. It exists so a call site can name "no easing" rather than branch on `undefined`. + +### easeInQuad + +```ts +easeInQuad(progress: number): number +``` + +### easeOutQuad + +```ts +easeOutQuad(progress: number): number +``` + +### easeInOutQuad + +```ts +easeInOutQuad(progress: number): number +``` + +### easeInCubic + +```ts +easeInCubic(progress: number): number +``` + +### easeOutCubic + +```ts +easeOutCubic(progress: number): number +``` + +### easeInOutCubic + +```ts +easeInOutCubic(progress: number): number +``` + +### easeInQuart + +```ts +easeInQuart(progress: number): number +``` + +### easeOutQuart + +```ts +easeOutQuart(progress: number): number +``` + +### easeInOutQuart + +```ts +easeInOutQuart(progress: number): number +``` + +### easeInQuint + +```ts +easeInQuint(progress: number): number +``` + +### easeOutQuint + +```ts +easeOutQuint(progress: number): number +``` + +### easeInOutQuint + +```ts +easeInOutQuint(progress: number): number +``` + +### easeInSine + +```ts +easeInSine(progress: number): number +``` + +### easeOutSine + +```ts +easeOutSine(progress: number): number +``` + +### easeInOutSine + +```ts +easeInOutSine(progress: number): number +``` + +### easeInCirc + +```ts +easeInCirc(progress: number): number +``` + +### easeOutCirc + +```ts +easeOutCirc(progress: number): number +``` + +### easeInOutCirc + +```ts +easeInOutCirc(progress: number): number +``` + +### easeInExpo + +```ts +easeInExpo(progress: number): number +``` + +### easeOutExpo + +```ts +easeOutExpo(progress: number): number +``` + +### easeInOutExpo + +```ts +easeInOutExpo(progress: number): number +``` diff --git a/packages/v4/docs/utils/focus.md b/packages/v4/docs/utils/focus.md index 7ffaa821..97d4912e 100644 --- a/packages/v4/docs/utils/focus.md +++ b/packages/v4/docs/utils/focus.md @@ -7,19 +7,7 @@ import { saveActiveElement, trapFocus, untrapFocus } from '@studiometa/js-toolki The three calls a modal surface needs. -## The three - -```ts -saveActiveElement(): void -trapFocus(el: HTMLElement, event: KeyboardEvent): void -untrapFocus(): void -``` - -| Function | Does | -| ---------------------- | ------------------------------------------------------------- | -| `saveActiveElement()` | remembers what had focus, so `untrapFocus()` can give it back | -| `trapFocus(el, event)` | keeps `Tab` and `Shift+Tab` inside `el` | -| `untrapFocus()` | releases the trap and restores focus to the saved element | +[[toc]] ## Usage @@ -63,6 +51,32 @@ mounted() { `trapFocus()` calls `preventDefault()` on the event it is handed, and a passive listener cannot. See [`useKey()`](/api/services/useKey.html#the-listeners-are-neither-passive-nor-capturing). ::: +## The three + +### saveActiveElement + +```ts +saveActiveElement(): void +``` + +Remembers what had focus, so `untrapFocus()` can give it back. + +### trapFocus + +```ts +trapFocus(el: HTMLElement, event: KeyboardEvent): void +``` + +Keeps `Tab` and `Shift+Tab` inside `el`. + +### untrapFocus + +```ts +untrapFocus(): void +``` + +Releases the trap and restores focus to the saved element. + ## The saved element is shared across copies Through the shared runtime, for the same reason the scroll lock counts: **there is one focus per document**. Two independently evaluated copies of the package must not each think they own it. diff --git a/packages/v4/docs/utils/history.md b/packages/v4/docs/utils/history.md index f0c5398f..e63b1748 100644 --- a/packages/v4/docs/utils/history.md +++ b/packages/v4/docs/utils/history.md @@ -9,11 +9,17 @@ import { } from '@studiometa/js-toolkit-v4/utils'; ``` -## `historyPush` and `historyReplace` +[[toc]] -```ts -historyPush(options: HistoryOptions, data?: unknown, title?: string): void -historyReplace(options: HistoryOptions, data?: unknown, title?: string): void +## Writing the URL + +```js twoslash +// @twoslash-cache: {"v":1,"hash":"1c4232b87df279fc3201f1da1afb7ef4e0fa5b9c755d8f52a170390ec908e116","data":"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"} +import { historyPush, historyReplace } from '@studiometa/js-toolkit-v4/utils'; + +historyPush({ path: '/products', search: { page: 2, sort: 'price' } }); +historyReplace({ search: { page: 3 } }); +historyPush({ hash: 'section-2' }); ``` ```ts @@ -24,30 +30,35 @@ interface HistoryOptions { } ``` -```js twoslash -// @twoslash-cache: {"v":1,"hash":"1c4232b87df279fc3201f1da1afb7ef4e0fa5b9c755d8f52a170390ec908e116","data":"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"} -import { historyPush, historyReplace } from '@studiometa/js-toolkit-v4/utils'; +**Each part is optional, and an omitted part is kept.** `historyReplace({ search })` keeps the path and the hash; `historyPush({ hash })` keeps the path and the query string. -historyPush({ path: '/products', search: { page: 2, sort: 'price' } }); -historyReplace({ search: { page: 3 } }); -historyPush({ hash: 'section-2' }); +That is the whole reason these exist over `history.pushState()`: rebuilding a URL from `location` by hand is where a filter update loses the hash, or a hash update drops the query string. + +### historyPush + +```ts +historyPush(options: HistoryOptions, data?: unknown, title?: string): void ``` -**Each part is optional, and an omitted part is kept.** `historyReplace({ search })` keeps the path and the hash; `historyPush({ hash })` keeps the path and the query string. +Adds a history entry. This is what makes a state navigable with the back button. -That is the whole reason these exist over `history.pushState()`: rebuilding a URL from `location` by hand is where a filter update loses the hash, or a hash update drops the query string. +### historyReplace + +```ts +historyReplace(options: HistoryOptions, data?: unknown, title?: string): void +``` -| Function | Effect | -| ------------------ | ------------------------ | -| `historyPush()` | adds a history entry | -| `historyReplace()` | replaces the current one | +Replaces the current entry. This is what keeps a scroll position or a tab index out of the history. -`push` is what makes a state navigable with the back button. `replace` is what keeps a scroll position or a tab index out of the history. +## Building the query string -## `objectToURLSearchParams` +### objectToURLSearchParams ```ts -objectToURLSearchParams(object: Record, defaultSearch?: string): URLSearchParams +objectToURLSearchParams( + object: Record, + defaultSearch?: string, +): URLSearchParams ``` ```js twoslash diff --git a/packages/v4/docs/utils/is.md b/packages/v4/docs/utils/is.md index 58dc2a37..9b22f7b8 100644 --- a/packages/v4/docs/utils/is.md +++ b/packages/v4/docs/utils/is.md @@ -7,15 +7,7 @@ import { isDefined, isNumber, isObject, isString } from '@studiometa/js-toolkit- Each one narrows the type. -| Function | Signature | -| ------------------- | ------------------------------------------ | -| `isNull(value)` | `value is null` | -| `isDefined(value)` | `value is T` — for `T \| undefined` | -| `isString(value)` | `value is string` | -| `isNumber(value)` | `value is number` | -| `isBoolean(value)` | `value is boolean` | -| `isFunction(value)` | `value is (...args: unknown[]) => unknown` | -| `isObject(value)` | `value is Record` | +[[toc]] ## Usage @@ -36,13 +28,53 @@ function first(items: (T | undefined)[]): T[] { `isDefined` as a `filter` predicate is the case that earns the export: it is the one narrowing TypeScript will not do from a truthiness check. -## `isNumber` rejects `NaN` +## The guards -```js -isNumber(NaN); // false +### isNull + +```ts +isNull(value: unknown): value is null +``` + +### isDefined + +```ts +isDefined(value: T | undefined): value is T +``` + +Narrows `T | undefined` to `T`. Passing it by reference to `filter` is what it is for. + +### isString + +```ts +isString(value: unknown): value is string +``` + +### isNumber + +```ts +isNumber(value: unknown): value is number +``` + +**It rejects `NaN`**, because a `NaN` that passes a number check is a bug that surfaces three functions later. + +### isBoolean + +```ts +isBoolean(value: unknown): value is boolean ``` -Because a `NaN` that passes a number check is a bug that surfaces three functions later. +### isFunction + +```ts +isFunction(value: unknown): value is (...args: unknown[]) => unknown +``` + +### isObject + +```ts +isObject(value: unknown): value is Record +``` ## What is not here diff --git a/packages/v4/docs/utils/load.md b/packages/v4/docs/utils/load.md index 0d34e568..9bf9db00 100644 --- a/packages/v4/docs/utils/load.md +++ b/packages/v4/docs/utils/load.md @@ -5,15 +5,11 @@ import { loadImage, loadLink, loadScript } from '@studiometa/js-toolkit-v4/utils'; ``` -Three promises over three tags. +Three promises over three tags. Each resolves with the **element** it created, so a caller can read the loaded image's natural size, or keep the script tag to remove it later. -## The three +[[toc]] -```ts -loadImage(src: string): Promise -loadScript(src: string, attributes?: Record): Promise -loadLink(href: string, attributes: { rel: string; [key: string]: string }): Promise -``` +## Usage ```js twoslash // @twoslash-cache: {"v":1,"hash":"7fcf1e3ace01833bfe62c1f59e7e719afb04be195f7a169343154974b8b2c1a1","data":"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"} @@ -28,7 +24,23 @@ async function load() { } ``` -Each resolves with the **element** it created, so a caller can read the loaded image's natural size, or keep the script tag to remove it later. +### loadImage + +```ts +loadImage(src: string): Promise +``` + +### loadScript + +```ts +loadScript(src: string, attributes?: Record): Promise +``` + +### loadLink + +```ts +loadLink(href: string, attributes: { rel: string; [key: string]: string }): Promise +``` ## Where they belong diff --git a/packages/v4/docs/utils/math.md b/packages/v4/docs/utils/math.md index 5ba0db2f..30bcbe3d 100644 --- a/packages/v4/docs/utils/math.md +++ b/packages/v4/docs/utils/math.md @@ -11,11 +11,6 @@ Values in, values out. For anything with a time dimension — damping, springs, ## Ranges -```ts -clamp(value: number, min: number, max: number): number -clamp01(value: number): number -``` - ```js twoslash // @twoslash-cache: {"v":1,"hash":"2f9240bc13defa62aed44e329e826140ad1118316f38206277e8f3e4a4c41f24","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvEa2YBbLIyJtBMRLzCC5AIzIVec9mHWadeg81omtu0t2tnSAHTDsFEUmmmyFlEFAgRBEQQAGEfLF5mXmVWVV4jKN4Ac3YSKVJmMGSYADo/ODRmTyQATipWGGy0fCQAVioi0hyGEJl5LD8OMFxEAAYqEXxi5jEyMoBfCnRsXoJicca6VpAWDi4+IVFxSW8OvoBGJRU1DRsyezPHFzcsDy92hUO/AKC8cI6k2PjEmphePqAZAIDrxMtk8gUmq0DgA2CpVZI1JAAdkaxRaeEeWGeFSMvQGICGIzG5EQsKmMxweEIJHIS3oTDYnB4ewUxzip1Mtn0hmMV25Fis/IuDlsN3cnlZnSor2CYQiXxOCSk0VS6VBWRy+SohWKrRhqJAlWqtUQDWo6JgKyxXTxSAATINhpkSUg+hTqLNqQs6dRloz1iysezVKLzLyw6QeZZI5cuWRxXdJTaZYE5R8FIqOcqkmqqhrwdqQLqSohSgd4Sb6mjmlbMRFbT0kABmJ3Emik93TT1UkI0xZ+hkhNbMvhYw4hznnOyRxP3KU4/xp94K6Lff6/fD/IEgsFayF6pAHA4AFkriNNhqaGLaEUX3V6Z8JztGHbdEwAuoNoHNbvPgFK+jjiCEwCKQEByLwADkAAChSCFAEhyFazAAPQAFZwAAtGgEAQKwADW7BoFhRAnqhgjiKwcBQQA3C4LjBgcDQAvoBx9NwtG8KhqG8OxjERIwWEsQMfEcVxPEAgJ+xHAcuR1Jx3G8QcfjIUUSCgMsVRwBIYB4JhIATBMQA"} import { clamp, clamp01 } from '@studiometa/js-toolkit-v4/utils'; @@ -25,15 +20,24 @@ clamp(-5, 0, 10); // 0 clamp01(1.5); // 1 ``` -`clamp01` is the case that comes up in every progress calculation, so it has its own name and no arguments to get the wrong way round. +### clamp -## Interpolation +```ts +clamp(value: number, min: number, max: number): number +``` + +Keep a value inside a range. + +### clamp01 ```ts -lerp(min: number, max: number, ratio: number): number -map(value: number, inputMin: number, inputMax: number, outputMin: number, outputMax: number): number +clamp01(value: number): number ``` +Keep a value inside `0 → 1`. It is the case that comes up in every progress calculation, so it has its own name and no arguments to get the wrong way round. + +## Interpolation + ```js twoslash // @twoslash-cache: {"v":1,"hash":"ab7fb867894ecfd32410afb7d10f1bd7e91baf2058823a960806ab93ef4169b7","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvVmSyMAtuzCJeYQfIBGZCr3nNaKtZu29SzcREPqtpbleOkAOmHbysEUmmmzKIKBBEERBAASTAaUndWcxheZlNzCV4tNAB3GBgpNIhkiGEoOAA6Xzg0Zk8kAE4qGTAAczR8JAAWKjLSOpgGYJlI3w4wXEQABioRfHLmMTIqgF8KdGwhgmIZtrpukBYOLj4hUQspPTkiNkEYexsdJSxBNABZJUuTG7v7/WfSHTy0W4en1TWEw/P7vAyAhx2CE2ZyudyeXTMLC+fyBPDvLBxXinVjnASkCDyXiSWJmeqxSRoHLMSCNMjFKilcrdACMAFYapkGk1EABmNrlTqbY79JRDUYgcaTabkRDs+aLHB4QgkcjrehMNicHjeSIKAFGK6I8GGkxmCyfKGmpwuNweLy9ZFUVFBULhWQQaI0LHmpIpdKZXjZXL5IoldrdABsACZOfVGi0BR0unhHaLBkhY5KJmYZUhhgrqEtlas1dQNpqdjrjowcedPtcwKCDUCvrxXg8PtDgXdm8pu22QW8u9ara3YXaESLnQFXRisXXYvwCUSSQlycTwtTafh6eHmUgABwAdjj3KQ/OogpTwWnIAGQ0vUtzEXzswAumNoMs4fbeMBdSwHRjl4WZ8UJXgAHIAAFSkEKAJHkLpmAAegAKzgABaKlPQAa3YNBMKIZoULudhWDgSCAG5nGcR1GFGXgWWGRjhkKNluCo3gUJQ3g2WGZwaw5XhGOYnRRJYzjuN4/jfCQsokFADZMjgCQwDwDCQFmWYgA="} import { lerp, map } from '@studiometa/js-toolkit-v4/utils'; @@ -42,15 +46,24 @@ lerp(0, 100, 0.5); // 50 map(5, 0, 10, 0, 100); // 50 ``` -`lerp` takes a ratio; `map` re-scales one range onto another. `map` is `lerp` with the input range worked out for you. +### lerp -## Wrapping and folding +```ts +lerp(min: number, max: number, ratio: number): number +``` + +Interpolate between two values by a `0 → 1` ratio. + +### map ```ts -wrap(value: number, min: number, max: number): number -fold(value: number, min: number, max: number): number +map(value: number, inputMin: number, inputMax: number, outputMin: number, outputMax: number): number ``` +Re-scale one range onto another. It is `lerp` with the input range worked out for you. + +## Wrapping and folding + ```js twoslash // @twoslash-cache: {"v":1,"hash":"aa94f2881f80c2fa5d7fef9f9d0b38791f8e8b046fe96bb1f589378639f3c919","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwAiKyiMibQTES8wggLYAjMhV7r2YFWq069zWkY3bS3SydIAdMO3VYIpNNNmUQUCCIREEAAxGSheZl4FViVeTWYRAGsIsHD+dzR8XgMI3lJmMABzZWzPTQhhEXheCH5+OIhMhuEoOAA6Hzg0Zg8kAE4qVhgizKQAFipu0mKGIPTvQYNcRAAGKhF8HoSackQ+gF8KdGxlgmIyHxp6JjZOHgFK8UleAHd8rHlFEuNrXX1DVRWUzqcx2ay2QH2JwuNweV7vHx+AJ4ADq71y0ViOUi+SKJXYniGzBIcDiGBqYBgzVSKXCIgg6mq8SSZN4mSpjXwZA6VC6PVmAEYAKyDYaFUaICbUHozPBvZhYHwcSlINYgDZbMQXRDCw7HHB4QgkciTOizEAsDhcPjyj6Y75A0h/Axg4GgyHg12OZyuDLwhWI/yBEBohUYr7ZKQ4grFFQE3hEkmsyRU8otWm8emM0nM5Kacnsmrs0g8kB83qIABsAGZRSN8ONJjKYObbUqlkgAEzrTb5LW7FZ66gnQ3nE3UM03K33eZye1e50An5uiwesgQ5femF+2eB5FBUKycMxVMJZIFNIZLLYvIx/FlCqiaq1erlJpp1LtTpTQUrAAcdbig2kpNtMLZ4Luiwqogtbqr22zaoOAC66zQKc25wsAXhQLotq8PsAikAyvAAOQAAJdIIUASIy3QAPQAFZwAAtGgEAyIkBLMUQYx0YI4isHAJEANxOE4tqMAKAq6GsvACis3DCbwdF0XJvC8IAKASlJmDJMmeeSPlATizpJ0m8LJ8mKcpql9OpWnxh+VQ5mePi0cwSCgGawxwBIYB4ExID7PsQA==="} import { fold, wrap } from '@studiometa/js-toolkit-v4/utils'; @@ -59,15 +72,26 @@ wrap(11, 0, 10); // 1 — it comes back round fold(11, 0, 10); // 9 — it bounces back ``` -`wrap` is a carousel that loops; `fold` is a value that reverses at the edges. Two behaviours, two names, no flag. +Two behaviours, two names, no flag. -## Rounding and averaging +### wrap ```ts -round(value: number, decimals?: number): number -mean(numbers: readonly number[]): number +wrap(value: number, min: number, max: number): number +``` + +A carousel that loops: past the end, it comes back round to the start. + +### fold + +```ts +fold(value: number, min: number, max: number): number ``` +A value that reverses at the edges rather than looping. + +## Rounding and averaging + ```js twoslash // @twoslash-cache: {"v":1,"hash":"b0ee82bbdee72d859a0f5123636737b5fa11d3d66e2ac5044824a021a7e60637","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvALYxmYRmEHSARmTiJepOVEmsMvJarLIAut01G1pADph20rBFJoZcsJRC6RCRCAAq+DC8zKTsaPiy4iJu8rwQ/LwRwQDm7CRSVuoAdLwAglIwjpi8HHCuzCSkzCnwSRC8AAYADI3ZnuWhDIgAnFSsMGApEUgALFRoobXdILLynhxguIjNVCL4ocxiZEg9AL4U6NjLBMQ7E3QzLBxcfEKi4pJaEMJQjERsgjCWytYUvLARA42HAAPw/YykCyGX5kOwOJwuZ6vTzeXwgABKLzAUBCvA+rC+9SSQV4aQyMMh8USgOBrDg7SonRcSAAjABWfqDYb4JCc6hTGAzUjYqALdhLJCrEDrTbbciIDkHI44PCEKqeGj0JhsTg8ZE496fb6Uv4AmBA6Qg8GmsjQrK2eyOZyuEUoqhovBY154glEtANZJk9KDW2kanmy0gxkgZndABs4xAAyGI0Q/MmpGmeDdOPFksQACY1htqvKpcrqMc1WdyBdtX5rnq+HMFA6NFodHoDA6zPbYY6ES7Yh4PRAfHhAsFQuFIkL2DFWxGg+TQ+3cgVeEUsCUyhUqjU6gGmq0Y3HdtKUzyxhNBTNW/nlgBmEtymgK5p7UxraAnIdI4AR3+XNcT2AQRWkXgAHIAAFykEKAJCiZgAHoACs4AAWgDCBWAAa3CTCiFGFDBHEekoIAbjsOwQMYVlskLJ9Rk5XhC24SjeBQlDeAYpi7FbRhkFZf5i14F9eFGcxOO4tjsnZTxkKQUBLkGOAJA8PwMJAPY9iAA="} import { mean, round } from '@studiometa/js-toolkit-v4/utils'; @@ -76,7 +100,25 @@ round(1.2345, 2); // 1.23 mean([1, 2, 3, 4]); // 2.5 ``` -## `createRange` +### round + +```ts +round(value: number, decimals?: number): number +``` + +Round to a number of decimals. + +### mean + +```ts +mean(numbers: readonly number[]): number +``` + +The arithmetic mean of a list. + +## Series + +### createRange ```ts createRange(min: number, max: number, step: number): number[] diff --git a/packages/v4/docs/utils/motion.md b/packages/v4/docs/utils/motion.md index 79b9ef97..fb6b91be 100644 --- a/packages/v4/docs/utils/motion.md +++ b/packages/v4/docs/utils/motion.md @@ -13,7 +13,9 @@ That is what makes a factor mean the same thing at 60 Hz and at 120 Hz, and it i [[toc]] -## `damp` +## Damping + +### damp ```ts damp(targetValue: number, currentValue: number, factor: number, elapsed: number, precision?: number): number @@ -34,8 +36,6 @@ useRaf().subscribe(({ delta }) => { `factor` is **the fraction of the gap that closes per reference frame**, so it is stable for every value a caller can pass. `precision` defaults to `0.01`: below it, the value snaps to the target. -`DEFAULT_DAMP_FACTOR` is `0.85`. `clampDampFactor(factor)` keeps a factor in the usable range. - ::: warning v3's `damp()` had no `elapsed` ```js @@ -46,7 +46,25 @@ damp(target, current, 0.1, delta); // [!code ++] A factor without a time is a factor that means something different on every display. ::: -## `spring` +### clampDampFactor + +```ts +clampDampFactor(factor: number): number +``` + +Keeps a factor in the usable range. + +### decayOver + +```ts +decayOver(retained: number, elapsed: number): number +``` + +The decay of an elapsed time. + +## Springs + +### spring ```ts spring( @@ -77,7 +95,9 @@ It returns the pair, because a spring's state **is** the value and its velocity `precision` defaults to `1e-4`. -## `smoothTo` +## Smoothing + +### smoothTo ```ts smoothTo(start?: number, options?: SmoothToOptions): SmoothTo @@ -106,7 +126,7 @@ const unsubscribe = x.subscribe((value) => { x.destroy(); // release the frame subscription and every subscriber ``` -### Several channels on one subscription +#### Several channels on one subscription ```js twoslash // @twoslash-cache: {"v":1,"hash":"acf7aebb5693a43be004080a99d695f571563b20706f70533b70a5c1fda971ac","data":"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"} @@ -127,7 +147,7 @@ As a service hands the same props. Treat it as read-only, and copy it with `{ .. The mode — `spring`, `stiffness`, `mass` — belongs to the **instance**, not the channel. -### `damping` accepts a function +#### `damping` accepts a function ```js smoothTo({ x: 0, y: 0 }, { damping: (key) => (key === 'x' ? 0.9 : 0.7) }); @@ -141,30 +161,68 @@ It is read on **every frame and for every channel**, which matters for three rea A number stays a number. -### `precision` +#### `precision` Defaults to the default of the function each mode wraps — `0.01` damping, `1e-4` springing — so converting a raw `damp()` call to the helper does not move where it snaps. ## Inertia -The family a coast is built from, and what [`useDrag()`](/api/services/useDrag.html) uses: +The family a coast is built from, and what [`useDrag()`](/api/services/useDrag.html) uses. + +**`inertiaStep()` integrates the decay across the step**, so any sequence of frames sums to `velocity · τ` exactly — a coast lands in the same place whatever the frame rate did on the way. `inertiaFinalValue()` is what lets a carousel know which slide a fling is heading for **before** the coast starts. -| Function | Does | -| ------------------------------------------------ | -------------------------------------------------- | -| `decayOver(retained, elapsed)` | the decay of an elapsed time | -| `inertiaDecay(dampFactor, elapsed)` | the same with the tighter clamp a coast needs | -| `inertiaTimeConstant(dampFactor)` | `τ = INERTIA_FRAME / ln(1 / damp)` | -| `inertiaStep(velocity, dampFactor, elapsed)` | the distance travelled across the step | -| `inertiaFinalValue(value, velocity, dampFactor)` | where it will come to rest: `value + velocity · τ` | +### inertiaDecay + +```ts +inertiaDecay(dampFactor: number, elapsed: number): number +``` + +`decayOver` with the tighter clamp a coast needs. + +### inertiaTimeConstant + +```ts +inertiaTimeConstant(dampFactor: number): number +``` -**`inertiaStep()` integrates the decay across the step**, so any sequence of frames sums to `velocity · τ` exactly — a coast lands in the same place whatever the frame rate did on the way. +`τ = INERTIA_FRAME / ln(1 / damp)`. -`inertiaFinalValue()` is what lets a carousel know which slide a fling is heading for **before** the coast starts. +### inertiaStep + +```ts +inertiaStep(velocity: number, dampFactor: number, elapsed: number): number +``` + +The distance travelled across the step. + +### inertiaFinalValue + +```ts +inertiaFinalValue(value: number, velocity: number, dampFactor: number): number +``` + +Where it will come to rest: `value + velocity · τ`. ## Constants -| Constant | Value | -| --------------------- | ---------------------------------------------- | -| `INERTIA_FRAME` | `16.67` — the reference frame, in milliseconds | -| `DEFAULT_DAMP_FACTOR` | `0.85` | -| `MAX_SPRING_RATIO` | the clamp on `stiffness / mass` | +### INERTIA_FRAME + +```ts +const INERTIA_FRAME: number; +``` + +`16.67` — the reference frame, in milliseconds. + +### DEFAULT_DAMP_FACTOR + +```ts +const DEFAULT_DAMP_FACTOR = 0.85; +``` + +### MAX_SPRING_RATIO + +```ts +const MAX_SPRING_RATIO: number; +``` + +The clamp on `stiffness / mass`. diff --git a/packages/v4/docs/utils/objects.md b/packages/v4/docs/utils/objects.md index d26eeb15..7b063a28 100644 --- a/packages/v4/docs/utils/objects.md +++ b/packages/v4/docs/utils/objects.md @@ -7,7 +7,9 @@ import { deepmerge, random, randomInt, randomItem } from '@studiometa/js-toolkit [[toc]] -## `deepmerge` +## Merging + +### deepmerge ```ts deepmerge(...layers: Record[]): Record @@ -29,40 +31,43 @@ That last rule is the one worth knowing: an array is a value, not a structure to A utility is judged by consumer need, not by whether core calls it. Layering a default config under an author's config is the case every component author meets, and getting the array rule wrong is exactly how a hand-rolled merge misbehaves. ::: -## `random` and `randomInt` - -```ts -random(a: number, b?: number): number -randomInt(a: number, b?: number): number -``` +## Random ```js twoslash -// @twoslash-cache: {"v":1,"hash":"e6f4ef7657f64c4b7d3e04ee3f33001fe2719938fda597f27d5684b366745491","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvUszBQIAWxaJeYQQoBGZCrw0B+FWs1luh9VtIAdMOwVYIpNNNnyFlEPJEJEIAILO5RVVzMl0YNAB3GBgpNHwYXkiIXQhhKDgdOIS4GBFJKF5YfmZBVnEwAHNE5IADAAYagDp3ODRmRyQATipWGIq4pAA2KjbSCvC8GUC3HvYwXEQ6qhF8duYxMi6AXwp0bAWCYk2RugYfFg4uPiFRcUkA1wBJMDRlYONSHX0zD9N3i2stnsjgeimeDConm8flBCl4cxo41IYUi0Vi8USEWSGlScgyKTi8NErEEsAKLkSGJyeTk1iKJTKcyqaFqDUa1msAFESKQMETEaE5pSElNxvC4LwYABHQRsVh8jgAaxg8savAASrioEzeMxYQJWBBmGhAWBWjBmAVACgEvAi+HYK3FttWTiIAGZCuxrbwKuwSBKspjsVqJatWPxdbwVrIRAkLStmlRWu0zgBGADsPT6A0Q3Wo7XGZxAUyeL3cHHmSCWIGjMg25EQGZ2exweEIPPcNHoTDYnB4sLeRgsXwM/xMPwBNjsDicJcU7iheH8c7hQ9CWlRMWFQZSaXxgep+UKMGKpXKzNZTRaozO6ervUqOeG+bGEx8K/LcwWACZlqs6zQDZ1M21D7G2RzkCc3bnL2VwDswE7aLoo5rqQfyoYC04gh+kIQF4S76qhKJRFugZJLueKZFSuRHvSZ46iyvD1FeSY3l0v4gA+/T4EMIwFm+xYuPOsyVogbp/ms9ZViBmCtj47bHNQpw9pc/YruCg4hJ8yGIWhumYcCs5CQo4ILnh0LLsZ/IwEixFotu5E4nuXwQIScwiCSZK6nI26HrScgngy57VExbIcmA3JkHyCI2YK6IirIYrsBK0qyqw8q8EqKoYGqmppDqeorgaRomjYZo0JavA2naDr4E6drGrw7qet6vr+g5WIUekvBhhGerRqIcbrPgiYgMmHSNhmWaPjxuZ8a+RbqWWIkLAALBJAGbIsMlgfJEGdspMGqXwS2vAhY7ad8F3oVpBkzrCpm4fhPiWdM1m2RuJHxTuTmUQSdXuZ5MDkj5B40f5dGMpUIXMeyYBcjy0UvLFyJCoGooJMlkoynKCrsMqqoalqBX6vwhrGqa5qVdV9qOljDWuh62qtX68AdcGe49WwfVRqsg2SsNo3jWmACs62cdms15qMhaTMZj2cV+SAixt6yAdJAC6yzQAcQL3cAsI6KdvBbAIpBBAA5AAAq0pISAo4TMAA9AAVnAAC0LIQKwirsGg7tEKtTuCOIrBwBbADc4UrowqZ1NwEe8E7Tu8HUvCAEmEvBx9YMcq1n8eJ8nvAixn+c5/LLyxwXScp7I71IWnmfZ2Ap2MHnccJzX3n19pJdN3U7gO20SCgKcMRwBIYB4G7IBbFsQA="} -import { random, randomInt } from '@studiometa/js-toolkit-v4/utils'; +// @twoslash-cache: {"v":1,"hash":"97d2e42b178b0ca4d4da7b180a6edd87db627e098889dfee425f99da39ce769b","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvUszBQIAWxaJeYQQoBGZCrw0B+FWs1luh9VtIAdMOwVYIpNNNnyFlEPJEJEIAILO5RVVzMl0YNAB3GBgpNHwYXkiIXQhhKDgdOIS4GBFJKF5YfmZBVnEwAHNE5IADAAYagDp3ODRmRyQATipWGIq4pAA2KjbSCvC8GUC3HvYwXEQ6qhF8duYxMi6AXwp0bAWCYk2RugYfFg4uPiFRcUkA1wBJMDRlYONSHX0zD9N3i2stnsjgeimeDConm8flBCl4cxo41IYUi0Vi8USEWSGlScgyKTi8NErEEsAKLkSGJyeTk1iKJTKcyqaFqDUa1msAFESKQMETEaE5pSElNxvC4LwYABHQRsVh8jgAaxg8savAASrioEzeMxYQJWBBmGhAWBWjBmAVACgEvAi+HYK3FttWTiIAGZCuxrbwKuwSBKspjsVqJatWPxdbwVrIRAkLStmlRWu0zgBGADsPT6A0Q3Wo7XGZxAUyeL3cHHmSCWIGjMg25EQGZ2exweEIPPcNHoTDYnB4AmEYgkUhLYJoCgAPAAVAB8jHY47gKlIFvkYHlvCnyAAun8p7wAD68NIwfhzGAFRgAalTvCOpENlu4gLsDico4Uj3H7iheH8H/hcc7wjWRdVIGQMB0BxIwA2t1hoZEIBA3hWlIJlExAZMOkQAAmAAOLNKhzVNq1GQtJhcMcYBmEAKwWas4PrJB8Obah9jbe9O1OHtLn7D83iMCwvgMf4TB+AEbFfEEPx/CAvD/fVBNCLRURiYUgxSNJ8UDal8kKU8GXKZlWSaFpRjOPCcMI/p8CGEYCwmHwZNmStcOWVY6wQqtWMwVsfHbY5qG485eyuWEBJCT5dBEpTSD+WKX2Bd9KJo38fH/FLRORFSojUwMkk0vFMipXI9PpUojOqXh6lMpNzKQEiABZrJzYZ8zGRzixS8tzyQN13LWJjFh89j/M4k5uxC3i+A/cEIo+YTxLErLErfWFwVk+T0v1BEYCRFFcvRBICpxLSvggQk5hEEkyV1OR1N02k5AMiqdRZaq2Q5MBuTIPldv2oVA1FBJ2AlaVZVYDclRVDA1U1NIdT1AD+EfE0bDNGhLV4G07QdfAnTtY1eHdT1vV9f11JOkNeDDZDo1EON1nwDCsLTHC816IjbNzeyOqLWayxchZmprDz4M2YbdjYvzDg7CaiwuPsZpSr9qInVCmTnBdqKXaRV0kDcNcqHc/iNqojxPM95kvG87x5R8oGfSSkvW79ITk6EMumQDqOAu6wIgqDkSRzLGIQv29TNln6sbQY2s5myGtIhz+ZVt3aN6xAAFYBs8iW6i2bdlmgA4gTW4BYR0AW0CrtPfa2ARSCCAByAABVpSQkBRwmYAB6AArOAAFoWQgVhFQXIeiEa3vBHEVg4GbgBuL7+JI7gl94Xve94OpeEAJMJeBI6x+Jzo+6g3red6zg/z5PlWXkYdfN+3/3/u0Xfb+PsBZvHRhkGbswZuOhm4aGAbwZuIhm67iXu4bubQkCgFODEOAw48CDxAFsLYQA==="} +import { random, randomInt, randomItem } from '@studiometa/js-toolkit-v4/utils'; random(10); // 0 → 10 random(5, 10); // 5 → 10 randomInt(10); // an integer, 0 → 10 -randomInt(5, 10); // an integer, 5 → 10 +randomItem(['a', 'b', 'c']); ``` -**One argument is a maximum; two are a range.** The bounds are inclusive for `randomInt`. +**One argument is a maximum; two are a range.** -## `randomItem` +### random ```ts -randomItem(items: readonly T[]): T | undefined -randomItem(items: string): string | undefined +random(a: number, b?: number): number ``` -```js twoslash -// @twoslash-cache: {"v":1,"hash":"bccf2e1205cf47d57837fcb0faa3c6c3f7bb740d982ea073fad8b48c6c76e642","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvUszBQIAWwCSNBQB4AKgD5G7VXETSYzeWFYZeG5AF1uhjbwA+vYbH7swMKL0YBqAIy8xGSsECbcADpg7ApYEKRo0rLyyqqUIPIiCIggAIJJcoq8ejAKQfy8spWkMhgUQaSVBSm8IvjMMmJk5U1waKQeAOYAdOl9HQyIAJxUrDBgg2j4SP4ADFRoHYMwkyAyhaml6RyeSOsgbR3MXeTTAL4U6Ni4OYQk5Bt0uywcXHz7KRUpTUfQGCx0JQUBiMJkk5l4oKGNjsCP6QycLjkMHcnm8fkCwVIoXCURicQSzUUQIU6Uy2TylLKkJ6VQ6tXq8SaAKKl06NEaEAqzFRYJGY02CSQADYAMyzeaLZaINYbLY7PDcw402YeF4AJiovOu/LODyeODwbzI6Ro9CYbE4PEZ1N0+kMiIWKI9gwxrmxurxASC72JUEi0Vi8USmuptIgWTw+U1xVULKkbOYdQaXOSPPafO6gt6aIWoyo40lyr1cpAcwWSxW5wl212MbSOtOiBrRpupushugLxAZKjvGAztTdwEpCKAHIAAJ9QRQCQKHbMAD0ACs4ABaNAQCCsADWel3RAALBvBOJWHBZwBuKJRNulRjIWfMWf1WcAI2/vCziIs62A+vAbhugFfhif6zjBwG+liOJeC+uZaown6/sB3BgRBUFwc4sHwQRmJuAG6RrpsSCgF88xwBIYB4DuIB3HcQA=="} -import { randomItem } from '@studiometa/js-toolkit-v4/utils'; +A float in the range. + +### randomInt -randomItem(['a', 'b', 'c']); // 'a' | 'b' | 'c' | undefined -randomItem('abc'); // 'a' | 'b' | 'c' | undefined +```ts +randomInt(a: number, b?: number): number ``` -The return includes `undefined` because an empty input has no item to give — and a signature that pretended otherwise would put the bug three lines later. +An integer in the range, bounds inclusive. -The string overload picks a character, which is the same question asked of a different sequence. +### randomItem + +```ts +randomItem(items: readonly T[]): T | undefined +randomItem(items: string): string | undefined +``` + +One item from an array, or one character from a string. + +The return includes `undefined` because an empty input has no item to give — and a signature that pretended otherwise would put the bug three lines later. diff --git a/packages/v4/docs/utils/scroll.md b/packages/v4/docs/utils/scroll.md index b56d9f84..e5e43780 100644 --- a/packages/v4/docs/utils/scroll.md +++ b/packages/v4/docs/utils/scroll.md @@ -7,12 +7,7 @@ import { lockScroll, scrollPosition, scrollTo } from '@studiometa/js-toolkit-v4/ [[toc]] -## `scrollPosition` and `scrollTo` - -```ts -scrollPosition(target: ScrollToTarget, options?: ScrollPositionOptions): ScrollPosition -scrollTo(target: ScrollToTarget, options?: ScrollToOptions): ScrollPosition -``` +## Scrolling to a target **The two halves are separate because callers need them separately.** `scrollPosition()` measures and returns; `scrollTo()` calls it and moves. @@ -34,6 +29,24 @@ scrollTo(0); scrollTo({ top: 200 }); ``` +### scrollPosition + +```ts +scrollPosition(target: ScrollToTarget, options?: ScrollPositionOptions): ScrollPosition +``` + +Measures and returns `{ top, left }` without moving anything. + +### scrollTo + +```ts +scrollTo(target: ScrollToTarget, options?: ScrollToOptions): ScrollPosition +``` + +Calls `scrollPosition()` and moves there, returning the same result. + +`behavior` defaults to `'smooth'`, or to `'instant'` when the reader has asked for less motion — so honouring the preference is the default rather than a call site's responsibility. + ### The target ```ts @@ -53,29 +66,37 @@ interface ScrollPositionOptions { } interface ScrollToOptions extends ScrollPositionOptions { - behavior?: ScrollBehavior; // 'smooth', or 'instant' when the reader asked for less motion + behavior?: ScrollBehavior; } ``` -`SCROLL_AXES` and `SCROLL_ALIGNMENTS` are the frozen sets behind `axis` and `align`. - **`align` is `'start' | 'center' | 'end'`, or one per axis**, and it applies to an **element** target only. ::: tip The names are physical, like `axis` `x` and `y`, not the platform's `inline` and `block`. Nothing here maps a writing mode, and borrowing that vocabulary without the mapping would promise what `compute-scroll-into-view` promises and does not deliver. ::: -### What the arithmetic gets right +What the arithmetic gets right: **the viewport is the client box**, so a scrollbar gutter is out of it with no special case, and **the destination is clamped to the scroll range** — centring the first slide asks for a negative offset and gets `0`. + +### SCROLL_AXES + +```ts +const SCROLL_AXES: Readonly<{ x: 'x'; y: 'y'; both: 'both' }>; +``` + +### SCROLL_ALIGNMENTS -- **The viewport is the client box**, so a scrollbar gutter is out of the arithmetic with no special case. -- **The destination is clamped to the scroll range**: centring the first slide asks for a negative offset and gets `0`. -- `behavior` defaults to `'smooth'`, or to `'instant'` when the reader has asked for less motion — so honouring the preference is the default rather than a call site's responsibility. +```ts +const SCROLL_ALIGNMENTS: Readonly<{ start: 'start'; center: 'center'; end: 'end' }>; +``` ### A dependency was measured and refused `compute-scroll-into-view` is 1.4 kB and walks **every** scrolling ancestor — which is what v4's single `rootElement` contract declines, and what a boundary option already cancels. Its own source leaves writing modes unimplemented and reads no `scroll-padding`, so the real delta over core was about twenty lines. -## `lockScroll` +## Locking the page + +### lockScroll ```ts lockScroll(target?: HTMLElement): () => void @@ -98,19 +119,9 @@ release(); **The release is idempotent**, so a surface calls it on close and again on unmount without counting twice — and a component unmounted while open owes the page its scroll, which is what the second call is for. -```js -mounted() { - return this.release ?? undefined; -} -``` - **The count is shared across evaluated copies of the package**, through the same runtime slot the focus helpers use, for the same reason: there is one scroll per document. -### It is `overflow: hidden` and nothing else - -No `paddingRight` compensation. `scrollbar-gutter: stable` is the page's own answer and it does not mis-handle fixed children. - -iOS Safari remains unreliable — which is the argument for having **one** function rather than a copy per component. +It is **`overflow: hidden` and nothing else**. No `paddingRight` compensation: `scrollbar-gutter: stable` is the page's own answer and it does not mis-handle fixed children. iOS Safari remains unreliable, which is the argument for having one function rather than a copy per component. ::: tip A native `` needs it too `showModal()` gives the top layer, the backdrop, a focus trap and `Escape`. It does **not** stop the page behind it scrolling. diff --git a/packages/v4/docs/utils/strings.md b/packages/v4/docs/utils/strings.md index 1c6b6b5a..aa78afc0 100644 --- a/packages/v4/docs/utils/strings.md +++ b/packages/v4/docs/utils/strings.md @@ -9,16 +9,6 @@ import { camelCase, kebabCase, pascalCase } from '@studiometa/js-toolkit-v4/util ## Case -| Function | `'foo bar'` → | -| --------------- | ------------- | -| `lowerCase(s)` | `'foo bar'` | -| `upperCase(s)` | `'FOO BAR'` | -| `capitalize(s)` | `'Foo bar'` | -| `pascalCase(s)` | `'FooBar'` | -| `camelCase(s)` | `'fooBar'` | -| `kebabCase(s)` | `'foo-bar'` | -| `snakeCase(s)` | `'foo_bar'` | - ```js twoslash // @twoslash-cache: {"v":1,"hash":"fcc5c18f67a1882e573cc8a37d58a7a3256d60904aa5b4bb2bfc7c4a43c81402","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIBjCGDhoBzALYxWAYS4xEvALIxxEADzIRpdmADmCrTt0BdCr0N6AfAB0w7cVgilR/CVNlxcVKBH4JEINJCJM68zOZo2nq8aBC8AAaukjJy8QB0lCAizM5IAJxUrDB6aPj5VGg5ujAMAUnucpkcYLiIAAxU/Pg5zPw05Ih5AL4U6NitBMRkmTT0TGycPAJCIrwA1jAARsybHvJKKuqakUYGJ3qmEVG6NnYOTqIb27uN3r7+gcFkouEWujFxeJPHYAWlcnniZlKMHM3RwvAgADMwgkoMxKiD4mE0CdNgBXGgZKjZXKIABMHRARRKZUGFSqNTwwJeniaOlalK6PT603JbRGYxweEIIRmdFqIBYHC4fEEwlEWC4rhSngUylUGj+Z2ulz+t3sjlCirgyr2mR8fjwQTAIR+VyMAISAAUlWw9ulMiTagBmACMhWKulKSF9lMqpGqEuNpteVPZSE53VIvX6SD9Auo42FU3IFXF82lS2jbrkjC19r03G1RlsBoevGLKq8IAtH2ttpRf0d8RdJpLEKJWXDtQAHABWAM0kNhhlR11NtktJBkzpJlO8/mjTNCgIi6Z5uYBKWLWVuJtl876Cu6KvX2v3UL1BdvS0BdvfTuX7tP92Dr0h70ABZJyDWkCmoWc8B/WNmlab1V25VN2gzTAd0mUUDwlY8ZXWLYdj2C9rmrStiN0e9DUePCWWbVsrS+UJfi/WIEmZMFUihfAYTgOEYSRFF4jRDEsXRXECRgP9hxDEcVypQNgzpCCI0ZAJmTNQp40QYCQC5ZMeQGflTG06AJjrUJgDEZI9jMVS5DMRs9l4IZeERUgIHEXgAHIAAERDxKB2DcmpmAAegAKzgEFYggVg1nYNAQSIQDgoJdhWDgDyAG5bFsezSw8qBk10SL8FIeBCFYKAPO4DLeGC4LPIAEUKgAVEqyuiyrbGgzxGHywritK7iOqqmq6s8grmF0VrBvKzqwBsnq+sm6b2oqkbavqpaitKGbhsySRKiQUBxWKOAArAPBwpAIYhiAA==="} import { camelCase, kebabCase, pascalCase } from '@studiometa/js-toolkit-v4/utils'; @@ -28,22 +18,63 @@ camelCase('drag-threshold'); // 'dragThreshold' kebabCase('dragThreshold'); // 'drag-threshold' ``` -::: tip The four converters are memoised +These are the functions the framework itself uses: `kebabCase` turns `dragThreshold` into `data-option-drag-threshold`, and `pascalCase` turns an option name into `optionDragThresholdChanged`. + +::: tip Four of them are memoised `pascalCase`, `camelCase`, `kebabCase` and `snakeCase` are `memo`-wrapped, because the framework converts the **same** names on every option read and every handler resolution. `lowerCase`, `upperCase` and `capitalize` are not — they are single-pass and there is nothing to cache. ::: -These are the functions the framework itself uses: `kebabCase` turns `dragThreshold` into `data-option-drag-threshold`, and `pascalCase` turns an option name into `optionDragThresholdChanged`. +### lowerCase -## Leading and trailing characters +```ts +lowerCase(string: string): string +``` + +### upperCase + +```ts +upperCase(string: string): string +``` + +### capitalize + +```ts +capitalize(string: string): string +``` + +### pascalCase + +```ts +pascalCase(string: string): string +``` + +`'foo bar'` → `'FooBar'`. Memoised. -| Function | Does | -| ---------------------------------------------- | -------------------------------------- | -| `withLeadingCharacters(s, chars)` | adds them if absent | -| `withoutLeadingCharacters(s, chars)` | removes them once | -| `withoutLeadingCharactersRecursive(s, chars)` | removes them as long as they are there | -| `withTrailingCharacters(s, chars)` | adds them if absent | -| `withoutTrailingCharacters(s, chars)` | removes them once | -| `withoutTrailingCharactersRecursive(s, chars)` | removes them as long as they are there | +### camelCase + +```ts +camelCase(string: string): string +``` + +`'foo bar'` → `'fooBar'`. Memoised. + +### kebabCase + +```ts +kebabCase(string: string): string +``` + +`'fooBar'` → `'foo-bar'`. Memoised. + +### snakeCase + +```ts +snakeCase(string: string): string +``` + +`'fooBar'` → `'foo_bar'`. Memoised. + +## Leading and trailing characters ```js twoslash // @twoslash-cache: {"v":1,"hash":"c587a676dc6124acff5ffc0def82ee18254c6f3a3157e0560f66466cdb452fe9","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvAO7s0+ADIxmUdmADmAYXzNSzMWTiM4aUmvWJeJsxoq8ROvQdJxL189zenzAHTDsAWywIUjQZOUVlVQ1tXX0aF0oQKAgRBEQQAEEoKF55GF51dhIpBzjnODyIPPwCk10wiH5eZitvW15JERgAOiT60KQARiGqVhgNeSQAJlHqXXUYBgzZeSUVc1inBIQxtVxEObLtshmAXwp0bAOCYlOqGnomNk4eAWExCSlVwkE0ABU9OwODFHPFDAAlGAiQQuYowYztCxtGzqOzHcEuLyozwo3z+IIhMI/CB/QHMYGbMEVKEwuEkJIpNJ4KEBO41ApFEr2ak7ASkCABDm8Ca5JotPEdLidfg0KTS/IYXikGA4ZhoPpUAbLADMADYxhN1FNEDqACwPBZLPAkslAkFaXmQ6GwuDwpIgg7TKgY5znS7Ua54QgkcgPOjLEAsDhcPg/dbRR3lHaI1HY8zop1YyXqXHuDR+QLBULhNZRKnJwyM1LpLI5YVciY8ysuKrC7UyiX5tGdUS9fpoBrDIYAVkNk3wMzmg9Ii0j8fLoJbuxAnqQY5AvoSSAADBcrjhg3cw9QI88Y28FxslycXKnzOmOlvDI/c6/C4SS1fE1tMSumbW2S5PkhTwqUWaVGg1QgR24qtN2dhdP2WozssQx6haq5GiasyWrO1orBECYVreK5rogBqblmu77oGh4ZCG9ynk8GTRq8cYRKSAL2iRf60q68L3hor6Zsur55kiH7FsSnF2hSDq/jSLr0rgVAASyMBsiQDZgc2pH8oKwqip28FInY0pNHKLSQbUSoqmqGoDkOiDTDq3pYROSDmnhc42rJ3HybxSl0m6DJ7GABwAOw+tRiB7gAuj60A3EWRK8MAfi8KWkTXkmpEUJl2VceSlI3nxymhTABVgGcBlCgA5AAAiYgiqIKSzMAA9AAVnAAC0UEQKwADWch9UQZqdX8wJwPVADcfh+N+QUpvVABGuj1XY9X8BAED1dwc28J1nW8Dte0baQ9VLURi55X+jDnRAl1bWdu37Ydx2nU9L28IAKAQtKwKoqEq+Qqjd8jFTxZXBQJJCPVg6r4CdnWvfVqOfSdZ2I/I9VJAEHVIKAEYTG6kh4L1IBnGcQA=="} @@ -59,9 +90,57 @@ withoutTrailingCharactersRecursive('path///', '/'); // 'path' The **recursive** pair exists because a URL joined from parts collects separators, and removing one is not the same as removing them all. +### withLeadingCharacters + +```ts +withLeadingCharacters(string: string, characters: string): string +``` + +Adds them if absent. + +### withoutLeadingCharacters + +```ts +withoutLeadingCharacters(string: string, characters: string): string +``` + +Removes them once. + +### withoutLeadingCharactersRecursive + +```ts +withoutLeadingCharactersRecursive(string: string, characters: string): string +``` + +Removes them as long as they are there. + +### withTrailingCharacters + +```ts +withTrailingCharacters(string: string, characters: string): string +``` + +Adds them if absent. + +### withoutTrailingCharacters + +```ts +withoutTrailingCharacters(string: string, characters: string): string +``` + +Removes them once. + +### withoutTrailingCharactersRecursive + +```ts +withoutTrailingCharactersRecursive(string: string, characters: string): string +``` + +Removes them as long as they are there. + ## Slashes -The four named cases, for the one separator every path uses: +The four named cases, for the one separator every path uses. Each is the matching `*Characters` function with `'/'` filled in, which is what makes a call site readable at a glance. ```js twoslash // @twoslash-cache: {"v":1,"hash":"e39d97cd15dec1619a5909cd20f3446185a1bf1dddc30c8a37222f07473dd106","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvAO7s0+ADIxmUdmADmAZVZd8jOGlJr1iXgaMbup88YA6YdgFssEUmhlzFy1Ru27KIFAQIgiIIACCUFC8zLys3sZmOnD4vGgQMWaGxhS8kiIwAHQBBsxuSACMFVTxGvKVAGxUaGXqMAxhsvJKKsZ+KQEcYLiI1SAi+GXMYmRIAEwAvhTo2CMExLPNdB0gLBxcfEKi4pIe8hCCaD0+Wsl6NhrW2ZZPFur2Ti5uZ4SX1313AJBEJ4ABKMEcGzS+BgcQSGiSugEpAgjkyD3UuXyRRKLXKiAAzGNaup6qMAJzNVrtPBdX5XeG3fw1NQjOZUCZTGbkRCLZbUVZ4QgkchbehMNicHgCYRiCRSOkXNAAFVIzHYQyZKX0zxMWTeVn1dgczlc7kVl1V6s1/XwQOCoRA4MhJGhsMM1sScDuyNR6N1WNEOKopXxAFYACw1GB1O28gAMVNIbR2FpVao1AOZICGIwJHMmau58yWKxwQo2ouo2wl+2lRzlpzpVszvjuOrer2MhoxH1N32bGZtgKowMdkWisQ9rfUiJSaQysQxgYKxRDeI6AHYABzR2OVTdJlO0zwt4fZ3NIKPjQvTGg8vlltbCzbV8VhPZSvh0/5t3Qd4wuxeI0ND7L5zU8X8tTtUcHTwCdMniXoEW9JF0n9N4V2DEBQw6CoI0THMY1JOMKiaahqVTSDGVtQZWSQQ8by5e8kHjUsBXLMIXyrGh312SUDh+JUoNtADHhA9Qe11MCzSEv4aJHQI4LCZ0oXkWEkJuOdUn4FE0SXAM8iDNccI3A9KSI/cKSPGlOk8YSFIvejEF3JiixYxA2P5TBOPWEUAl4nZP0EwdPT/bUMSAySopkgdTyHLMBlgkEwgQqcEpQn10IMzCjNXXEyg6OY5mJYiyQqRi8WPOz5DPRKYJzZyLM5dzZk89ifOfSsAprD8BOlNM6vC+5dSiqS3liiDzktDLoPtFKnQhNSYTSWbtN9fSJKwkzcPmBp80skj5kIqrbJAQbZtollhkqQiWrvNq2IAXQ5aA1k+WTgHsXgfhEu4KG+uSGWQ6CAakIGhtBwHQpnW0wYWDbeAAcgAAQMQRVFRdpmAAegAKzgABadIIFYABrORCaICMccuDU4CRgBuex7B/RztSR5gACMlSR7hGd4HGceRnHud51n7PkkHRKR0WecuPmBaF5GxYViXasu9tZdVtBFcF4XtfltAcaR9X6UhmW5aVE3+f1kWdaRgJHGxpBQG2GM4HlPACZABYFiAA"} @@ -78,4 +157,26 @@ withTrailingSlash('/about'); // '/about/' withoutTrailingSlash('/about/'); // '/about' ``` -Each is the `*Characters` function with `'/'` filled in, which is what makes a call site readable at a glance. +### withLeadingSlash + +```ts +withLeadingSlash(string: string): string +``` + +### withoutLeadingSlash + +```ts +withoutLeadingSlash(string: string): string +``` + +### withTrailingSlash + +```ts +withTrailingSlash(string: string): string +``` + +### withoutTrailingSlash + +```ts +withoutTrailingSlash(string: string): string +``` diff --git a/packages/v4/docs/utils/timing.md b/packages/v4/docs/utils/timing.md index f110531a..9261d63e 100644 --- a/packages/v4/docs/utils/timing.md +++ b/packages/v4/docs/utils/timing.md @@ -7,12 +7,7 @@ import { debounce, memo, throttle, wait } from '@studiometa/js-toolkit-v4/utils' [[toc]] -## `debounce` and `throttle` - -```ts -debounce(fn: (...args: Args) => void, delay?: number): (...args: Args) => void -throttle(fn: (...args: Args) => void, delay?: number): (...args: Args) => void -``` +## Rate limiting ```js twoslash // @twoslash-cache: {"v":1,"hash":"daeafcc7b411344a60029e09f9ade6dcaabf1f494f677886ae8122bdb7bd0cae","data":"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"} @@ -22,18 +17,31 @@ const search = debounce((term) => console.log(term), 300); // after the last cal const track = throttle((y) => console.log(y), 100); // at most once per delay ``` -| Function | Runs | -| ---------- | -------------------------- | -| `debounce` | once, after the calls stop | -| `throttle` | at most once per `delay` | - ::: tip Not for scroll, resize, pointer or frame work The services already coalesce: the scroll service batches its events into **one `read` per frame**, and the resize service is a `ResizeObserver`. A `throttle` on top of that is a second, worse rate limiter. Reach for these for what the framework does not own — a `fetch` per keystroke, an analytics call, a `localStorage` write. ::: -## `wait` +### debounce + +```ts +debounce(fn: (...args: Args) => void, delay?: number): (...args: Args) => void +``` + +Runs once, after the calls stop. + +### throttle + +```ts +throttle(fn: (...args: Args) => void, delay?: number): (...args: Args) => void +``` + +Runs at most once per `delay`. + +## Waiting + +### wait ```ts wait(delay?: number): Promise @@ -52,7 +60,9 @@ async function pause() { For a frame rather than a timer, use [`nextFrame()`](/api/scheduler/nextFrame.html). For "the framework has caught up", use [`whenDOMSettled()`](/api/dom/whenDOMSettled.html) — a timer is the wrong tool for both, and the reason the [test helpers](/api/test/) exist at all. -## `memo` +## Memoisation + +### memo ```ts memo(fn: (...args: Args) => Value): Memo @@ -74,12 +84,9 @@ It is what the four memoised [string converters](./strings.html) are built on, a `cache` and `memoize` from v3 are not shipped — this covers the one case core needed. -## `noop` and `noopValue` +## Placeholders -```ts -noop(): void -noopValue(value: T): T -``` +They earn their place as **defaults**: a parameter defaulting to `noop` removes an `if` from every call site, and one defaulting to `noopValue` removes it from a transform pipeline. Both are one shared function, so a default costs no allocation per call. ```js twoslash // @twoslash-cache: {"v":1,"hash":"a90bfe5cdc8aa414e93bf2d075d71a488d161f8acfad585a833943607a7245eb","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvSBCyNuiXkQjsoAHTDsAtlgik00iLMogoEEQkQgAIhENp87MAHMKvAO75mNEqV7sDdjheETZWGCgPAPwAOhM4NGZ9JABOKnCXByQAFipE0mcYBisZLBMOMFxEAAYqES9SZjEyVIBfCnRsKoJiFry6YpAWDi4+IVFxSUNZADU2QRgAHgAVAD5GInmYRWWFXmWNbV19aaw51gWTMws8ACUiwVIpBxglLd5hNhpSCMUX/1gYHEmF4EH4vGYvDQjTAcH4ei0IS8TjiVASSWKAEYAKzpGCZfCpPJJQqDUrnS7pJxVWogepJJrfJA49qdHB4Qi+Ew0eh4ESSBKgsC2SqKeS8AC8qyUKig8XyxQA7LkQBlnFlEAA2YkFIp4SQi3BUypIABMdQajJaWtZ1C6HN65H6vKsw04PAEwjEEikpXkimUqkOOj0BlKV3MlhsdkgDicrg8Xh8ZH8gWCoVY4Ui7miqJA6OSiAAHABmPEEnI60l4cPGqrmumW5rkRAsjp29lWTl9agDPkCwKA4EYRQrdabC7bfZ8KX7eUY5mmxXl9WE4tVvVWVT44flalIMuNhnNpCa22YTs9LnOwZu0aeiY+04UpZrDZbHZ7A6aEMnclbCMbise40EeZ58FeCcFg+MAvjIX4oQggEdwCDBQXBSFoWYWF4VIRF6RRedC0xEsixXDU0moElN3AIwzgAusDwtY8mVbRVWgAXTqaBuiOUNeGAU43H/SdeFaARSAgREAHIAAEEkEKAJC0IpmAAegAKzgABaNAjFYABrAJtKIbI1MEcRWDgaSAG4NA0flYQMA1JFeCVThs3g1LUiEQjCAAjJoDMQ7xeDMeB7EcFwHIHZCgVQyVny2TzvN8rCcIREKDHpFwItjKLnBMFTEiQUABnxOAfTwLSQFaVogA==="} @@ -89,7 +96,17 @@ const onDone = noop; // a callback that does nothing const identity = noopValue; // a transform that changes nothing ``` -They earn their place as **defaults**: a parameter defaulting to `noop` removes an `if` from every call site, and one defaulting to `noopValue` removes it from a transform pipeline. Both are one shared function, so a default costs no allocation per call. +### noop + +```ts +noop(): void +``` + +### noopValue + +```ts +noopValue(value: T): T +``` ## What is not here diff --git a/packages/v4/docs/utils/transitions.md b/packages/v4/docs/utils/transitions.md index 60e8ae24..dca850b0 100644 --- a/packages/v4/docs/utils/transitions.md +++ b/packages/v4/docs/utils/transitions.md @@ -9,10 +9,14 @@ Promoted into core from the migration set, beside the easings, `spring()` and `s [[toc]] -## `transition` +## transition ```ts -transition(el: HTMLElement, nameOrStyles: string | TransitionStyles, mode?: 'keep' | 'remove'): Promise +transition( + el: HTMLElement, + nameOrStyles: string | TransitionStyles, + mode?: 'keep' | 'remove', +): Promise ``` ```ts @@ -26,35 +30,32 @@ interface TransitionStyles { The three-state dance every CSS transition needs, with the frame boundaries in the right places: ```js twoslash -// @twoslash-cache: {"v":1,"hash":"f7974e360499fb4e1289aa9837b82f4286bf1e5a7ff2593ac55b03ba96faf66e","data":"N4Igdg9gJgpgziAXAbVAFwJ4AcZJACwgDcYAnEAGhDRgA808AKAQwBsBLZuASgAIAzAK5gAxmnYQwvNKWZg47cZMYxWiXgAkAKgFkAMgFFWMALYwwaCrzDMzAeVIBlTMbjq4M9mADmvAD68WrLyihJgzhiuVibQMAD86gA6IADWMDBYyf68yaSmxDDJ3OoACqQQJuxwMAA8RBDsUAB8iWDsJlgQpGjSwQpKYJQgUBAiCIggAErCvMy8AMKOjr1y/WG8krNSqqbmaAB0ra0AgishA7wezN0wULwA7vjsxlsQaPhkG2AwvKTCcF9pB9LrYfjszBZeF4aKQ/lg0HBWop1O8ftCyHCaHdJD88jESADHsweopZlgsBxbls7nk4BBWASrHTZrwRGxjKRWsx7sxQj4oSSAd8SKReMZ+D0eXzfPwulteOY7o92CJ8A9nqxrG9WRUYPshmhmN5xshkCAsNdbEMYrBeAB1D6o0Wo3gAAzQEFdl0NNChALS8I2orxBTuxKBYLAUH1AF0Y1Qrt0kABOKjGHzvJAARgADFRDaRvDAGBMZKtQpIhhxvkg8yBVZaxGQUwBfCjobC4CaEEUGugl+uSDwKtSaXSGYwQhgJgslgBsqZA6e8mcQACZ89ciwPVFWvF2N/X8I2YUg522Ozg8D3m/n+3giNdeCMRIIp+oACKjN97IYv8YgAAVIBrr3F4Iz3PsL4/hYrrAb8xaCKQ8gsnk/BkOYIg/B6EbPt+U46hYvLfHcXi4WBUYQJBRxgMgOgfgAcrwkwwOheSiDAMaMPgaBoFgbgAPQCbAJCsBAOCkPsMQAF4asw+xdN4wmjHAAl2jAABGAnHCUACSangVRymvlO3BDImJYAOwACxpuYK74EgAAcm6FsWeDQVOe41uuVANrITbkIgWZZhe1CdteBTkHe9BMFg5QSZgfBfiZez7Bp0AYOo2j6EYuwWH+Kl4FowLAa6KUwQcGVQBgcGAbw8XiWQmAIfF8B7ACLo1NVGBNEGvA1PwshmNUaB9ZAtoQPwuGvrCex4alFhWHKYCCKwmrsNNkCXIIqojvlPR0FUCKHGArR0YxzGsRhHFcTxfGCcJMCiU1kkyXJCmFsZqnqVpOn6RVU4CT1ZkztcJZOQAzHZGaOYgtnUFu7kTD13kHn5x4Bae645mFmBXt2UV9rFEwsBwXB8GW5xhCoo45ROB1WDY9hOC48DuJ4/IBEE5YDBEUS8Da8RJKk6SZCA2S5PkJBFKU5SVNUdQNM0SIdF0PRU2slZUP+eDTFIcyLMsmsVlImxyPtU6nScZxa1IFlUsqLxyG8Hyijivz/ICLpwKClvzeisKCPCiJtGgKLAoHmJUh7IYEg8x4kj0zDkpSYZRghdIMvATIQCybLrWQXJSuI/KklU1jPZ84qSrypcynKFuKgnKpqmB61aj0Ii6vqm7GkgprmpaJjWrE9qOm7uHup63rEmi/oZD0cpx1S4YuoqsbxiAFnZgArAjy6rrmrnbngJsDGjSC7xjJ7Nog57tuFBMEETMUDt38iHXT455V5YNJsFOc18lz2VXIeAsp8Ji7jTPuK+N8sZ3yzJZPGEVCa9jfkwNgnAeC21NrTbKP9Jx7CZqCBw/N2belIF4Xw3M+im3IXAaIsQEg5FFhkLIAQpb4kKCAYovAygVCqLUeojQWhtDVt0XBF8dZFQmPrFkRspHrHNtsIhFhrZgFOOfdYDslRPGdpAJ0XxcRe02D7P24IA4WAxMHBESJw64SjrYmO3wELcMJInAUZIKTsFXhnWk9JGSXDznMAuHJi512oV4iuwpq6sVrtKAQjdtgZ2VHtdumpDE6jML3RG/cUBmgtMNUetoHTFkni6aeXori+grgGJewZparw1sCDeIA4z/xLGuEKMMHLZjrBA5G1A6HSKXLAxALkjy3yCg/S8XYX7oOoPeCYH9hyqAIblNR05t6ziQGufZvSwEnyGdAsZPlJn+WYIFbMyDH743mTeaKSySYgEYI1RKGA+A82ppIBh+whoVBYfMVgXBqhwDIWzAEARhCwH4PuKA5ldnrkhnWQ+cMEaDIHACkeMCfKLkudc9cKDn6POJgON5CVmqfI2BaEQigMAsI8FQrmvAYWsXhYVMYxVSogXElc+ldUFhLAapS7oGBLjFk6sCPldKWpTXlJY9RvA7C0vpX6XCsBvB5GwnnNJaoP40EhBpGATwM5Ny2eqp4UBYBgCsHIUiUqfjiU6P0J101NZFMwhgDRF0mIsTYphTi3FeL8UQEJESqhXpSQgLJda8lFLfTUppASRsBL+pulhASAjEq+NUjK+loMdngz2ZDSZaKkCWWOQOfNmBL6IHxZjK52NczEoea/Z55L3lUq+SMsIfym3sBIECkFcAwUQsiPAbIbK4UkURcW9cu85yHLhkuxGbkBwDpIHW4+UyEFBTXK2yKiyaAvIpa9JKSjJCMs5jQ1lUZ2WzpkVyiYJUfhlW0ZIQViiu1ivVXMOAhBujHgzj+lqspnS9skAAWlAxgKwH6wBQagEhYkYR4OQcQ+ISoPgoNCFEAMdDvMwhIdUMwOD1JL2IeNceIgEhJI0V9VdANt1g0PTDU9F6Elo2xpBZ9JS/4k1aVTem9imbs3NVzQJBDhad7rjnKu8twUBlIwHAh7ddYCXNoPXc1BCzbwdriqKi93y7Z/I9MO0F8Bx2uCnfemdtw50ALXMmRcinwEqbPhAbdWZ4FNrvtpuZR79Mns7UZ6lNaGUc2Zbe6dHKn0AVfbwMqEWv3CtgxKhEuEIsbGmuag6+xlWqpahXF0mrtXSF1U8PaBr5rUfAgqi1FcrU2rtRnEr0ryQQFdTl22nrRDeoY/RP111RNBvuqG8NVcxJcfenGvjibfopqWGmkbgas1GckxFmTSKUXAMU5WtdkCQARe875wlTlD1oOC8s15WCKaUfwWOTZjNrCkNZhOtwlCom0KI78yFTDYAsOSGkdhEtOEgBXrLfh8shFK1EarToki1Pxb1jMQ2wqEOAjy1bGiWiMOzxuHo54PwXZGNjqYqQ5izD+0hE4kO9iI5omsUHeELjcRNI8eGUkKcfF+JpPAQJOdgn53ZEXGwJconlyFFXUUNcHiRP5OBhVqSqttw1J3bJeoDRGhNIU4eJSfhlKMZUj01SfTz14PU/qK8wwtMjNGdpW9ZOQwXMu/pVaz4Ye3cAzTd9ZlPzbcem7qyv4bIZn/ItADS2otAXDdz668CnOrF2LMPvG2EqQS2Le3dYB4HaIjnowBKO8BbAIeWvAADkAABDwggoASDMIaASAArOAUGPT0hSIoKDRBrICUEOIVgcAK8AG4aIh5HLwAAvAtSq6VMpj7OmAISgRgQiBHQCcDJh1Cun4MwWAuH5auisLv/fMAoObpgMft0e+D+m9OtyOuD3VBWAr7fmAFfuCL9aCvxLXhk9QYeATpJKkAmAP4lzP6sBWDACtC8Bl4VDqBF4RbqAV45gV4l4UCwGzBiCDowCIGUYoHZYoo5gmDD4YFYEej4HIGV5ZjoFtitAthf40Qr4ADSYsU8pus8vojw5gXiiocA4BT+CGtMr+7+Fer+IOWAn+I+1oxYzASAoA/Y5gCglYEwreIALYLYQAA==="} +// @twoslash-cache: {"v":1,"hash":"b3e1a13fcd5bb55f39bc7848470b4a5be2ca5fdd31c5a795c399ac9e79f05811","data":"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"} import { transition } from '@studiometa/js-toolkit-v4/utils'; const el = document.body; -// The class form: `fade-from`, `fade-active`, `fade-to`. -await transition(el, 'fade'); +async function run() { + // The class form: `fade-from`, `fade-active`, `fade-to`. + await transition(el, 'fade'); -// The inline-style form. -await transition(el, { - from: { opacity: '0' }, - active: { transition: 'opacity 300ms' }, - to: { opacity: '1' }, -}); + // The inline-style form. + await transition(el, { + from: { opacity: '0' }, + active: { transition: 'opacity 300ms' }, + to: { opacity: '1' }, + }); -// Keep the `to` state when it ends. -await transition(el, 'fade', 'keep'); + // Keep the `to` state when it ends. + await transition(el, 'fade', 'keep'); +} ``` `mode` decides what happens at the end: `'remove'` (the default) clears the states, `'keep'` leaves the `to` state applied. Each of the three states takes a class name, an array of class names, or an inline-style object — [`setClassesOrStyles()`](./css.html#setclassesorstyles) is what makes that work. -## `enterTransition` and `leaveTransition` - -```ts -enterTransition(el: HTMLElement, options: TransitionOptions): Promise -leaveTransition(el: HTMLElement, options: TransitionOptions): Promise -``` +## The two directions ```ts type TransitionOptions = { @@ -69,14 +70,32 @@ type TransitionOptions = { }; ``` -The two directions, from one option object — so a component declares eight options once and both calls read the same thing: +One option object, so a component declares eight options once and both calls read the same thing: ```js await enterTransition(this.$el, this.$options); await leaveTransition(this.$el, this.$options); ``` -`TRANSITION_OPTIONS` is that option set as a `config.options` fragment, ready to spread: +### enterTransition + +```ts +enterTransition(el: HTMLElement, options: TransitionOptions): Promise +``` + +### leaveTransition + +```ts +leaveTransition(el: HTMLElement, options: TransitionOptions): Promise +``` + +### TRANSITION_OPTIONS + +```ts +const TRANSITION_OPTIONS: Record; +``` + +That option set as a `config.options` fragment, ready to spread: ```js static config = { diff --git a/packages/v4/package.json b/packages/v4/package.json index dd1db4fd..19237158 100644 --- a/packages/v4/package.json +++ b/packages/v4/package.json @@ -6,7 +6,7 @@ "sideEffects": [ "./dist/responsive-options.js" ], - "description": "v4 prototype — tested in a real browser through Vitest browser mode", + "description": "v4 prototype \u2014 tested in a real browser through Vitest browser mode", "files": [ "dist" ], @@ -801,6 +801,7 @@ "build": "node scripts/build.js", "check:constant-subpaths": "node scripts/check-constant-subpaths.js", "check:diagnostics": "node scripts/check-diagnostics.js", + "check:doc-links": "node scripts/check-doc-links.js", "check:package": "npm run build && node scripts/check-package.js", "demo:dev": "vite demo --host", "demo:build": "vite build demo", @@ -808,7 +809,7 @@ "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", - "test": "npm run check:diagnostics && npm run test:fixtures && vitest run", + "test": "npm run check:diagnostics && npm run check:doc-links && npm run test:fixtures && vitest run", "test:fixtures": "node scripts/build-runtime-fixtures.js", "test:runtime": "npm run test:fixtures && vitest run src/shared-runtime.spec.ts src/duplicate-runtime.spec.ts", "test:watch": "vitest", diff --git a/packages/v4/scripts/check-diagnostics.js b/packages/v4/scripts/check-diagnostics.js index 17dac7a1..ae5496c3 100644 --- a/packages/v4/scripts/check-diagnostics.js +++ b/packages/v4/scripts/check-diagnostics.js @@ -9,12 +9,28 @@ const sourceRoot = resolve(packageRoot, 'src'); const files = glob.sync(['**/*.ts', '!**/*.spec.ts', '!**/*.bench.ts', '!diagnostics.ts'], { cwd: sourceRoot, }); +/** + * Drop comments before the checks below. + * + * All three guards are about *emitted code*: a console call, a `reportError()` + * call, a reference that pulls in the whole frozen object. A comment is none of + * those — it is gone by the time the bundle exists. Testing the raw source made + * prose trip the guard, and the doc-page links are the case that surfaced it: + * `@link .../DIAGNOSTICS.html` matched the `DIAGNOSTICS.` reference test. + * + * @param {string} source The file contents. + * @returns {string} The contents with block comments and whole-line `//` comments removed. + */ +function withoutComments(source) { + return source.replaceAll(/\/\*[\s\S]*?\*\//g, '').replaceAll(/^[ \t]*\/\/.*$/gm, ''); +} + const directOutput = []; const directErrorReports = []; const fullObjectReferences = []; for (const file of files) { - const source = await readFile(resolve(sourceRoot, file), 'utf8'); + const source = withoutComments(await readFile(resolve(sourceRoot, file), 'utf8')); if (/console\.(?:debug|info|log|warn|error)\s*\(/.test(source)) { directOutput.push(file); } diff --git a/packages/v4/scripts/check-doc-links.js b/packages/v4/scripts/check-doc-links.js new file mode 100644 index 00000000..92cb9184 --- /dev/null +++ b/packages/v4/scripts/check-doc-links.js @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import glob from 'fast-glob'; + +/** + * Every public export carries an `@link` to the page that documents it. + * + * The subpath stubs are generated re-exports with no comment of their own, so an + * editor follows them through to the implementation — which is where the link + * has to be, and the only place this checks. + * + * It asserts the link *exists*, not that it resolves: resolving an anchor means + * building the site, and this runs on every `npm test`. The build is where a + * dead link is caught, through VitePress' own dead-link check. + */ +const packageRoot = resolve(dirname(new URL(import.meta.url).pathname), '..'); +const sourceRoot = resolve(packageRoot, 'src'); +const DOCS_ORIGIN = 'https://js-toolkit-v4.studiometa.dev'; + +const stubs = glob.sync('subpaths/**/*.ts', { cwd: sourceRoot }); +const missing = []; + +for (const stub of stubs) { + const source = await readFile(resolve(sourceRoot, stub), 'utf8'); + const from = /from '([^']+)';/.exec(source); + const [, symbol] = /export \{ (\w+)/.exec(source) ?? []; + if (!from || !symbol) continue; + + const implementation = resolve(sourceRoot, dirname(stub), from[1]).replace(/\.js$/, '.ts'); + const code = await readFile(implementation, 'utf8'); + const declaration = new RegExp( + String.raw`^[ \t]*export (?:async )?(?:function|const|class|let) ${symbol}\b`, + 'm', + ); + const match = declaration.exec(code); + if (!match) continue; + + // The docblock, if there is one, is whatever `/** … */` closes right before it. + const head = code.slice(0, match.index).trimEnd(); + const block = head.endsWith('*/') ? head.slice(head.lastIndexOf('/**')) : ''; + if (!new RegExp(String.raw`@link ${DOCS_ORIGIN}\S`).test(block)) { + missing.push(`${symbol} (${implementation.slice(sourceRoot.length + 1)})`); + } +} + +assert.deepEqual( + missing, + [], + `Every public export must link to its documentation page with \`@link ${DOCS_ORIGIN}/…\`:\n${missing.join('\n')}`, +); +console.log(`Doc links: ${stubs.length} public exports each point at their documentation page.`); diff --git a/packages/v4/src/Base.ts b/packages/v4/src/Base.ts index 99c760df..96273522 100644 --- a/packages/v4/src/Base.ts +++ b/packages/v4/src/Base.ts @@ -887,6 +887,10 @@ function reportLifecycleFailure(instance: Base, message: string, error: unknown) /** What `#guard()` returns instead of a value when the guarded call threw. */ const GUARD_FAILED = Symbol('guard-failed'); +/** + * The class every component extends. + * @link https://js-toolkit-v4.studiometa.dev/api/ + */ export class Base { /** A class-owned brand inherited by subclasses and shared by bundled copies. */ static readonly [BASE_BRAND] = true; diff --git a/packages/v4/src/attribute-namespaces.ts b/packages/v4/src/attribute-namespaces.ts index 5f9ca5fe..491853a5 100644 --- a/packages/v4/src/attribute-namespaces.ts +++ b/packages/v4/src/attribute-namespaces.ts @@ -69,6 +69,7 @@ export interface AttributeNamespaceOptions { * @param bind Called once per declaration; returns that binding's release. * @param options An optional finite head vocabulary, and a component name. * @returns An idempotent cleanup releasing every binding and stopping the watch. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/watchAttributeNamespace.html */ export function watchAttributeNamespace( el: Element, diff --git a/packages/v4/src/attributes.ts b/packages/v4/src/attributes.ts index 8cc2d0db..523cea25 100644 --- a/packages/v4/src/attributes.ts +++ b/packages/v4/src/attributes.ts @@ -36,7 +36,10 @@ import { kebabCase } from './utils/strings.js'; /** Declares one or more component names, as whitespace-separated tokens. */ export const COMPONENT_ATTRIBUTE = 'data-component'; -/** Overrides the mount strategy of the element's declared components. */ +/** + * Overrides the mount strategy of the element's declared components. + * @link https://js-toolkit-v4.studiometa.dev/api/html/data-mount.html + */ export const MOUNT_ATTRIBUTE = 'data-mount'; /** Names an element as a ref of the component which owns it. */ @@ -84,6 +87,7 @@ export function isInNamespace(namespace: string, name: string | null): name is s * The qualifier a name carries within a namespace, or `null` when the name is * outside it — the bare namespace included, since a namespace with nothing * after the colon declares nothing. + * @link https://js-toolkit-v4.studiometa.dev/guide/concepts/attribute-grammar.html */ export function namespaceQualifier(namespace: string, name: string): string | null { const prefix = `${namespace}${QUALIFIER_SEPARATOR}`; diff --git a/packages/v4/src/context-subscription.ts b/packages/v4/src/context-subscription.ts index ee51e208..5d097da4 100644 --- a/packages/v4/src/context-subscription.ts +++ b/packages/v4/src/context-subscription.ts @@ -189,6 +189,7 @@ function unsubscribeSubscription(subscription: ContextSubscription): void { * value only updates provider ownership and does not restart the callback. * * @returns One unsubscribe function. + * @link https://js-toolkit-v4.studiometa.dev/api/context/subscribeContext.html */ export function subscribeContext( el: Element, diff --git a/packages/v4/src/context.ts b/packages/v4/src/context.ts index b7e56de2..d338395c 100644 --- a/packages/v4/src/context.ts +++ b/packages/v4/src/context.ts @@ -6,7 +6,10 @@ const CONTEXT_REQUEST = 'js-toolkit:context:request'; /** A typed context key. `__context` is type-only. */ export type ContextKey = symbol & { readonly __context?: T }; -/** Create a typed context key. */ +/** + * Create a typed context key. + * @link https://js-toolkit-v4.studiometa.dev/api/context/createContext.html + */ export function createContext(description = 'context'): ContextKey { return Symbol(description); } @@ -29,6 +32,7 @@ interface Subscriber { * Create a reactive value. * * Writes settle synchronously. A reentrant write supersedes the current delivery, so no subscriber observes an older value after a newer one. + * @link https://js-toolkit-v4.studiometa.dev/api/context/signal.html */ export function signal(initialValue: T): Signal { let current = initialValue; @@ -162,7 +166,10 @@ export function cancelContextRequest(request: ContextRequest): void { pendingRequests.delete(request); } -/** Provide a value verbatim for a subtree. The nearest provider wins. */ +/** + * Provide a value verbatim for a subtree. The nearest provider wins. + * @link https://js-toolkit-v4.studiometa.dev/api/context/provideContext.html + */ export function provideContext( el: Element, key: ContextKey, @@ -196,6 +203,7 @@ export function provideContext( * Provide a document-wide value created once per key. Nearer providers override it. * * Root providers are page-lifetime and cannot be disposed. + * @link https://js-toolkit-v4.studiometa.dev/api/context/provideRootContext.html */ export function provideRootContext(key: ContextKey, create: () => T): T { if (rootProviders.has(key)) { @@ -214,6 +222,7 @@ export function provideRootContext(key: ContextKey, create: () => T): T { * * The promise stays pending while no provider exists. `cancel()` removes that * pending request and does nothing after an answer. + * @link https://js-toolkit-v4.studiometa.dev/api/context/injectContext.html */ export function injectContext( el: Element, @@ -234,7 +243,10 @@ export function injectContext( return { promise, cancel: () => cancelContextRequest(request) }; } -/** Resolve the nearest provided value synchronously, or return `undefined`. */ +/** + * Resolve the nearest provided value synchronously, or return `undefined`. + * @link https://js-toolkit-v4.studiometa.dev/api/context/injectContextSync.html + */ export function injectContextSync(el: Element, key: ContextKey): T | undefined { let resolved: T | undefined; dispatchContextRequest({ diff --git a/packages/v4/src/decorators.ts b/packages/v4/src/decorators.ts index 1508d702..aec40743 100644 --- a/packages/v4/src/decorators.ts +++ b/packages/v4/src/decorators.ts @@ -100,6 +100,7 @@ function withInitializer( * Decorate a method as an event handler. * One string binds an event on the component root. A string and event type bind a child or ref. * A component class binds that child type. `window` and `document` bind global events. + * @link https://js-toolkit-v4.studiometa.dev/api/decorators/on.html */ export function on( type: K, @@ -179,10 +180,16 @@ function inPhase(phase: '$read' | '$write') { }; } -/** Schedule the method body in the next read phase. Destruction cancels pending work. */ +/** + * Schedule the method body in the next read phase. Destruction cancels pending work. + * @link https://js-toolkit-v4.studiometa.dev/api/decorators/read-write.html + */ export const read = inPhase('$read'); -/** Schedule the method body in the next write phase. Destruction cancels pending work. */ +/** + * Schedule the method body in the next write phase. Destruction cancels pending work. + * @link https://js-toolkit-v4.studiometa.dev/api/decorators/read-write.html + */ export const write = inPhase('$write'); /** Config keys that merge rather than override; the rest are compared as scalars. */ @@ -254,7 +261,10 @@ function conflictingEntries( return conflicts; } -/** Set the component config and register the class when it is defined. */ +/** + * Set the component config and register the class when it is defined. + * @link https://js-toolkit-v4.studiometa.dev/api/decorators/component.html + */ export function component(config: BaseConfig) { return function decorate( value: T, @@ -301,7 +311,10 @@ export function component(config: BaseConfig) { }; } -/** Provide the decorated field to descendants. The nearest provider wins. */ +/** + * Provide the decorated field to descendants. The nearest provider wins. + * @link https://js-toolkit-v4.studiometa.dev/api/decorators/provide.html + */ export function provide(key: ContextKey): ValueDecorator { return function decorate( _target: unknown, @@ -316,6 +329,7 @@ export function provide(key: ContextKey): ValueDecorator { /** * Resolve the nearest provided value into the decorated field. The field stays `undefined` until resolution. * Resolution starts once at construction and does not restart after destruction. + * @link https://js-toolkit-v4.studiometa.dev/api/decorators/inject.html */ export function inject(key: ContextKey): ValueObserver { return function decorate( @@ -333,6 +347,7 @@ export function inject(key: ContextKey): ValueObserver { /** * Track mounted descendants as a live collection. Strings match `config.name` exactly. * Component classes include named subclasses. Callbacks bind to the host component. + * @link https://js-toolkit-v4.studiometa.dev/api/decorators/children.html */ export function children( name: string, diff --git a/packages/v4/src/diagnostic-contract.ts b/packages/v4/src/diagnostic-contract.ts index 760934ea..136d46b7 100644 --- a/packages/v4/src/diagnostic-contract.ts +++ b/packages/v4/src/diagnostic-contract.ts @@ -71,7 +71,10 @@ const storage = Object.freeze({ serializeFailed: 'storage.serialize-failed', } as const); -/** Stable codes carried by toolkit diagnostics. */ +/** + * Stable codes carried by toolkit diagnostics. + * @link https://js-toolkit-v4.studiometa.dev/api/diagnostics/DIAGNOSTICS.html + */ export const DIAGNOSTICS = Object.freeze({ attribute, callback, diff --git a/packages/v4/src/diagnostics.ts b/packages/v4/src/diagnostics.ts index c6959125..a006e16b 100644 --- a/packages/v4/src/diagnostics.ts +++ b/packages/v4/src/diagnostics.ts @@ -49,6 +49,7 @@ function dispatchDiagnostic( * A component reports through {@link Base.$error}, which fills in its own name * and element. This is the form for code with no instance to report as — a * module-level helper, or a class which is not a `Base`. + * @link https://js-toolkit-v4.studiometa.dev/api/diagnostics/reportDiagnostic.html */ export function reportDiagnostic( code: ToolkitDiagnosticCode, @@ -88,6 +89,7 @@ export function isolateCallbackFailure( * A component reports through {@link Base.$warn}, which fills in its own name * and element. This is the form for code with no instance to report as — a * module-level helper, or a class which is not a `Base`. + * @link https://js-toolkit-v4.studiometa.dev/api/diagnostics/warn.html */ export function warn( code: ToolkitDiagnosticCode, diff --git a/packages/v4/src/dom-mutations.ts b/packages/v4/src/dom-mutations.ts index 7e28404e..417484d6 100644 --- a/packages/v4/src/dom-mutations.ts +++ b/packages/v4/src/dom-mutations.ts @@ -163,6 +163,7 @@ export function replaceDOMOptionAttributes( * @param el The element to observe. * @param callback Called once per coalesced attribute change. * @returns An idempotent cleanup function. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/watchAttributes.html */ export function watchAttributes(el: Element, callback: AttributeWatcher): () => void { const entry: AttributeWatcherEntry = { @@ -360,6 +361,7 @@ export function trackDOMLifecycleWork(work: Promise | undefined): void * * This does not wait for visibility, interaction, idle, or media * conditions, and it does not await promises returned by `mounted()`. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/whenDOMSettled.html */ export async function whenDOMSettled(): Promise { const currentObserver = observe(); diff --git a/packages/v4/src/events.ts b/packages/v4/src/events.ts index 5c24757b..dfd817e0 100644 --- a/packages/v4/src/events.ts +++ b/packages/v4/src/events.ts @@ -7,7 +7,10 @@ const dom = Object.freeze({ update: 'js-toolkit:dom:update', } as const); -/** Public framework event names. */ +/** + * Public framework event names. + * @link https://js-toolkit-v4.studiometa.dev/api/diagnostics/EVENTS.html + */ export const EVENTS = Object.freeze({ component, dom, diff --git a/packages/v4/src/group.ts b/packages/v4/src/group.ts index 42039d85..d1cace44 100644 --- a/packages/v4/src/group.ts +++ b/packages/v4/src/group.ts @@ -51,6 +51,7 @@ export interface Group { * A member leaves through the function `join()` returned; nothing sweeps * disconnected elements, because v4 unmounts a component when its element * leaves the DOM and the member's own teardown is what removes it. + * @link https://js-toolkit-v4.studiometa.dev/api/context/createGroup.html */ export function createGroup(): Group { const joined = new Set(); diff --git a/packages/v4/src/instances.ts b/packages/v4/src/instances.ts index 96b4d1d0..1f47e9da 100644 --- a/packages/v4/src/instances.ts +++ b/packages/v4/src/instances.ts @@ -45,6 +45,7 @@ function collect( * it is a `querySelectorAll`. A **detached** element is therefore unreachable * by this form even though it still carries its instance — pass the element * to the overload below, or pass its detached root as `root`. + * @link https://js-toolkit-v4.studiometa.dev/api/registry/getInstances.html */ export function getInstances(name: string, root?: ParentNode): T[]; /** @@ -75,6 +76,7 @@ export function getInstances( * counted or inspected. * * Scoping and the detached-element blind spot are {@link getInstances}'. + * @link https://js-toolkit-v4.studiometa.dev/api/registry/getMountedInstances.html */ export function getMountedInstances(name: string, root?: ParentNode): T[]; /** The live instances on one element, in mount order. Works when detached. */ @@ -100,6 +102,7 @@ export function getMountedInstances( * declaration withdrawn by a breakpoint. Neither has an instance at all. * * Scoping and the detached-element blind spot are {@link getInstances}'. + * @link https://js-toolkit-v4.studiometa.dev/api/registry/getUnmountedInstances.html */ export function getUnmountedInstances(name: string, root?: ParentNode): T[]; /** The built-but-unmounted instances on one element, in mount order. */ @@ -135,6 +138,7 @@ export function getUnmountedInstances( * There is deliberately no `getMountedInstance`. The result is one object, so * a caller who needs the live one reads `.$isMounted` on it — a second export * would only hide that check behind a `undefined` that means two things. + * @link https://js-toolkit-v4.studiometa.dev/api/registry/getInstance.html */ export function getInstance( el: Element | null, diff --git a/packages/v4/src/manifest.ts b/packages/v4/src/manifest.ts index 234d576f..15fb4b98 100644 --- a/packages/v4/src/manifest.ts +++ b/packages/v4/src/manifest.ts @@ -30,6 +30,7 @@ function deriveToken(path: string): string { /** * Build a lazy component manifest from bundler module importers without importing or registering * anything. The registry resolves direct classes, named exports and default exports when needed. + * @link https://js-toolkit-v4.studiometa.dev/api/registry/defineManifest.html */ export function defineManifest({ modules, @@ -61,7 +62,10 @@ export function defineManifest({ return manifest; } -/** Return a lazy Vite glob unchanged, and reject eager glob values immediately. */ +/** + * Return a lazy Vite glob unchanged, and reject eager glob values immediately. + * @link https://js-toolkit-v4.studiometa.dev/api/registry/fromMetaGlob.html + */ export function fromMetaGlob(glob: Record): ModuleRecord { for (const [path, importer] of Object.entries(glob)) { if (typeof importer !== 'function') { @@ -73,7 +77,10 @@ export function fromMetaGlob(glob: Record): ModuleRecord { return glob as ModuleRecord; } -/** Convert a webpack context to lazy importers without loading any matched module. */ +/** + * Convert a webpack context to lazy importers without loading any matched module. + * @link https://js-toolkit-v4.studiometa.dev/api/registry/fromWebpackContext.html + */ export function fromWebpackContext(context: WebpackContextLike): ModuleRecord { return Object.fromEntries( context.keys().map((key) => [key, () => Promise.resolve(context(key))]), diff --git a/packages/v4/src/negotiated-events.ts b/packages/v4/src/negotiated-events.ts index 7d2bda5d..80a89ad2 100644 --- a/packages/v4/src/negotiated-events.ts +++ b/packages/v4/src/negotiated-events.ts @@ -103,6 +103,7 @@ export interface ExtendableDetail { /** * Announce a DOM change and apply it exactly once. * The mutation runs synchronously when no listener claims it. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/domUpdate.html */ export async function domUpdate( target: Node, @@ -171,6 +172,7 @@ export async function domUpdate( /** * Announce a step and await all registered extensions. * Extension failures are reported and do not reject this function. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/emitExtendable.html */ export async function emitExtendable( target: Node, diff --git a/packages/v4/src/registry.ts b/packages/v4/src/registry.ts index 76fa8c3b..692df533 100644 --- a/packages/v4/src/registry.ts +++ b/packages/v4/src/registry.ts @@ -230,6 +230,7 @@ if (!registryState.isReplacementListenerAttached) { * `INSTANCES` map key it publishes itself under: a subclass which extends a * component with extra config and forgets to rename would otherwise register * under `undefined` instead of colliding with the name it inherited. + * @link https://js-toolkit-v4.studiometa.dev/api/registry/registerComponent.html */ export function registerComponent(ComponentClass: BaseConstructor): void { const { name } = resolveConfig(ComponentClass); @@ -255,6 +256,7 @@ export function registerComponent(ComponentClass: BaseConstructor): void { scanName(document.documentElement, name); } +/** @link https://js-toolkit-v4.studiometa.dev/api/registry/registerComponents.html */ export function registerComponents(...classes: BaseConstructor[]): void { for (const ComponentClass of classes) { registerComponent(ComponentClass); @@ -263,6 +265,7 @@ export function registerComponents(...classes: BaseConstructor[]): void { /** * Register lazy component entries. Element `data-mount` overrides the entry strategy; unloaded components have no instance. + * @link https://js-toolkit-v4.studiometa.dev/api/registry/registerManifest.html */ export function registerManifest(entries: ComponentManifest): void { const added: string[] = []; diff --git a/packages/v4/src/scheduler.ts b/packages/v4/src/scheduler.ts index 55883f69..2bdc9f27 100644 --- a/packages/v4/src/scheduler.ts +++ b/packages/v4/src/scheduler.ts @@ -308,13 +308,20 @@ export class Scheduler { } } +/** + * The one frame-aligned scheduler: the clock of the framework. + * @link https://js-toolkit-v4.studiometa.dev/api/scheduler/defaultScheduler.html + */ export const defaultScheduler = /* @__PURE__ */ getSharedRuntimeSlot( 'scheduler:default', 1, () => new Scheduler(), ); -/** Await the next animation frame. */ +/** + * Await the next animation frame. + * @link https://js-toolkit-v4.studiometa.dev/api/scheduler/nextFrame.html + */ export function nextFrame(): Promise { return new Promise((resolve) => requestAnimationFrame(() => resolve())); } diff --git a/packages/v4/src/services/breakpoint.ts b/packages/v4/src/services/breakpoint.ts index a729e236..22f47e4f 100644 --- a/packages/v4/src/services/breakpoint.ts +++ b/packages/v4/src/services/breakpoint.ts @@ -4,6 +4,7 @@ import { createService, type MutableProps, type Service } from './service.js'; /** * Default named viewport widths, ascending. Media-query `rem` uses the browser's initial font size, not the root element's computed size. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/breakpoints.html */ export const BREAKPOINTS: Readonly> = { xxs: '0rem', @@ -67,7 +68,10 @@ function queryList(): Array { return breakpointState.queries; } -/** Replace the ascending named breakpoint set and refresh active subscribers. */ +/** + * Replace the ascending named breakpoint set and refresh active subscribers. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/breakpoints.html + */ export function setBreakpoints(next: Record): void { breakpointState.breakpoints = { ...next }; breakpointState.queries = null; @@ -81,7 +85,10 @@ export function setBreakpoints(next: Record): void { breakpointState.refresh?.(); } -/** Return a copy of the active breakpoint set. */ +/** + * Return a copy of the active breakpoint set. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/breakpoints.html + */ export function getBreakpoints(): Record { return { ...breakpointState.breakpoints }; } @@ -165,6 +172,7 @@ function createBreakpointService(): Service { /** * Use the viewport breakpoint service. It listens to media-query crossings, including changes caused by the browser's initial font size. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useBreakpoint.html */ export function useBreakpoint(): Service { breakpointState.service ??= createBreakpointService(); diff --git a/packages/v4/src/services/drag.ts b/packages/v4/src/services/drag.ts index 1fde3ecc..0cb712d6 100644 --- a/packages/v4/src/services/drag.ts +++ b/packages/v4/src/services/drag.ts @@ -14,7 +14,10 @@ import { createService, perTarget, type MutableProps, type Service } from './ser /** Anything that can be grabbed: an HTML element, or an SVG one. */ export type DragTarget = HTMLElement | SVGElement; -/** Drag lifecycle modes: `idle → start → drag → drop → inertia → stop → idle`. */ +/** + * Drag lifecycle modes: `idle → start → drag → drop → inertia → stop → idle`. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useDrag.html + */ export const DRAG_MODES = { IDLE: 'idle', START: 'start', @@ -387,7 +390,10 @@ const dragServices = /* @__PURE__ */ getSharedRuntimeSlot('service:drag', 1, () perTarget(createDragService), ); -/** Use one shared drag service per target and option set. */ +/** + * Use one shared drag service per target and option set. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useDrag.html + */ export function useDrag(target: DragTarget, options: DragOptions = {}): Service { return dragServices(target, options); } @@ -399,7 +405,10 @@ export interface DragHook { export type DragMixinOptions = DragOptions & ServiceMixinOptions; -/** Subscribe `dragged()` to the drag service for each mount cycle. The root element is the default target. */ +/** + * Subscribe `dragged()` to the drag service for each mount cycle. The root element is the default target. + * @link https://js-toolkit-v4.studiometa.dev/api/services/mixins.html + */ export const withDrag = /* @__PURE__ */ createServiceMixin< DragHook & ServiceHandles<'dragged'>, DragTarget, diff --git a/packages/v4/src/services/in-view.ts b/packages/v4/src/services/in-view.ts index be36d453..5cda3097 100644 --- a/packages/v4/src/services/in-view.ts +++ b/packages/v4/src/services/in-view.ts @@ -101,7 +101,10 @@ const inViewState = /* @__PURE__ */ getSharedRuntimeSlot( }, ); -/** Observe intersection through one lazy service per target and observer options. */ +/** + * Observe intersection through one lazy service per target and observer options. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useInView.html + */ export function useInView( target: Element, init: IntersectionObserverInit = {}, @@ -118,6 +121,7 @@ export type InViewMixinOptions = IntersectionObserverInit & ServiceMixinOptions< /** * Subscribe `intersected()` for each mount cycle. The root element is the default target, and immediate delivery waits for a real observer entry. + * @link https://js-toolkit-v4.studiometa.dev/api/services/mixins.html */ export const withInView = /* @__PURE__ */ createServiceMixin< InViewHook & ServiceHandles<'intersected'>, diff --git a/packages/v4/src/services/key.ts b/packages/v4/src/services/key.ts index 79d937e1..2c2d313c 100644 --- a/packages/v4/src/services/key.ts +++ b/packages/v4/src/services/key.ts @@ -129,7 +129,10 @@ const keyServices = /* @__PURE__ */ getSharedRuntimeSlot('service:key', 1, () => perTarget(createKeyService), ); -/** Use one key service per target. Defaults to the document. */ +/** + * Use one key service per target. Defaults to the document. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useKey.html + */ export function useKey(target: KeyTarget = document): Service { return keyServices(target); } @@ -146,6 +149,7 @@ export type KeyMixinOptions = ServiceMixinOptions; * as in v3 and as `withScroll` and `withResize` do for their own page-wide * source. Scope it to a region with a target: * `withKey(Base, { target: (instance) => instance.$refs.wrapper })`. + * @link https://js-toolkit-v4.studiometa.dev/api/services/mixins.html */ export const withKey = /* @__PURE__ */ createServiceMixin< KeyHook & ServiceHandles<'keyed'>, diff --git a/packages/v4/src/services/media.ts b/packages/v4/src/services/media.ts index c7e662f8..c77c5909 100644 --- a/packages/v4/src/services/media.ts +++ b/packages/v4/src/services/media.ts @@ -40,7 +40,10 @@ function createMediaQueryService(query: string): Service { }); } -/** Use one shared service per normalized media query. */ +/** + * Use one shared service per normalized media query. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useMediaQuery.html + */ export function useMediaQuery(query: string): Service { const key = query.trim(); let service = services.get(key); @@ -51,7 +54,10 @@ export function useMediaQuery(query: string): Service { return service; } -/** Use the `(prefers-reduced-motion: reduce)` media-query service. */ +/** + * Use the `(prefers-reduced-motion: reduce)` media-query service. + * @link https://js-toolkit-v4.studiometa.dev/api/services/usePrefersReducedMotion.html + */ export function usePrefersReducedMotion(): Service { return useMediaQuery(REDUCED_MOTION); } diff --git a/packages/v4/src/services/mixin.ts b/packages/v4/src/services/mixin.ts index 6d656dea..c727cb92 100644 --- a/packages/v4/src/services/mixin.ts +++ b/packages/v4/src/services/mixin.ts @@ -122,6 +122,7 @@ export type MixedClass = Pick & /** * Build a lifecycle-bound service mixin. Automatic subscriptions last for one mount cycle. Manual subscriptions also stop on unmount. + * @link https://js-toolkit-v4.studiometa.dev/api/services/createServiceMixin.html */ export function createServiceMixin( definition: ServiceMixinDefinition, diff --git a/packages/v4/src/services/mutation.ts b/packages/v4/src/services/mutation.ts index b19c814a..5190fce8 100644 --- a/packages/v4/src/services/mutation.ts +++ b/packages/v4/src/services/mutation.ts @@ -102,6 +102,7 @@ const mutationServices = /* @__PURE__ */ getSharedRuntimeSlot('service:mutation' * internal observer handles component discovery and declared options; nothing * here replaces it, and a subscriber that needs the framework's own ordering * awaits `whenDOMSettled()` from its callback. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useMutation.html */ export function useMutation( target: Node, @@ -120,6 +121,7 @@ export type MutationMixinOptions = MutationObserverInit & ServiceMixinOptions, diff --git a/packages/v4/src/services/pointer.ts b/packages/v4/src/services/pointer.ts index bfbac179..f41ac1c8 100644 --- a/packages/v4/src/services/pointer.ts +++ b/packages/v4/src/services/pointer.ts @@ -223,7 +223,10 @@ const pointerState = /* @__PURE__ */ getSharedRuntimeSlot( }), ); -/** Use the viewport-relative pointer service. */ +/** + * Use the viewport-relative pointer service. + * @link https://js-toolkit-v4.studiometa.dev/api/services/usePointer.html + */ export function usePointer(): Service; /** Use one lazy service per target, adding the pointer's position inside its box. */ export function usePointer(target: Element): Service; @@ -244,6 +247,7 @@ export type PointerMixinOptions = ServiceMixinOptions; /** * Subscribe `moved()` to the pointer for each mount cycle. The root element is the default target, so a component reads the pointer both in the viewport and inside its own box. + * @link https://js-toolkit-v4.studiometa.dev/api/services/mixins.html */ export const withPointer = /* @__PURE__ */ createServiceMixin< PointerHook & ServiceHandles<'moved'>, diff --git a/packages/v4/src/services/raf.ts b/packages/v4/src/services/raf.ts index a8ebcb94..226ff5ac 100644 --- a/packages/v4/src/services/raf.ts +++ b/packages/v4/src/services/raf.ts @@ -70,7 +70,10 @@ const rafState = /* @__PURE__ */ getSharedRuntimeSlot<{ service: RafService | undefined; }>('service:raf', 1, () => ({ service: undefined })); -/** Use the scheduler-backed frame service. Returned functions run in the write phase. */ +/** + * Use the scheduler-backed frame service. Returned functions run in the write phase. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useRaf.html + */ export function useRaf(): RafService { rafState.service ??= createRafService(); return rafState.service; @@ -83,7 +86,10 @@ export interface RafHook { export type RafMixinOptions = ServiceMixinOptions; -/** Subscribe `ticked()` to the frame service for each mount cycle. */ +/** + * Subscribe `ticked()` to the frame service for each mount cycle. + * @link https://js-toolkit-v4.studiometa.dev/api/services/mixins.html + */ export const withRaf = /* @__PURE__ */ createServiceMixin, void>( { hook: 'ticked', diff --git a/packages/v4/src/services/resize.ts b/packages/v4/src/services/resize.ts index 72fa45f5..190faa49 100644 --- a/packages/v4/src/services/resize.ts +++ b/packages/v4/src/services/resize.ts @@ -85,7 +85,10 @@ const resizeServices = /* @__PURE__ */ getSharedRuntimeSlot('service:resize', 1, perTarget(createResizeService), ); -/** Use one resize service per element. Defaults to the document element. */ +/** + * Use one resize service per element. Defaults to the document element. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useResize.html + */ export function useResize(target: Element = document.documentElement): Service { return resizeServices(target); } @@ -93,6 +96,7 @@ export function useResize(target: Element = document.documentElement): Service { return resizeServices(document.documentElement); @@ -105,7 +109,10 @@ export interface ResizeHook { export type ResizeMixinOptions = ServiceMixinOptions; -/** Subscribe `resized()` for each mount cycle. The document element is the default target. */ +/** + * Subscribe `resized()` for each mount cycle. The document element is the default target. + * @link https://js-toolkit-v4.studiometa.dev/api/services/mixins.html + */ export const withResize = /* @__PURE__ */ createServiceMixin< ResizeHook & ServiceHandles<'resized'>, Element diff --git a/packages/v4/src/services/scroll-progress.ts b/packages/v4/src/services/scroll-progress.ts index cadc71d8..2b8937b4 100644 --- a/packages/v4/src/services/scroll-progress.ts +++ b/packages/v4/src/services/scroll-progress.ts @@ -157,6 +157,7 @@ const scrollProgressServices = /* @__PURE__ */ getSharedRuntimeSlot( * The service is lazy and shared by target plus resolved offset. It measures * geometry in the scheduler's read phase and releases its observers and shared * source subscriptions with its last subscriber. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useScrollProgress.html */ export function useScrollProgress( target: Element, @@ -167,6 +168,7 @@ export function useScrollProgress( /** * Subscribe `scrolledInView()` to raw viewport progress for each mount cycle. Returned mutations run through the instance write lane. + * @link https://js-toolkit-v4.studiometa.dev/api/services/mixins.html */ export const withScrollProgress = /* @__PURE__ */ createServiceMixin< ScrollProgressHook & ServiceHandles<'scrolledInView'>, diff --git a/packages/v4/src/services/scroll.ts b/packages/v4/src/services/scroll.ts index 74f805c9..25c0232d 100644 --- a/packages/v4/src/services/scroll.ts +++ b/packages/v4/src/services/scroll.ts @@ -204,7 +204,10 @@ const scrollServices = /* @__PURE__ */ getSharedRuntimeSlot('service:scroll', 1, perTarget(createScrollService), ); -/** Use one scroll service per target. Defaults to the window. */ +/** + * Use one scroll service per target. Defaults to the window. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useScroll.html + */ export function useScroll(target: ScrollTarget = window): Service { // The document scrolling element uses the window service. if (target === document.scrollingElement || target === document.documentElement) { @@ -215,6 +218,7 @@ export function useScroll(target: ScrollTarget = window): Service { /** * Use the scroll service for the window. + * @link https://js-toolkit-v4.studiometa.dev/api/services/useWindowScroll.html */ export function useWindowScroll(): Service { return scrollServices(window); @@ -227,7 +231,10 @@ export interface ScrollHook { export type ScrollMixinOptions = ServiceMixinOptions; -/** Subscribe `scrolled()` for each mount cycle. The window is the default target. */ +/** + * Subscribe `scrolled()` for each mount cycle. The window is the default target. + * @link https://js-toolkit-v4.studiometa.dev/api/services/mixins.html + */ export const withScroll = /* @__PURE__ */ createServiceMixin< ScrollHook & ServiceHandles<'scrolled'>, ScrollTarget diff --git a/packages/v4/src/services/service.ts b/packages/v4/src/services/service.ts index 072a872c..39259087 100644 --- a/packages/v4/src/services/service.ts +++ b/packages/v4/src/services/service.ts @@ -54,6 +54,7 @@ interface Subscription { /** * Build a service from its definition. + * @link https://js-toolkit-v4.studiometa.dev/api/services/createService.html */ export function createService({ props, @@ -178,6 +179,7 @@ export function stableKey(...args: unknown[]): string { * * @param create Called once per target and argument pair. * @param keyOf Serializes arguments. Defaults to {@link stableKey}. + * @link https://js-toolkit-v4.studiometa.dev/api/services/perTarget.html */ export function perTarget( create: (target: Target, ...args: Args) => Service, diff --git a/packages/v4/src/services/toggle.ts b/packages/v4/src/services/toggle.ts index b6e529b9..59e31ea2 100644 --- a/packages/v4/src/services/toggle.ts +++ b/packages/v4/src/services/toggle.ts @@ -14,6 +14,7 @@ export interface Toggle { * Make an unsubscribe-producing operation suspendable and resumable. * * `start()` and `stop()` are idempotent. + * @link https://js-toolkit-v4.studiometa.dev/api/services/toggle.html */ export function toggle(subscribe: () => Unsubscribe): Toggle { let unsubscribe: Unsubscribe | null = null; diff --git a/packages/v4/src/services/until.ts b/packages/v4/src/services/until.ts index b0a4fb32..385cbd17 100644 --- a/packages/v4/src/services/until.ts +++ b/packages/v4/src/services/until.ts @@ -4,6 +4,7 @@ import type { Service, Unsubscribe } from './service.js'; * Wait for current or future service props that satisfy a predicate. * * The subscription is released before resolution. Object props are copied because services can reuse them. + * @link https://js-toolkit-v4.studiometa.dev/api/services/until.html */ export function until(service: Service, predicate: (props: T) => boolean): Promise { return new Promise((resolve) => { diff --git a/packages/v4/src/storage/createStorage.ts b/packages/v4/src/storage/createStorage.ts index 8d6e945f..422a4c9d 100644 --- a/packages/v4/src/storage/createStorage.ts +++ b/packages/v4/src/storage/createStorage.ts @@ -11,6 +11,7 @@ import type { StorageInstance, StorageOptions } from './types.js'; * * Values are namespaced by `prefix`, serialized on the way in and out, and * observable per key. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/createStorage.html */ export function createStorage>( options: StorageOptions = {}, diff --git a/packages/v4/src/storage/index.ts b/packages/v4/src/storage/index.ts index 650f7c32..b5849bfe 100644 --- a/packages/v4/src/storage/index.ts +++ b/packages/v4/src/storage/index.ts @@ -33,21 +33,30 @@ export type { type PresetOptions = Omit; -/** A storage over `localStorage`. */ +/** + * A storage over `localStorage`. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/presets.html + */ export function createLocalStorage>( options?: PresetOptions, ): StorageInstance { return createStorage({ ...options, provider: localStorageProvider }); } -/** A storage over `sessionStorage`. */ +/** + * A storage over `sessionStorage`. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/presets.html + */ export function createSessionStorage>( options?: PresetOptions, ): StorageInstance { return createStorage({ ...options, provider: sessionStorageProvider }); } -/** A storage over the query string. */ +/** + * A storage over the query string. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/presets.html + */ export function createUrlSearchParamsStorage>( options?: PresetOptions & UrlProviderOptions, ): StorageInstance { @@ -58,7 +67,10 @@ export function createUrlSearchParamsStorage>( options?: PresetOptions & UrlProviderOptions, ): StorageInstance { diff --git a/packages/v4/src/storage/providers.ts b/packages/v4/src/storage/providers.ts index 53016f95..796ad26a 100644 --- a/packages/v4/src/storage/providers.ts +++ b/packages/v4/src/storage/providers.ts @@ -46,7 +46,10 @@ function createWebStorageProvider(name: 'localStorage' | 'sessionStorage'): Stor }; } -/** A provider backed by a `Map`, for tests and for opting out of persistence. */ +/** + * A provider backed by a `Map`, for tests and for opting out of persistence. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/providers.html + */ export function createMemoryStorageProvider(): StorageProvider { const map = new Map(); @@ -60,7 +63,10 @@ export function createMemoryStorageProvider(): StorageProvider { }; } -/** Read from the first provider holding the key; write through to all of them. */ +/** + * Read from the first provider holding the key; write through to all of them. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/providers.html + */ export function createFallbackProvider(...providers: StorageProvider[]): StorageProvider { const syncEvents = [...new Set(providers.flatMap((provider) => provider.syncEvents ?? []))]; @@ -129,7 +135,10 @@ function createUrlProvider( }; } -/** Store values in the query string. */ +/** + * Store values in the query string. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/providers.html + */ export function createUrlSearchParamsProvider(options: UrlProviderOptions = {}): StorageProvider { return createUrlProvider( () => new URLSearchParams(location.search), @@ -147,6 +156,7 @@ export function createUrlSearchParamsProvider(options: UrlProviderOptions = {}): * * Back and forward navigation announces itself as `hashchange`, and as * `popstate` when the write replaced the entry rather than pushing one. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/providers.html */ export function createUrlSearchParamsInHashProvider( options: UrlProviderOptions = {}, @@ -174,8 +184,28 @@ const defaults = /* @__PURE__ */ getSharedRuntimeSlot('storage:providers', 1, () urlSearchParamsInHash: createUrlSearchParamsInHashProvider(), })); +/** + * A storage provider over `localStorage`. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/providers.html + */ export const localStorageProvider = defaults.local; +/** + * A storage provider over `sessionStorage`. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/providers.html + */ export const sessionStorageProvider = defaults.session; +/** + * The shared in-memory storage provider. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/providers.html + */ export const memoryStorageProvider = defaults.memory; +/** + * A storage provider over `location.search`. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/providers.html + */ export const urlSearchParamsProvider = defaults.urlSearchParams; +/** + * A storage provider over `location.hash`, read as search params. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/providers.html + */ export const urlSearchParamsInHashProvider = defaults.urlSearchParamsInHash; diff --git a/packages/v4/src/storage/serializers.ts b/packages/v4/src/storage/serializers.ts index 0d282846..465ffe8b 100644 --- a/packages/v4/src/storage/serializers.ts +++ b/packages/v4/src/storage/serializers.ts @@ -3,6 +3,7 @@ import type { StorageSerializer } from './types.js'; /** * The default serializer. `deserialize` throws on malformed input so the * storage instance can name the key it failed on. + * @link https://js-toolkit-v4.studiometa.dev/api/storage/createStorage.html */ export const jsonSerializer: StorageSerializer = { serialize: (value) => JSON.stringify(value), diff --git a/packages/v4/src/swap.ts b/packages/v4/src/swap.ts index 5add6094..76560d03 100644 --- a/packages/v4/src/swap.ts +++ b/packages/v4/src/swap.ts @@ -2,7 +2,10 @@ import morphdom from 'morphdom'; import { warn } from './diagnostics.js'; import { whenDOMSettled } from './dom-mutations.js'; -/** Named content swap modes. */ +/** + * Named content swap modes. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/swap.html + */ export const SWAP_MODES = { REPLACE: 'replace', PREPEND: 'prepend', @@ -43,6 +46,7 @@ export interface SwapOptions { * @param target The element whose content changes. With `self`, the element itself. * @param content The new content. * @returns Resolves after eager lifecycle work. Conditional mount strategies are not awaited. + * @link https://js-toolkit-v4.studiometa.dev/api/dom/swap.html */ export async function swap( target: Element, diff --git a/packages/v4/src/utils/deepmerge.ts b/packages/v4/src/utils/deepmerge.ts index 8b7b6de8..da157a41 100644 --- a/packages/v4/src/utils/deepmerge.ts +++ b/packages/v4/src/utils/deepmerge.ts @@ -83,6 +83,7 @@ function merge( * deepmerge({ ecommerce: { currency: 'EUR' } }, { ecommerce: { items: [1] } }); * // { ecommerce: { currency: 'EUR', items: [1] } } * ``` + * @link https://js-toolkit-v4.studiometa.dev/utils/objects.html#deepmerge */ export function deepmerge(...layers: Record[]): Record { return layers.reduce>(merge, {}); diff --git a/packages/v4/src/utils/dom.ts b/packages/v4/src/utils/dom.ts index 1ae597e9..f742d59c 100644 --- a/packages/v4/src/utils/dom.ts +++ b/packages/v4/src/utils/dom.ts @@ -29,6 +29,7 @@ export interface CreateElementAttributes { * createElement('a', 'link'); // link * createElement('a', { href: '#' }, [createElement('span')]); // * ``` + * @link https://js-toolkit-v4.studiometa.dev/utils/dom.html#createelement */ export function createElement( tag?: T, @@ -88,6 +89,7 @@ export interface OffsetSizes { * nothing were transformed. Coordinates are the viewport's, as that method's. * * A pure read: the caller owns the phase it happens in. + * @link https://js-toolkit-v4.studiometa.dev/utils/css.html#getoffsetsizes */ export function getOffsetSizes(element: HTMLElement): OffsetSizes { let x = element.offsetLeft; diff --git a/packages/v4/src/utils/easings.ts b/packages/v4/src/utils/easings.ts index 3f63b6e5..17db7dae 100644 --- a/packages/v4/src/utils/easings.ts +++ b/packages/v4/src/utils/easings.ts @@ -7,12 +7,18 @@ /** Takes a progress between `0` and `1`, answers an eased value in that range. */ export type EasingFunction = (progress: number) => number; -/** Mirror an ease-in function into its ease-out counterpart. */ +/** + * Mirror an ease-in function into its ease-out counterpart. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#createeaseout + */ export function createEaseOut(easeIn: EasingFunction): EasingFunction { return (progress) => 1 - easeIn(1 - progress); } -/** Join an ease-in function with its mirror, each over half the progress. */ +/** + * Join an ease-in function with its mirror, each over half the progress. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#createeaseinout + */ export function createEaseInOut(easeIn: EasingFunction): EasingFunction { return (progress) => { if (progress === 0 || progress === 1) return progress; @@ -20,58 +26,145 @@ export function createEaseInOut(easeIn: EasingFunction): EasingFunction { }; } -/** No easing: the progress is the value. */ +/** + * No easing: the progress is the value. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easelinear + */ export function easeLinear(progress: number): number { return progress; } +/** + * Quadratic ease-in. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinquad + */ export function easeInQuad(progress: number): number { return progress ** 2; } +/** + * Quadratic ease-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeoutquad + */ export const easeOutQuad = /* @__PURE__ */ createEaseOut(easeInQuad); +/** + * Quadratic ease-in-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinoutquad + */ export const easeInOutQuad = /* @__PURE__ */ createEaseInOut(easeInQuad); +/** + * Cubic ease-in. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeincubic + */ export function easeInCubic(progress: number): number { return progress ** 3; } +/** + * Cubic ease-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeoutcubic + */ export const easeOutCubic = /* @__PURE__ */ createEaseOut(easeInCubic); +/** + * Cubic ease-in-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinoutcubic + */ export const easeInOutCubic = /* @__PURE__ */ createEaseInOut(easeInCubic); +/** + * Quartic ease-in. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinquart + */ export function easeInQuart(progress: number): number { return progress ** 4; } +/** + * Quartic ease-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeoutquart + */ export const easeOutQuart = /* @__PURE__ */ createEaseOut(easeInQuart); +/** + * Quartic ease-in-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinoutquart + */ export const easeInOutQuart = /* @__PURE__ */ createEaseInOut(easeInQuart); +/** + * Quintic ease-in. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinquint + */ export function easeInQuint(progress: number): number { return progress ** 5; } +/** + * Quintic ease-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeoutquint + */ export const easeOutQuint = /* @__PURE__ */ createEaseOut(easeInQuint); +/** + * Quintic ease-in-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinoutquint + */ export const easeInOutQuint = /* @__PURE__ */ createEaseInOut(easeInQuint); +/** + * Sinusoidal ease-in. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinsine + */ export function easeInSine(progress: number): number { // Exact at the end, which the cosine is not. return progress === 1 ? 1 : 1 - Math.cos((progress * Math.PI) / 2); } +/** + * Sinusoidal ease-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeoutsine + */ export const easeOutSine = /* @__PURE__ */ createEaseOut(easeInSine); +/** + * Sinusoidal ease-in-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinoutsine + */ export const easeInOutSine = /* @__PURE__ */ createEaseInOut(easeInSine); +/** + * Circular ease-in. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeincirc + */ export function easeInCirc(progress: number): number { return 1 - Math.sqrt(1 - progress * progress); } +/** + * Circular ease-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeoutcirc + */ export const easeOutCirc = /* @__PURE__ */ createEaseOut(easeInCirc); +/** + * Circular ease-in-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinoutcirc + */ export const easeInOutCirc = /* @__PURE__ */ createEaseInOut(easeInCirc); +/** + * Exponential ease-in. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinexpo + */ export function easeInExpo(progress: number): number { // Exact at the start, where the power would answer 1/1024. return progress === 0 ? 0 : 2 ** (10 * (progress - 1)); } +/** + * Exponential ease-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeoutexpo + */ export const easeOutExpo = /* @__PURE__ */ createEaseOut(easeInExpo); +/** + * Exponential ease-in-out. + * @link https://js-toolkit-v4.studiometa.dev/utils/easings.html#easeinoutexpo + */ export const easeInOutExpo = /* @__PURE__ */ createEaseInOut(easeInExpo); diff --git a/packages/v4/src/utils/focus.ts b/packages/v4/src/utils/focus.ts index d128b760..eee43fe9 100644 --- a/packages/v4/src/utils/focus.ts +++ b/packages/v4/src/utils/focus.ts @@ -25,6 +25,7 @@ const state = /* @__PURE__ */ getSharedRuntimeSlot('focus', 1, () => ({ /** * Remember what had the focus, to restore it later. + * @link https://js-toolkit-v4.studiometa.dev/utils/focus.html#saveactiveelement */ export function saveActiveElement(): void { state.focusedBefore = document.activeElement; @@ -32,6 +33,7 @@ export function saveActiveElement(): void { /** * Keep tab navigation inside an element. + * @link https://js-toolkit-v4.studiometa.dev/utils/focus.html#trapfocus */ export function trapFocus(el: HTMLElement, event: KeyboardEvent): void { if (event.key !== 'Tab') { @@ -65,6 +67,7 @@ export function trapFocus(el: HTMLElement, event: KeyboardEvent): void { /** * Give the focus back to whatever had it before the trap. + * @link https://js-toolkit-v4.studiometa.dev/utils/focus.html#untrapfocus */ export function untrapFocus(): void { if (state.focusedBefore instanceof HTMLElement) { diff --git a/packages/v4/src/utils/history.ts b/packages/v4/src/utils/history.ts index c6fb32ec..9cfa2bad 100644 --- a/packages/v4/src/utils/history.ts +++ b/packages/v4/src/utils/history.ts @@ -63,6 +63,7 @@ function updateUrlSearchParam( * objectToURLSearchParams({ filters: { color: 'red' } }, '').toString(); * // filters%5Bcolor%5D=red * ``` + * @link https://js-toolkit-v4.studiometa.dev/utils/history.html#objecttourlsearchparams */ export function objectToURLSearchParams( object: Record, @@ -87,12 +88,18 @@ function urlFor({ path, search, hash }: HistoryOptions): string { return url; } -/** Push a new history entry for the given URL parts. */ +/** + * Push a new history entry for the given URL parts. + * @link https://js-toolkit-v4.studiometa.dev/utils/history.html#historypush + */ export function historyPush(options: HistoryOptions, data: unknown = {}, title = ''): void { history.pushState(data, title, urlFor(options)); } -/** Replace the current history entry with the given URL parts. */ +/** + * Replace the current history entry with the given URL parts. + * @link https://js-toolkit-v4.studiometa.dev/utils/history.html#historyreplace + */ export function historyReplace(options: HistoryOptions, data: unknown = {}, title = ''): void { history.replaceState(data, title, urlFor(options)); } diff --git a/packages/v4/src/utils/is.ts b/packages/v4/src/utils/is.ts index 987f32b0..0044b290 100644 --- a/packages/v4/src/utils/is.ts +++ b/packages/v4/src/utils/is.ts @@ -1,31 +1,49 @@ /** Type guards for the values a component reads from the DOM or its options. */ -/** Whether a value is `null`. */ +/** + * Whether a value is `null`. + * @link https://js-toolkit-v4.studiometa.dev/utils/is.html#isnull + */ export function isNull(value: unknown): value is null { return value === null; } -/** Whether a value is anything but `undefined`. */ +/** + * Whether a value is anything but `undefined`. + * @link https://js-toolkit-v4.studiometa.dev/utils/is.html#isdefined + */ export function isDefined(value: T | undefined): value is T { return typeof value !== 'undefined'; } -/** Whether a value is a string. */ +/** + * Whether a value is a string. + * @link https://js-toolkit-v4.studiometa.dev/utils/is.html#isstring + */ export function isString(value: unknown): value is string { return typeof value === 'string'; } -/** Whether a value is a number, `NaN` excluded. */ +/** + * Whether a value is a number, `NaN` excluded. + * @link https://js-toolkit-v4.studiometa.dev/utils/is.html#isnumber + */ export function isNumber(value: unknown): value is number { return typeof value === 'number' && !Number.isNaN(value); } -/** Whether a value is a boolean. */ +/** + * Whether a value is a boolean. + * @link https://js-toolkit-v4.studiometa.dev/utils/is.html#isboolean + */ export function isBoolean(value: unknown): value is boolean { return typeof value === 'boolean'; } -/** Whether a value is callable. */ +/** + * Whether a value is callable. + * @link https://js-toolkit-v4.studiometa.dev/utils/is.html#isfunction + */ export function isFunction(value: unknown): value is (...args: unknown[]) => unknown { return typeof value === 'function'; } @@ -36,6 +54,7 @@ export function isFunction(value: unknown): value is (...args: unknown[]) => unk * * The tag is read through `Object.prototype`, so an object with no prototype * answers instead of throwing on a missing `toString`. + * @link https://js-toolkit-v4.studiometa.dev/utils/is.html#isobject */ export function isObject(value: unknown): value is Record { return Object.prototype.toString.call(value) === '[object Object]'; diff --git a/packages/v4/src/utils/load.ts b/packages/v4/src/utils/load.ts index 44b0cf16..fa56a2da 100644 --- a/packages/v4/src/utils/load.ts +++ b/packages/v4/src/utils/load.ts @@ -67,6 +67,7 @@ function relIsSupported(rel: string): boolean { * * Nothing is deduplicated here: the HTTP cache already does it, and a detached * element costs nothing to build. + * @link https://js-toolkit-v4.studiometa.dev/utils/load.html#loadimage */ export async function loadImage(src: string): Promise { const image = new Image(); @@ -100,6 +101,7 @@ export async function loadImage(src: string): Promise { * deduplicated by resolved URL, so asking twice for one embed returns the very * same promise and executes the third-party code once. A failed load is * forgotten, so it can be retried. + * @link https://js-toolkit-v4.studiometa.dev/utils/load.html#loadscript */ export function loadScript( src: string, @@ -147,6 +149,7 @@ export function loadScript( * entirely — and a hint is best-effort, so an ignored one is a resolved * promise over an inert element rather than a promise waiting forever for an * event that will never fire. The element is appended either way. + * @link https://js-toolkit-v4.studiometa.dev/utils/load.html#loadlink */ export function loadLink( href: string, diff --git a/packages/v4/src/utils/maths.ts b/packages/v4/src/utils/maths.ts index 2aaa51c5..7a725d20 100644 --- a/packages/v4/src/utils/maths.ts +++ b/packages/v4/src/utils/maths.ts @@ -1,13 +1,20 @@ /** Time-based functions take elapsed milliseconds. */ -/** The default inertia factor. */ +/** + * The default inertia factor. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#default-damp-factor + */ export const DEFAULT_DAMP_FACTOR = 0.85; -/** The 60 Hz reference frame for damping factors, in milliseconds. */ +/** + * The 60 Hz reference frame for damping factors, in milliseconds. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#inertia-frame + */ export const INERTIA_FRAME = 1000 / 60; /** * Clamp a value in a given range. + * @link https://js-toolkit-v4.studiometa.dev/utils/math.html#clamp */ export function clamp(value: number, min: number, max: number): number { if (min < max) { @@ -18,6 +25,7 @@ export function clamp(value: number, min: number, max: number): number { /** * Clamp a value in the 0–1 range. + * @link https://js-toolkit-v4.studiometa.dev/utils/math.html#clamp01 */ export function clamp01(value: number): number { return clamp(value, 0, 1); @@ -25,6 +33,7 @@ export function clamp01(value: number): number { /** * Map a value from one range onto another. + * @link https://js-toolkit-v4.studiometa.dev/utils/math.html#map */ export function map( value: number, @@ -38,6 +47,7 @@ export function map( /** * Interpolate a ratio between two bounds. + * @link https://js-toolkit-v4.studiometa.dev/utils/math.html#lerp */ export function lerp(min: number, max: number, ratio: number): number { return (1 - ratio) * min + ratio * max; @@ -45,6 +55,7 @@ export function lerp(min: number, max: number, ratio: number): number { /** * Wrap a value in a range: it leaves by one bound and comes back by the other. + * @link https://js-toolkit-v4.studiometa.dev/utils/math.html#wrap */ export function wrap(value: number, min: number, max: number): number { const range = max - min; @@ -58,6 +69,7 @@ export function wrap(value: number, min: number, max: number): number { /** * Fold a value back and forth in a range: it bounces off both bounds. + * @link https://js-toolkit-v4.studiometa.dev/utils/math.html#fold */ export function fold(value: number, min: number, max: number): number { const range = max - min; @@ -72,6 +84,7 @@ export function fold(value: number, min: number, max: number): number { /** * Round a value to the given number of decimals. + * @link https://js-toolkit-v4.studiometa.dev/utils/math.html#round */ export function round(value: number, decimals = 0): number { return Number(value.toFixed(decimals)); @@ -79,6 +92,7 @@ export function round(value: number, decimals = 0): number { /** * The arithmetic mean of the given numbers. An empty list averages to `0`. + * @link https://js-toolkit-v4.studiometa.dev/utils/math.html#mean */ export function mean(numbers: readonly number[]): number { if (numbers.length === 0) { @@ -98,6 +112,7 @@ export function mean(numbers: readonly number[]): number { * Each value is computed from the index rather than accumulated, so a * fractional step does not drift. A step which is not positive and finite * describes no range and returns an empty array. + * @link https://js-toolkit-v4.studiometa.dev/utils/math.html#createrange */ export function createRange(min: number, max: number, step: number): number[] { if (!Number.isFinite(step) || step <= 0 || !Number.isFinite(max - min) || max < min) { @@ -112,6 +127,7 @@ export function createRange(min: number, max: number, step: number): number[] { * Return the fraction retained after `elapsed` milliseconds. * * Retention is clamped to `[0, 1]`, elapsed time to non-negative values, and non-finite inputs return `0`. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#decayover */ export function decayOver(retained: number, elapsed: number): number { if (!Number.isFinite(retained) || !Number.isFinite(elapsed)) { @@ -120,7 +136,10 @@ export function decayOver(retained: number, elapsed: number): number { return Math.min(Math.max(retained, 0), 1) ** (Math.max(elapsed, 0) / INERTIA_FRAME); } -/** Return the next time-based damped value and snap within `precision`. */ +/** + * Return the next time-based damped value and snap within `precision`. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#damp + */ export function damp( targetValue: number, currentValue: number, @@ -136,7 +155,10 @@ export function damp( return currentValue + (targetValue - currentValue) * closed; } -/** Clamp an inertia damping factor to a finite decaying range. */ +/** + * Clamp an inertia damping factor to a finite decaying range. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#clampdampfactor + */ export function clampDampFactor(factor: number): number { if (!Number.isFinite(factor)) { return DEFAULT_DAMP_FACTOR; @@ -144,23 +166,33 @@ export function clampDampFactor(factor: number): number { return Math.min(Math.max(factor, 0), 0.99999); } -/** Return the velocity fraction retained after elapsed time. */ +/** + * Return the velocity fraction retained after elapsed time. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#inertiadecay + */ export function inertiaDecay(dampFactor: number, elapsed: number): number { return decayOver(clampDampFactor(dampFactor), elapsed); } -/** Return the inertia decay time constant in milliseconds. */ +/** + * Return the inertia decay time constant in milliseconds. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#inertiatimeconstant + */ export function inertiaTimeConstant(dampFactor: number): number { return INERTIA_FRAME / Math.log(1 / clampDampFactor(dampFactor)); } -/** Integrate one inertia step exactly for velocity in pixels per millisecond. */ +/** + * Integrate one inertia step exactly for velocity in pixels per millisecond. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#inertiastep + */ export function inertiaStep(velocity: number, dampFactor: number, elapsed: number): number { return velocity * inertiaTimeConstant(dampFactor) * (1 - inertiaDecay(dampFactor, elapsed)); } /** * Return the exact coast destination for velocity in pixels per millisecond. The destination remains invariant during the coast. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#inertiafinalvalue */ export function inertiaFinalValue(value: number, velocity: number, dampFactor: number): number { return value + velocity * inertiaTimeConstant(dampFactor); @@ -169,7 +201,10 @@ export function inertiaFinalValue(value: number, velocity: number, dampFactor: n /** Fixed spring integration step in milliseconds. */ const SPRING_STEP = INERTIA_FRAME / 4; -/** Largest stable `stiffness / mass` ratio for {@link SPRING_STEP}. */ +/** + * Largest stable `stiffness / mass` ratio for {@link SPRING_STEP}. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#max-spring-ratio + */ export const MAX_SPRING_RATIO = (4 / (SPRING_STEP / INERTIA_FRAME) ** 2) * 0.9; export interface SpringOptions { @@ -187,6 +222,7 @@ export interface SpringOptions { * Advance a spring by elapsed milliseconds with fixed, bounded substeps. Stiffness-to-mass ratio is clamped for stability. * * Returns the exact target and zero velocity within `precision`. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#spring */ export function spring( targetValue: number, diff --git a/packages/v4/src/utils/memo.ts b/packages/v4/src/utils/memo.ts index b6abc202..c2d120ea 100644 --- a/packages/v4/src/utils/memo.ts +++ b/packages/v4/src/utils/memo.ts @@ -14,6 +14,7 @@ function isWeakKey(value: unknown): value is WeakKey { * Cache a function's result per argument, until told to forget. * * @param fn The function to memoise. It takes one argument, or none. + * @link https://js-toolkit-v4.studiometa.dev/utils/timing.html#memo */ export function memo( fn: (...args: Args) => Value, diff --git a/packages/v4/src/utils/noop.ts b/packages/v4/src/utils/noop.ts index 5c96b0fe..a7721f1f 100644 --- a/packages/v4/src/utils/noop.ts +++ b/packages/v4/src/utils/noop.ts @@ -1,9 +1,15 @@ /** Placeholders for an optional callback, so a caller never branches on one. */ -/** Do nothing, whatever it is called with. */ +/** + * Do nothing, whatever it is called with. + * @link https://js-toolkit-v4.studiometa.dev/utils/timing.html#noop + */ export function noop(): void {} -/** Return the value unaltered: the identity of a transform chain. */ +/** + * Return the value unaltered: the identity of a transform chain. + * @link https://js-toolkit-v4.studiometa.dev/utils/timing.html#noopvalue + */ export function noopValue(value: T): T { return value; } diff --git a/packages/v4/src/utils/random.ts b/packages/v4/src/utils/random.ts index bbdee7f7..7bab1b97 100644 --- a/packages/v4/src/utils/random.ts +++ b/packages/v4/src/utils/random.ts @@ -1,6 +1,9 @@ /** Random values, in the ranges a component describes. */ -/** A random number between the two bounds, the second defaulting to `0`. */ +/** + * A random number between the two bounds, the second defaulting to `0`. + * @link https://js-toolkit-v4.studiometa.dev/utils/objects.html#random + */ export function random(a: number, b = 0): number { return Math.random() * (b - a) + a; } @@ -11,6 +14,7 @@ export function random(a: number, b = 0): number { * * Every integer in the range is equally likely. Rounding a random float * instead — which is what v3 did — gives the two bounds half a chance each. + * @link https://js-toolkit-v4.studiometa.dev/utils/objects.html#randomint */ export function randomInt(a: number, b = 0): number { const min = Math.ceil(Math.min(a, b)); @@ -18,7 +22,10 @@ export function randomInt(a: number, b = 0): number { return min + Math.floor(Math.random() * (max - min + 1)); } -/** A random item of an array, or a random character of a string. */ +/** + * A random item of an array, or a random character of a string. + * @link https://js-toolkit-v4.studiometa.dev/utils/objects.html#randomitem + */ export function randomItem(items: readonly T[]): T | undefined; export function randomItem(items: string): string | undefined; export function randomItem(items: readonly T[] | string): T | string | undefined { diff --git a/packages/v4/src/utils/scroll-lock.ts b/packages/v4/src/utils/scroll-lock.ts index 93f2e4e3..04495642 100644 --- a/packages/v4/src/utils/scroll-lock.ts +++ b/packages/v4/src/utils/scroll-lock.ts @@ -37,6 +37,7 @@ const state = /* @__PURE__ */ getSharedRuntimeSlot('scroll-lock', 1, () => ({ * // …later, whichever comes first * release(); * ``` + * @link https://js-toolkit-v4.studiometa.dev/utils/scroll.html#lockscroll */ export function lockScroll(target: HTMLElement = document.documentElement): () => void { const lock = state.locks.get(target); diff --git a/packages/v4/src/utils/scrollTo.ts b/packages/v4/src/utils/scrollTo.ts index 284f3fef..4a382a72 100644 --- a/packages/v4/src/utils/scrollTo.ts +++ b/packages/v4/src/utils/scrollTo.ts @@ -11,7 +11,10 @@ export interface ScrollPosition { /** What a scroll can be aimed at. */ export type ScrollToTarget = string | Element | number | Partial; -/** The axes a scroll is allowed to move. */ +/** + * The axes a scroll is allowed to move. + * @link https://js-toolkit-v4.studiometa.dev/utils/scroll.html#scroll-axes + */ export const SCROLL_AXES = /* @__PURE__ */ Object.freeze({ x: 'x', y: 'y', @@ -20,7 +23,10 @@ export const SCROLL_AXES = /* @__PURE__ */ Object.freeze({ export type ScrollAxis = (typeof SCROLL_AXES)[keyof typeof SCROLL_AXES]; -/** Where an element comes to rest inside the scroller. */ +/** + * Where an element comes to rest inside the scroller. + * @link https://js-toolkit-v4.studiometa.dev/utils/scroll.html#scroll-alignments + */ export const SCROLL_ALIGNMENTS = /* @__PURE__ */ Object.freeze({ start: 'start', center: 'center', @@ -187,6 +193,7 @@ function requestedPosition( * ```js * const { left } = scrollPosition(slide, { rootElement: track, axis: 'x', align: 'center' }); * ``` + * @link https://js-toolkit-v4.studiometa.dev/utils/scroll.html#scrollposition */ export function scrollPosition( target: ScrollToTarget, @@ -222,6 +229,7 @@ export function scrollPosition( * scrollTo({ left: 0 }, { rootElement: carousel }); * scrollTo(slide, { rootElement: track, axis: 'x', align: 'center' }); * ``` + * @link https://js-toolkit-v4.studiometa.dev/utils/scroll.html#scrollto */ export function scrollTo(target: ScrollToTarget, options: ScrollToOptions = {}): ScrollPosition { const { rootElement = window, behavior } = options; diff --git a/packages/v4/src/utils/selectors.ts b/packages/v4/src/utils/selectors.ts index d4145d11..7d6c51b7 100644 --- a/packages/v4/src/utils/selectors.ts +++ b/packages/v4/src/utils/selectors.ts @@ -14,6 +14,7 @@ import { responsiveAttributeNames } from '../responsive-options.js'; * not re-add an `$isMounted` filter here or at a call site to "fix" the * over-matching: it would also hide every instance a reversible `in-view` or * `media:` strategy has legitimately stood down. + * @link https://js-toolkit-v4.studiometa.dev/utils/dom.html#selectorfor */ export function selectorFor(name: string): string { return [COMPONENT_ATTRIBUTE, ...responsiveAttributeNames(COMPONENT_ATTRIBUTE)] diff --git a/packages/v4/src/utils/smoothTo.ts b/packages/v4/src/utils/smoothTo.ts index 7f3e0839..df0052bb 100644 --- a/packages/v4/src/utils/smoothTo.ts +++ b/packages/v4/src/utils/smoothTo.ts @@ -70,7 +70,10 @@ export interface SmoothToRecord { destroy(): void; } -/** Smooth a value toward a target with one shared frame subscription. */ +/** + * Smooth a value toward a target with one shared frame subscription. + * @link https://js-toolkit-v4.studiometa.dev/utils/motion.html#smoothto + */ export function smoothTo(start?: number, options?: SmoothToOptions): SmoothTo; /** * Smooth several named values toward their targets, on one frame subscription, diff --git a/packages/v4/src/utils/strings.ts b/packages/v4/src/utils/strings.ts index d1304589..a03e4d4a 100644 --- a/packages/v4/src/utils/strings.ts +++ b/packages/v4/src/utils/strings.ts @@ -39,12 +39,18 @@ function delimitedCase(string: string, delimiter: string): string { return split(string).map(lowerCase).join(delimiter); } -/** Convert a string to lowercase. */ +/** + * Convert a string to lowercase. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#lowercase + */ export function lowerCase(string: string): string { return string.toLowerCase(); } -/** Convert a string to UPPERCASE. */ +/** + * Convert a string to UPPERCASE. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#uppercase + */ export function upperCase(string: string): string { return string.toUpperCase(); } @@ -55,34 +61,50 @@ export function upperCase(string: string): string { * * This is not {@link pascalCase} — it never splits words, so it round-trips a * name the framework read from source. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#capitalize */ export function capitalize(string: string): string { return upperCase(string.charAt(0)) + string.slice(1); } -/** Convert a string to `PascalCase`. */ +/** + * Convert a string to `PascalCase`. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#pascalcase + */ export const pascalCase = /* @__PURE__ */ memo(function pascalCase(string: string): string { return split(string) .map((word) => upperCase(word.charAt(0)) + lowerCase(word.slice(1))) .join(''); }); -/** Convert a string to `camelCase`. */ +/** + * Convert a string to `camelCase`. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#camelcase + */ export const camelCase = /* @__PURE__ */ memo(function camelCase(string: string): string { const result = pascalCase(string); return lowerCase(result.charAt(0)) + result.slice(1); }); -/** Convert a string to `kebab-case`, the shape of a `data-` attribute. */ +/** + * Convert a string to `kebab-case`, the shape of a `data-` attribute. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#kebabcase + */ export const kebabCase = /* @__PURE__ */ memo(function kebabCase(string: string): string { return delimitedCase(string, '-'); }); -/** Convert a string to `snake_case`. */ +/** + * Convert a string to `snake_case`. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#snakecase + */ export const snakeCase = /* @__PURE__ */ memo(function snakeCase(string: string): string { return delimitedCase(string, '_'); }); -/** Add the given characters to the start of a string, once. */ +/** + * Add the given characters to the start of a string, once. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withleadingcharacters + */ export function withLeadingCharacters(string: string, characters: string): string { return `${characters}${withoutLeadingCharacters(string, characters)}`; } @@ -93,6 +115,7 @@ export function withLeadingCharacters(string: string, characters: string): strin * No characters is nothing to remove: every helper here returns the string * unchanged rather than treating the empty match every string starts and ends * with as a hit. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withoutleadingcharacters */ export function withoutLeadingCharacters(string: string, characters: string): string { if (characters.length === 0) { @@ -101,7 +124,10 @@ export function withoutLeadingCharacters(string: string, characters: string): st return string.startsWith(characters) ? string.slice(characters.length) : string; } -/** Remove the given characters from the start of a string, as often as they repeat. */ +/** + * Remove the given characters from the start of a string, as often as they repeat. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withoutleadingcharactersrecursive + */ export function withoutLeadingCharactersRecursive(string: string, characters: string): string { if (characters.length === 0) { return string; @@ -113,12 +139,18 @@ export function withoutLeadingCharactersRecursive(string: string, characters: st return result; } -/** Add the given characters to the end of a string, once. */ +/** + * Add the given characters to the end of a string, once. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withtrailingcharacters + */ export function withTrailingCharacters(string: string, characters: string): string { return `${withoutTrailingCharacters(string, characters)}${characters}`; } -/** Remove the given characters from the end of a string, once. */ +/** + * Remove the given characters from the end of a string, once. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withouttrailingcharacters + */ export function withoutTrailingCharacters(string: string, characters: string): string { if (characters.length === 0) { return string; @@ -126,7 +158,10 @@ export function withoutTrailingCharacters(string: string, characters: string): s return string.endsWith(characters) ? string.slice(0, -characters.length) : string; } -/** Remove the given characters from the end of a string, as often as they repeat. */ +/** + * Remove the given characters from the end of a string, as often as they repeat. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withouttrailingcharactersrecursive + */ export function withoutTrailingCharactersRecursive(string: string, characters: string): string { if (characters.length === 0) { return string; @@ -138,22 +173,34 @@ export function withoutTrailingCharactersRecursive(string: string, characters: s return result; } -/** Add a leading slash to a string, once. */ +/** + * Add a leading slash to a string, once. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withleadingslash + */ export function withLeadingSlash(string: string): string { return withLeadingCharacters(string, '/'); } -/** Remove the leading slash from a string, once. */ +/** + * Remove the leading slash from a string, once. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withoutleadingslash + */ export function withoutLeadingSlash(string: string): string { return withoutLeadingCharacters(string, '/'); } -/** Add a trailing slash to a string, once. */ +/** + * Add a trailing slash to a string, once. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withtrailingslash + */ export function withTrailingSlash(string: string): string { return withTrailingCharacters(string, '/'); } -/** Remove the trailing slash from a string, once. */ +/** + * Remove the trailing slash from a string, once. + * @link https://js-toolkit-v4.studiometa.dev/utils/strings.html#withouttrailingslash + */ export function withoutTrailingSlash(string: string): string { return withoutTrailingCharacters(string, '/'); } diff --git a/packages/v4/src/utils/timing.ts b/packages/v4/src/utils/timing.ts index c6e83b3f..2b112b65 100644 --- a/packages/v4/src/utils/timing.ts +++ b/packages/v4/src/utils/timing.ts @@ -3,6 +3,7 @@ /** * Delay a function until it stops being called for the given milliseconds. * Only the last call's arguments are used. + * @link https://js-toolkit-v4.studiometa.dev/utils/timing.html#debounce */ export function debounce( fn: (...args: Args) => void, @@ -19,6 +20,7 @@ export function debounce( /** * Run a function at most once per given milliseconds, on the leading edge. * A call inside the window is dropped, not deferred. + * @link https://js-toolkit-v4.studiometa.dev/utils/timing.html#throttle */ export function throttle( fn: (...args: Args) => void, @@ -38,7 +40,10 @@ export function throttle( }; } -/** Resolve after the given milliseconds. */ +/** + * Resolve after the given milliseconds. + * @link https://js-toolkit-v4.studiometa.dev/utils/timing.html#wait + */ export function wait(delay = 0): Promise { return new Promise((resolve) => { setTimeout(resolve, delay); diff --git a/packages/v4/src/utils/transform.ts b/packages/v4/src/utils/transform.ts index b1bc5ace..cac86236 100644 --- a/packages/v4/src/utils/transform.ts +++ b/packages/v4/src/utils/transform.ts @@ -20,7 +20,10 @@ export interface TransformProps { skewY?: number; } -/** The properties {@link transform} formats, for callers splitting a style patch. */ +/** + * The properties {@link transform} formats, for callers splitting a style patch. + * @link https://js-toolkit-v4.studiometa.dev/utils/css.html#transform-props + */ export const TRANSFORM_PROPS = /* @__PURE__ */ Object.freeze([ 'x', 'y', @@ -49,6 +52,7 @@ export const TRANSFORM_PROPS = /* @__PURE__ */ Object.freeze([ * transform({ x: 100, scale: 0.5 }); * // translate3d(100px, 0px, 0px) scale(0.5) * ``` + * @link https://js-toolkit-v4.studiometa.dev/utils/css.html#transform */ export function transform(props: TransformProps): string { const parts: string[] = []; @@ -106,6 +110,7 @@ export interface MatrixProps { * matrix({ scaleX: 0.5, scaleY: 0.5 }); * // matrix(0.5, 0, 0, 0.5, 0, 0) * ``` + * @link https://js-toolkit-v4.studiometa.dev/utils/css.html#matrix */ export function matrix(props: MatrixProps = {}): string { const { scaleX = 1, skewY = 0, skewX = 0, scaleY = 1, translateX = 0, translateY = 0 } = props; diff --git a/packages/v4/src/utils/transition.ts b/packages/v4/src/utils/transition.ts index 85625db1..a749ef89 100644 --- a/packages/v4/src/utils/transition.ts +++ b/packages/v4/src/utils/transition.ts @@ -26,6 +26,7 @@ function toClassList(value: string | string[]): string[] { return (Array.isArray(value) ? value : value.split(' ')).filter(Boolean); } +/** @link https://js-toolkit-v4.studiometa.dev/utils/css.html#setclassesorstyles */ export function setClassesOrStyles( el: HTMLElement, value: ClassesOrStyles | undefined, @@ -57,6 +58,7 @@ function hasTransition(el: HTMLElement): boolean { * awaiting it is never left waiting for an end which will not come. * * @param mode Whether the `to` state is kept or removed at the end. + * @link https://js-toolkit-v4.studiometa.dev/utils/transitions.html#transition */ export async function transition( el: HTMLElement, @@ -127,7 +129,10 @@ export async function transition( end(mode); } -/** Shared transition option definitions. */ +/** + * Shared transition option definitions. + * @link https://js-toolkit-v4.studiometa.dev/utils/transitions.html#transition-options + */ export const TRANSITION_OPTIONS: Record = { enterFrom: String, enterActive: String, @@ -168,6 +173,7 @@ function removeClasses(el: HTMLElement, classes: string): void { * The opposite direction's `to` classes are cleared first: with `enterKeep` or * `leaveKeep` they are still on the element from the last transition, and a * `to` state left behind fights the one being applied. + * @link https://js-toolkit-v4.studiometa.dev/utils/transitions.html#entertransition */ export async function enterTransition(el: HTMLElement, options: TransitionOptions): Promise { const { enterFrom, enterActive, enterTo, enterKeep, leaveTo } = options; @@ -180,7 +186,10 @@ export async function enterTransition(el: HTMLElement, options: TransitionOption ); } -/** Run the leave transition on an element. */ +/** + * Run the leave transition on an element. + * @link https://js-toolkit-v4.studiometa.dev/utils/transitions.html#leavetransition + */ export async function leaveTransition(el: HTMLElement, options: TransitionOptions): Promise { const { leaveFrom, leaveActive, leaveTo, leaveKeep, enterTo } = options; removeClasses(el, enterTo); diff --git a/packages/v4/src/viewTransition.ts b/packages/v4/src/viewTransition.ts index 77dee7ff..bbf83ceb 100644 --- a/packages/v4/src/viewTransition.ts +++ b/packages/v4/src/viewTransition.ts @@ -22,6 +22,7 @@ let vtTail = Promise.resolve(); * animation when the API is unavailable. * * @returns Resolved once the transition has finished. + * @link https://js-toolkit-v4.studiometa.dev/api/scheduler/viewTransition.html */ export function viewTransition(update: ViewTransitionUpdate): Promise { return new Promise((resolve, reject) => {