@studiometa/js-toolkit 4.0.0-alpha.0 → 4.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/utils/index.d.ts +2 -2
- package/package.json +5 -2
- package/skills/studiometa-js-toolkit/SKILL.md +87 -0
- package/skills/studiometa-js-toolkit/references/api/configuration.md +169 -0
- package/skills/studiometa-js-toolkit/references/api/context/createContext.md +46 -0
- package/skills/studiometa-js-toolkit/references/api/context/createGroup.md +67 -0
- package/skills/studiometa-js-toolkit/references/api/context/index.md +69 -0
- package/skills/studiometa-js-toolkit/references/api/context/injectContext.md +58 -0
- package/skills/studiometa-js-toolkit/references/api/context/injectContextSync.md +44 -0
- package/skills/studiometa-js-toolkit/references/api/context/provideContext.md +61 -0
- package/skills/studiometa-js-toolkit/references/api/context/provideRootContext.md +42 -0
- package/skills/studiometa-js-toolkit/references/api/context/signal.md +85 -0
- package/skills/studiometa-js-toolkit/references/api/context/subscribeContext.md +71 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/children.md +92 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/component.md +75 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/index.md +41 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/inject.md +73 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/on.md +149 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/provide.md +77 -0
- package/skills/studiometa-js-toolkit/references/api/decorators/read-write.md +93 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/DIAGNOSTICS.md +137 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/EVENTS.md +45 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/index.md +58 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/reportDiagnostic.md +67 -0
- package/skills/studiometa-js-toolkit/references/api/diagnostics/warn.md +54 -0
- package/skills/studiometa-js-toolkit/references/api/dom/breakpoints.md +81 -0
- package/skills/studiometa-js-toolkit/references/api/dom/domUpdate.md +70 -0
- package/skills/studiometa-js-toolkit/references/api/dom/emitExtendable.md +68 -0
- package/skills/studiometa-js-toolkit/references/api/dom/index.md +26 -0
- package/skills/studiometa-js-toolkit/references/api/dom/swap.md +105 -0
- package/skills/studiometa-js-toolkit/references/api/dom/watchAttributeNamespace.md +96 -0
- package/skills/studiometa-js-toolkit/references/api/dom/watchAttributes.md +56 -0
- package/skills/studiometa-js-toolkit/references/api/dom/whenDOMSettled.md +41 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-component.md +105 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-mount.md +69 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-option.md +106 -0
- package/skills/studiometa-js-toolkit/references/api/html/data-ref.md +63 -0
- package/skills/studiometa-js-toolkit/references/api/html/index.md +32 -0
- package/skills/studiometa-js-toolkit/references/api/index.md +73 -0
- package/skills/studiometa-js-toolkit/references/api/instance-events.md +75 -0
- package/skills/studiometa-js-toolkit/references/api/instance-methods.md +267 -0
- package/skills/studiometa-js-toolkit/references/api/instance-properties.md +90 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-events.md +149 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-lifecycle.md +90 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-options.md +96 -0
- package/skills/studiometa-js-toolkit/references/api/methods-hooks-services.md +164 -0
- package/skills/studiometa-js-toolkit/references/api/registry/defineManifest.md +57 -0
- package/skills/studiometa-js-toolkit/references/api/registry/fromMetaGlob.md +33 -0
- package/skills/studiometa-js-toolkit/references/api/registry/fromWebpackContext.md +37 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getInstance.md +52 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getInstances.md +48 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getMountedInstances.md +39 -0
- package/skills/studiometa-js-toolkit/references/api/registry/getUnmountedInstances.md +46 -0
- package/skills/studiometa-js-toolkit/references/api/registry/index.md +49 -0
- package/skills/studiometa-js-toolkit/references/api/registry/registerComponent.md +93 -0
- package/skills/studiometa-js-toolkit/references/api/registry/registerComponents.md +37 -0
- package/skills/studiometa-js-toolkit/references/api/registry/registerManifest.md +72 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/defaultScheduler.md +104 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/index.md +27 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/nextFrame.md +31 -0
- package/skills/studiometa-js-toolkit/references/api/scheduler/viewTransition.md +53 -0
- package/skills/studiometa-js-toolkit/references/api/services/createService.md +95 -0
- package/skills/studiometa-js-toolkit/references/api/services/createServiceMixin.md +81 -0
- package/skills/studiometa-js-toolkit/references/api/services/index.md +94 -0
- package/skills/studiometa-js-toolkit/references/api/services/mixins.md +148 -0
- package/skills/studiometa-js-toolkit/references/api/services/perTarget.md +65 -0
- package/skills/studiometa-js-toolkit/references/api/services/toggle.md +80 -0
- package/skills/studiometa-js-toolkit/references/api/services/until.md +44 -0
- package/skills/studiometa-js-toolkit/references/api/services/useBreakpoint.md +42 -0
- package/skills/studiometa-js-toolkit/references/api/services/useDrag.md +119 -0
- package/skills/studiometa-js-toolkit/references/api/services/useInView.md +64 -0
- package/skills/studiometa-js-toolkit/references/api/services/useKey.md +105 -0
- package/skills/studiometa-js-toolkit/references/api/services/useMediaQuery.md +45 -0
- package/skills/studiometa-js-toolkit/references/api/services/useMutation.md +69 -0
- package/skills/studiometa-js-toolkit/references/api/services/usePointer.md +91 -0
- package/skills/studiometa-js-toolkit/references/api/services/usePrefersReducedMotion.md +42 -0
- package/skills/studiometa-js-toolkit/references/api/services/useRaf.md +91 -0
- package/skills/studiometa-js-toolkit/references/api/services/useResize.md +60 -0
- package/skills/studiometa-js-toolkit/references/api/services/useScroll.md +88 -0
- package/skills/studiometa-js-toolkit/references/api/services/useScrollProgress.md +88 -0
- package/skills/studiometa-js-toolkit/references/api/services/useWindowScroll.md +25 -0
- package/skills/studiometa-js-toolkit/references/api/services/useWindowSize.md +25 -0
- package/skills/studiometa-js-toolkit/references/api/storage/createStorage.md +126 -0
- package/skills/studiometa-js-toolkit/references/api/storage/index.md +33 -0
- package/skills/studiometa-js-toolkit/references/api/storage/presets.md +58 -0
- package/skills/studiometa-js-toolkit/references/api/storage/providers.md +109 -0
- package/skills/studiometa-js-toolkit/references/api/test/index.md +214 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/attribute-grammar.md +93 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/mutation-engine.md +71 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/philosophy.md +78 -0
- package/skills/studiometa-js-toolkit/references/guide/concepts/the-registry.md +110 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/autoloading.md +104 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/handling-diagnostics.md +104 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/mount-strategies.md +95 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/scheduling-work.md +124 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/sharing-state.md +218 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/swapping-content.md +92 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/testing-components.md +137 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/typing-components.md +190 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/using-decorators.md +168 -0
- package/skills/studiometa-js-toolkit/references/guide/going-further/using-storage.md +125 -0
- package/skills/studiometa-js-toolkit/references/guide/index.md +163 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/installation.md +144 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/lifecycle-hooks.md +174 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/managing-components.md +229 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/managing-options.md +235 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/managing-refs.md +174 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/using-services.md +218 -0
- package/skills/studiometa-js-toolkit/references/guide/introduction/working-with-events.md +251 -0
- package/skills/studiometa-js-toolkit/references/guide/migration/v3-to-v4.md +373 -0
- package/skills/studiometa-js-toolkit/references/utils/css.md +98 -0
- package/skills/studiometa-js-toolkit/references/utils/dom.md +54 -0
- package/skills/studiometa-js-toolkit/references/utils/easings.md +217 -0
- package/skills/studiometa-js-toolkit/references/utils/focus.md +84 -0
- package/skills/studiometa-js-toolkit/references/utils/history.md +69 -0
- package/skills/studiometa-js-toolkit/references/utils/index.md +53 -0
- package/skills/studiometa-js-toolkit/references/utils/is.md +84 -0
- package/skills/studiometa-js-toolkit/references/utils/load.md +59 -0
- package/skills/studiometa-js-toolkit/references/utils/math.md +129 -0
- package/skills/studiometa-js-toolkit/references/utils/motion.md +218 -0
- package/skills/studiometa-js-toolkit/references/utils/objects.md +68 -0
- package/skills/studiometa-js-toolkit/references/utils/scroll.md +128 -0
- package/skills/studiometa-js-toolkit/references/utils/strings.md +176 -0
- package/skills/studiometa-js-toolkit/references/utils/timing.md +111 -0
- package/skills/studiometa-js-toolkit/references/utils/transitions.md +122 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Motion
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
import { damp, smoothTo, spring } from '@studiometa/js-toolkit/utils';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Everything here takes a **time**, not a frame count.
|
|
8
|
+
|
|
9
|
+
> **Decay is expressed in time, not in frames.** `INERTIA_FRAME` (16.67 ms) is the reference of every factor.
|
|
10
|
+
|
|
11
|
+
That is what makes a factor mean the same thing at 60 Hz and at 120 Hz, and it is why `damp()` takes the elapsed time as a **required** argument.
|
|
12
|
+
|
|
13
|
+
## Damping
|
|
14
|
+
|
|
15
|
+
### damp
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
damp(targetValue: number, currentValue: number, factor: number, elapsed: number, precision?: number): number
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { useRaf } from '@studiometa/js-toolkit';
|
|
23
|
+
import { damp } from '@studiometa/js-toolkit/utils';
|
|
24
|
+
let current = 0;
|
|
25
|
+
let target = 100;
|
|
26
|
+
|
|
27
|
+
useRaf().subscribe(({ delta }) => {
|
|
28
|
+
current = damp(target, current, 0.1, delta);
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`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.
|
|
33
|
+
|
|
34
|
+
**v3's `damp()` had no `elapsed`**
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
damp(current, target, 0.1); // [!code --]
|
|
38
|
+
damp(target, current, 0.1, delta); // [!code ++]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A factor without a time is a factor that means something different on every display.
|
|
42
|
+
|
|
43
|
+
### clampDampFactor
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
clampDampFactor(factor: number): number
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Keeps a factor in the usable range.
|
|
50
|
+
|
|
51
|
+
### decayOver
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
decayOver(retained: number, elapsed: number): number
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The decay of an elapsed time.
|
|
58
|
+
|
|
59
|
+
## Springs
|
|
60
|
+
|
|
61
|
+
### spring
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
spring(
|
|
65
|
+
targetValue: number,
|
|
66
|
+
currentValue: number,
|
|
67
|
+
currentVelocity: number,
|
|
68
|
+
elapsed: number,
|
|
69
|
+
options?: { stiffness?: number; damping?: number; mass?: number; precision?: number },
|
|
70
|
+
): [value: number, velocity: number]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import { useRaf } from '@studiometa/js-toolkit';
|
|
75
|
+
import { spring } from '@studiometa/js-toolkit/utils';
|
|
76
|
+
let value = 0;
|
|
77
|
+
let velocity = 0;
|
|
78
|
+
|
|
79
|
+
useRaf().subscribe(({ delta }) => {
|
|
80
|
+
[value, velocity] = spring(100, value, velocity, delta, { stiffness: 0.1, damping: 0.8 });
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It returns the pair, because a spring's state **is** the value and its velocity — hiding the velocity would make the next step wrong.
|
|
85
|
+
|
|
86
|
+
**It integrates on a fixed step of a quarter frame**, however long the real frame is, so `stiffness`, `damping` and `mass` keep their meaning and the duration is real. `stiffness / mass` is clamped to `MAX_SPRING_RATIO`, from which that step is derived.
|
|
87
|
+
|
|
88
|
+
`precision` defaults to `1e-4`.
|
|
89
|
+
|
|
90
|
+
## Smoothing
|
|
91
|
+
|
|
92
|
+
### smoothTo
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
smoothTo(start?: number, options?: SmoothToOptions): SmoothTo
|
|
96
|
+
smoothTo<K extends string>(start: Record<K, number>, options?: SmoothToOptions): SmoothToRecord<K>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**A `toggle()` over `useRaf()`**: one subscription however many times the target is set, started when the value has somewhere to go, released when it arrives.
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { smoothTo } from '@studiometa/js-toolkit/utils';
|
|
103
|
+
|
|
104
|
+
const x = smoothTo(0, { damping: 0.85 });
|
|
105
|
+
|
|
106
|
+
x(400); // set a target, read the smoothed value
|
|
107
|
+
x(); // read it
|
|
108
|
+
x.raw(); // the target, unsmoothed
|
|
109
|
+
x.add(50); // move the target
|
|
110
|
+
x.jump(0); // set value and target at once — no travel, no frame
|
|
111
|
+
x.isMoving; // still travelling?
|
|
112
|
+
|
|
113
|
+
const unsubscribe = x.subscribe((value) => {
|
|
114
|
+
document.body.style.setProperty('--x', `${value}px`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
x.destroy(); // release the frame subscription and every subscriber
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Several channels on one subscription
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
import { smoothTo } from '@studiometa/js-toolkit/utils';
|
|
124
|
+
|
|
125
|
+
const view = smoothTo({ x: 0, y: 0, scale: 1 });
|
|
126
|
+
|
|
127
|
+
view({ x: 100, y: 50 }); // only these two are re-aimed; `scale` keeps travelling
|
|
128
|
+
view.jump({ scale: 1 }); // reset one channel with no travel
|
|
129
|
+
view.isMoving; // true while *any* channel moves
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**The keys are the consumer's own** — a scale, an opacity or a progress as readily as a coordinate. One frame subscription, one settled state (the loop stops when the **last** channel arrives), and one subscriber call per frame carrying the whole record.
|
|
133
|
+
|
|
134
|
+
**The record handed out is the same object every frame**
|
|
135
|
+
|
|
136
|
+
As a service hands the same props. Treat it as read-only, and copy it with `{ ...values }` to keep one.
|
|
137
|
+
|
|
138
|
+
The mode — `spring`, `stiffness`, `mass` — belongs to the **instance**, not the channel.
|
|
139
|
+
|
|
140
|
+
#### `damping` accepts a function
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
smoothTo({ x: 0, y: 0 }, { damping: (key) => (key === 'x' ? 0.9 : 0.7) });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It is read on **every frame and for every channel**, which matters for three reasons:
|
|
147
|
+
|
|
148
|
+
1. **A component's factor is an option**, and `$options` is a live view over attributes — so a factor captured once would freeze an attribute the framework keeps live.
|
|
149
|
+
2. It expresses a factor that depends on the **direction of travel**: read the current value and decide.
|
|
150
|
+
3. It gives one channel of a record a different rate from its neighbour.
|
|
151
|
+
|
|
152
|
+
A number stays a number.
|
|
153
|
+
|
|
154
|
+
#### `precision`
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
158
|
+
## Inertia
|
|
159
|
+
|
|
160
|
+
The family a coast is built from, and what [`useDrag()`](../api/services/useDrag.md) uses.
|
|
161
|
+
|
|
162
|
+
**`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.
|
|
163
|
+
|
|
164
|
+
### inertiaDecay
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
inertiaDecay(dampFactor: number, elapsed: number): number
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`decayOver` with the tighter clamp a coast needs.
|
|
171
|
+
|
|
172
|
+
### inertiaTimeConstant
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
inertiaTimeConstant(dampFactor: number): number
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`τ = INERTIA_FRAME / ln(1 / damp)`.
|
|
179
|
+
|
|
180
|
+
### inertiaStep
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
inertiaStep(velocity: number, dampFactor: number, elapsed: number): number
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The distance travelled across the step.
|
|
187
|
+
|
|
188
|
+
### inertiaFinalValue
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
inertiaFinalValue(value: number, velocity: number, dampFactor: number): number
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Where it will come to rest: `value + velocity · τ`.
|
|
195
|
+
|
|
196
|
+
## Constants
|
|
197
|
+
|
|
198
|
+
### INERTIA_FRAME
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const INERTIA_FRAME: number;
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`16.67` — the reference frame, in milliseconds.
|
|
205
|
+
|
|
206
|
+
### DEFAULT_DAMP_FACTOR
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const DEFAULT_DAMP_FACTOR = 0.85;
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### MAX_SPRING_RATIO
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const MAX_SPRING_RATIO: number;
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The clamp on `stiffness / mass`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Objects & random
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
import { deepmerge, random, randomInt, randomItem } from '@studiometa/js-toolkit/utils';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## Merging
|
|
8
|
+
|
|
9
|
+
### deepmerge
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
deepmerge(...layers: Record<string, unknown>[]): Record<string, unknown>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { deepmerge } from '@studiometa/js-toolkit/utils';
|
|
17
|
+
|
|
18
|
+
deepmerge({ tween: { ease: 'linear', duration: 300 } }, { tween: { ease: 'ease-out' } });
|
|
19
|
+
// { tween: { ease: 'ease-out', duration: 300 } }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Later layers win, at every depth. Plain objects merge; anything else — an array, a `Date`, an element, a class instance — **replaces**.
|
|
23
|
+
|
|
24
|
+
That last rule is the one worth knowing: an array is a value, not a structure to merge, because merging two arrays by index is almost never what a caller meant.
|
|
25
|
+
|
|
26
|
+
**It ships for the consumer, not for core**
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
## Random
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { random, randomInt, randomItem } from '@studiometa/js-toolkit/utils';
|
|
34
|
+
|
|
35
|
+
random(10); // 0 → 10
|
|
36
|
+
random(5, 10); // 5 → 10
|
|
37
|
+
randomInt(10); // an integer, 0 → 10
|
|
38
|
+
randomItem(['a', 'b', 'c']);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**One argument is a maximum; two are a range.**
|
|
42
|
+
|
|
43
|
+
### random
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
random(a: number, b?: number): number
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A float in the range.
|
|
50
|
+
|
|
51
|
+
### randomInt
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
randomInt(a: number, b?: number): number
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
An integer in the range, bounds inclusive.
|
|
58
|
+
|
|
59
|
+
### randomItem
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
randomItem<T>(items: readonly T[]): T | undefined
|
|
63
|
+
randomItem(items: string): string | undefined
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
One item from an array, or one character from a string.
|
|
67
|
+
|
|
68
|
+
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.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Scroll
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
import { lockScroll, scrollPosition, scrollTo } from '@studiometa/js-toolkit/utils';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## Scrolling to a target
|
|
8
|
+
|
|
9
|
+
**The two halves are separate because callers need them separately.** `scrollPosition()` measures and returns; `scrollTo()` calls it and moves.
|
|
10
|
+
|
|
11
|
+
A carousel asks which slide is nearest three times for every time it travels — and in v3, asking meant scrolling.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { scrollPosition, scrollTo } from '@studiometa/js-toolkit/utils';
|
|
15
|
+
|
|
16
|
+
const el = document.body;
|
|
17
|
+
|
|
18
|
+
// Where would we end up?
|
|
19
|
+
const { top, left } = scrollPosition(el, { align: 'center' });
|
|
20
|
+
|
|
21
|
+
// Go there.
|
|
22
|
+
scrollTo(el, { align: 'center', offset: -80 });
|
|
23
|
+
scrollTo('#section', { axis: 'y' });
|
|
24
|
+
scrollTo(0);
|
|
25
|
+
scrollTo({ top: 200 });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### scrollPosition
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
scrollPosition(target: ScrollToTarget, options?: ScrollPositionOptions): ScrollPosition
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Measures and returns `{ top, left }` without moving anything.
|
|
35
|
+
|
|
36
|
+
### scrollTo
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
scrollTo(target: ScrollToTarget, options?: ScrollToOptions): ScrollPosition
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Calls `scrollPosition()` and moves there, returning the same result.
|
|
43
|
+
|
|
44
|
+
`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.
|
|
45
|
+
|
|
46
|
+
### The target
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
type ScrollToTarget = string | Element | number | Partial<{ left: number; top: number }>;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A selector, an element, a number, or a position. **A number or a position names its own destination**, so `align` does not apply to it.
|
|
53
|
+
|
|
54
|
+
### The options
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface ScrollPositionOptions {
|
|
58
|
+
rootElement?: Element | Window; // what scrolls. Defaults to the window.
|
|
59
|
+
axis?: 'x' | 'y' | 'both'; // which axes a target that names none may move. Defaults to 'y'.
|
|
60
|
+
offset?: number; // pixels to stop short. Defaults to 0.
|
|
61
|
+
align?: ScrollAlign | { x?: ScrollAlign; y?: ScrollAlign };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ScrollToOptions extends ScrollPositionOptions {
|
|
65
|
+
behavior?: ScrollBehavior;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**`align` is `'start' | 'center' | 'end'`, or one per axis**, and it applies to an **element** target only.
|
|
70
|
+
|
|
71
|
+
**The names are physical, like `axis`**
|
|
72
|
+
|
|
73
|
+
`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.
|
|
74
|
+
|
|
75
|
+
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`.
|
|
76
|
+
|
|
77
|
+
### SCROLL_AXES
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const SCROLL_AXES: Readonly<{ x: 'x'; y: 'y'; both: 'both' }>;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### SCROLL_ALIGNMENTS
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const SCROLL_ALIGNMENTS: Readonly<{ start: 'start'; center: 'center'; end: 'end' }>;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### A dependency was measured and refused
|
|
90
|
+
|
|
91
|
+
`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.
|
|
92
|
+
|
|
93
|
+
## Locking the page
|
|
94
|
+
|
|
95
|
+
### lockScroll
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
lockScroll(target?: HTMLElement): () => void
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { lockScroll } from '@studiometa/js-toolkit/utils';
|
|
103
|
+
|
|
104
|
+
const release = lockScroll();
|
|
105
|
+
// … the modal is open
|
|
106
|
+
release();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**It counts.** A modal surface is not alone on a page: a dialog opened from inside a drawer is two holders, and the one that closes first must not put the scroll back under the one still open.
|
|
110
|
+
|
|
111
|
+
- The **first** lock saves the inline value it found.
|
|
112
|
+
- The **last** release puts exactly that value back.
|
|
113
|
+
- The ones between only move the count.
|
|
114
|
+
|
|
115
|
+
**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.
|
|
116
|
+
|
|
117
|
+
**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.
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
**A native `<dialog>` needs it too**
|
|
122
|
+
|
|
123
|
+
`showModal()` gives the top layer, the backdrop, a focus trap and `Escape`. It does **not** stop the page behind it scrolling.
|
|
124
|
+
|
|
125
|
+
## See also
|
|
126
|
+
|
|
127
|
+
- [`useScroll()`](../api/services/useScroll.md) — scroll position as a service
|
|
128
|
+
- [`useScrollProgress()`](../api/services/useScrollProgress.md) — an element's progress through the viewport
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Strings
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
import { camelCase, kebabCase, pascalCase } from '@studiometa/js-toolkit/utils';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## Case
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { camelCase, kebabCase, pascalCase } from '@studiometa/js-toolkit/utils';
|
|
11
|
+
|
|
12
|
+
pascalCase('drag-threshold'); // 'DragThreshold'
|
|
13
|
+
camelCase('drag-threshold'); // 'dragThreshold'
|
|
14
|
+
kebabCase('dragThreshold'); // 'drag-threshold'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
These are the functions the framework itself uses: `kebabCase` turns `dragThreshold` into `data-option-drag-threshold`, and `pascalCase` turns an option name into `optionDragThresholdChanged`.
|
|
18
|
+
|
|
19
|
+
**Four of them are memoised**
|
|
20
|
+
|
|
21
|
+
`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.
|
|
22
|
+
|
|
23
|
+
### lowerCase
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
lowerCase(string: string): string
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### upperCase
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
upperCase(string: string): string
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### capitalize
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
capitalize(string: string): string
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### pascalCase
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
pascalCase(string: string): string
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`'foo bar'` → `'FooBar'`. Memoised.
|
|
48
|
+
|
|
49
|
+
### camelCase
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
camelCase(string: string): string
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`'foo bar'` → `'fooBar'`. Memoised.
|
|
56
|
+
|
|
57
|
+
### kebabCase
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
kebabCase(string: string): string
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`'fooBar'` → `'foo-bar'`. Memoised.
|
|
64
|
+
|
|
65
|
+
### snakeCase
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
snakeCase(string: string): string
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`'fooBar'` → `'foo_bar'`. Memoised.
|
|
72
|
+
|
|
73
|
+
## Leading and trailing characters
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import {
|
|
77
|
+
withLeadingCharacters,
|
|
78
|
+
withoutTrailingCharactersRecursive,
|
|
79
|
+
} from '@studiometa/js-toolkit/utils';
|
|
80
|
+
|
|
81
|
+
withLeadingCharacters('bar', 'foo'); // 'foobar'
|
|
82
|
+
withLeadingCharacters('foobar', 'foo'); // 'foobar' — already there
|
|
83
|
+
withoutTrailingCharactersRecursive('path///', '/'); // 'path'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The **recursive** pair exists because a URL joined from parts collects separators, and removing one is not the same as removing them all.
|
|
87
|
+
|
|
88
|
+
### withLeadingCharacters
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
withLeadingCharacters(string: string, characters: string): string
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Adds them if absent.
|
|
95
|
+
|
|
96
|
+
### withoutLeadingCharacters
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
withoutLeadingCharacters(string: string, characters: string): string
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Removes them once.
|
|
103
|
+
|
|
104
|
+
### withoutLeadingCharactersRecursive
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
withoutLeadingCharactersRecursive(string: string, characters: string): string
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Removes them as long as they are there.
|
|
111
|
+
|
|
112
|
+
### withTrailingCharacters
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
withTrailingCharacters(string: string, characters: string): string
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Adds them if absent.
|
|
119
|
+
|
|
120
|
+
### withoutTrailingCharacters
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
withoutTrailingCharacters(string: string, characters: string): string
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Removes them once.
|
|
127
|
+
|
|
128
|
+
### withoutTrailingCharactersRecursive
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
withoutTrailingCharactersRecursive(string: string, characters: string): string
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Removes them as long as they are there.
|
|
135
|
+
|
|
136
|
+
## Slashes
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import {
|
|
142
|
+
withLeadingSlash,
|
|
143
|
+
withoutLeadingSlash,
|
|
144
|
+
withoutTrailingSlash,
|
|
145
|
+
withTrailingSlash,
|
|
146
|
+
} from '@studiometa/js-toolkit/utils';
|
|
147
|
+
|
|
148
|
+
withLeadingSlash('about'); // '/about'
|
|
149
|
+
withoutLeadingSlash('/about'); // 'about'
|
|
150
|
+
withTrailingSlash('/about'); // '/about/'
|
|
151
|
+
withoutTrailingSlash('/about/'); // '/about'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### withLeadingSlash
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
withLeadingSlash(string: string): string
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### withoutLeadingSlash
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
withoutLeadingSlash(string: string): string
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### withTrailingSlash
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
withTrailingSlash(string: string): string
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### withoutTrailingSlash
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
withoutTrailingSlash(string: string): string
|
|
176
|
+
```
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Timing
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
import { debounce, memo, throttle, wait } from '@studiometa/js-toolkit/utils';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## Rate limiting
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { debounce, throttle } from '@studiometa/js-toolkit/utils';
|
|
11
|
+
|
|
12
|
+
const search = debounce((term) => console.log(term), 300); // after the last call
|
|
13
|
+
const track = throttle((y) => console.log(y), 100); // at most once per delay
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Not for scroll, resize, pointer or frame work**
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
Reach for these for what the framework does not own — a `fetch` per keystroke, an analytics call, a `localStorage` write.
|
|
21
|
+
|
|
22
|
+
### debounce
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
debounce<Args>(fn: (...args: Args) => void, delay?: number): (...args: Args) => void
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Runs once, after the calls stop.
|
|
29
|
+
|
|
30
|
+
### throttle
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
throttle<Args>(fn: (...args: Args) => void, delay?: number): (...args: Args) => void
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Runs at most once per `delay`.
|
|
37
|
+
|
|
38
|
+
## Waiting
|
|
39
|
+
|
|
40
|
+
### wait
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
wait(delay?: number): Promise<void>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { wait } from '@studiometa/js-toolkit/utils';
|
|
48
|
+
|
|
49
|
+
async function pause() {
|
|
50
|
+
await wait(300);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`await wait()` with no argument is one turn of the event loop.
|
|
55
|
+
|
|
56
|
+
For a frame rather than a timer, use [`nextFrame()`](../api/scheduler/nextFrame.md). For "the framework has caught up", use [`whenDOMSettled()`](../api/dom/whenDOMSettled.md) — a timer is the wrong tool for both, and the reason the [test helpers](../api/test/index.md) exist at all.
|
|
57
|
+
|
|
58
|
+
## Memoisation
|
|
59
|
+
|
|
60
|
+
### memo
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
memo<Args extends [] | [key: unknown], Value>(fn: (...args: Args) => Value): Memo<Args, Value>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import { memo } from '@studiometa/js-toolkit/utils';
|
|
68
|
+
|
|
69
|
+
const expensive = memo((key) => key.toString().repeat(2));
|
|
70
|
+
|
|
71
|
+
expensive('a'); // computed
|
|
72
|
+
expensive('a'); // cached
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Zero or one argument, and that argument is the key.** That is the whole surface, and it is deliberate: a memo keyed on several arguments needs a key strategy, and a key strategy is a decision the caller should make visibly rather than inherit.
|
|
76
|
+
|
|
77
|
+
It is what the four memoised [string converters](strings.md) are built on, and what memoises the active breakpoint name for the length of one task.
|
|
78
|
+
|
|
79
|
+
`cache` and `memoize` from v3 are not shipped — this covers the one case core needed.
|
|
80
|
+
|
|
81
|
+
## Placeholders
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
import { noop, noopValue } from '@studiometa/js-toolkit/utils';
|
|
87
|
+
|
|
88
|
+
const onDone = noop; // a callback that does nothing
|
|
89
|
+
const identity = noopValue; // a transform that changes nothing
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### noop
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
noop(): void
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### noopValue
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
noopValue<T>(value: T): T
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## What is not here
|
|
105
|
+
|
|
106
|
+
| Not shipped | Write |
|
|
107
|
+
| ------------------------------ | ---------------------------------------------------------- |
|
|
108
|
+
| `nextTick` | `await Promise.resolve()` |
|
|
109
|
+
| `nextMicrotask` | `queueMicrotask(fn)` |
|
|
110
|
+
| `Queue`, `SmartQueue` | the [scheduler](../api/scheduler/index.md)'s lanes |
|
|
111
|
+
| `domScheduler`, `useScheduler` | [`defaultScheduler`](../api/scheduler/defaultScheduler.md) |
|