@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.
Files changed (126) hide show
  1. package/README.md +10 -0
  2. package/dist/utils/index.d.ts +2 -2
  3. package/package.json +5 -2
  4. package/skills/studiometa-js-toolkit/SKILL.md +87 -0
  5. package/skills/studiometa-js-toolkit/references/api/configuration.md +169 -0
  6. package/skills/studiometa-js-toolkit/references/api/context/createContext.md +46 -0
  7. package/skills/studiometa-js-toolkit/references/api/context/createGroup.md +67 -0
  8. package/skills/studiometa-js-toolkit/references/api/context/index.md +69 -0
  9. package/skills/studiometa-js-toolkit/references/api/context/injectContext.md +58 -0
  10. package/skills/studiometa-js-toolkit/references/api/context/injectContextSync.md +44 -0
  11. package/skills/studiometa-js-toolkit/references/api/context/provideContext.md +61 -0
  12. package/skills/studiometa-js-toolkit/references/api/context/provideRootContext.md +42 -0
  13. package/skills/studiometa-js-toolkit/references/api/context/signal.md +85 -0
  14. package/skills/studiometa-js-toolkit/references/api/context/subscribeContext.md +71 -0
  15. package/skills/studiometa-js-toolkit/references/api/decorators/children.md +92 -0
  16. package/skills/studiometa-js-toolkit/references/api/decorators/component.md +75 -0
  17. package/skills/studiometa-js-toolkit/references/api/decorators/index.md +41 -0
  18. package/skills/studiometa-js-toolkit/references/api/decorators/inject.md +73 -0
  19. package/skills/studiometa-js-toolkit/references/api/decorators/on.md +149 -0
  20. package/skills/studiometa-js-toolkit/references/api/decorators/provide.md +77 -0
  21. package/skills/studiometa-js-toolkit/references/api/decorators/read-write.md +93 -0
  22. package/skills/studiometa-js-toolkit/references/api/diagnostics/DIAGNOSTICS.md +137 -0
  23. package/skills/studiometa-js-toolkit/references/api/diagnostics/EVENTS.md +45 -0
  24. package/skills/studiometa-js-toolkit/references/api/diagnostics/index.md +58 -0
  25. package/skills/studiometa-js-toolkit/references/api/diagnostics/reportDiagnostic.md +67 -0
  26. package/skills/studiometa-js-toolkit/references/api/diagnostics/warn.md +54 -0
  27. package/skills/studiometa-js-toolkit/references/api/dom/breakpoints.md +81 -0
  28. package/skills/studiometa-js-toolkit/references/api/dom/domUpdate.md +70 -0
  29. package/skills/studiometa-js-toolkit/references/api/dom/emitExtendable.md +68 -0
  30. package/skills/studiometa-js-toolkit/references/api/dom/index.md +26 -0
  31. package/skills/studiometa-js-toolkit/references/api/dom/swap.md +105 -0
  32. package/skills/studiometa-js-toolkit/references/api/dom/watchAttributeNamespace.md +96 -0
  33. package/skills/studiometa-js-toolkit/references/api/dom/watchAttributes.md +56 -0
  34. package/skills/studiometa-js-toolkit/references/api/dom/whenDOMSettled.md +41 -0
  35. package/skills/studiometa-js-toolkit/references/api/html/data-component.md +105 -0
  36. package/skills/studiometa-js-toolkit/references/api/html/data-mount.md +69 -0
  37. package/skills/studiometa-js-toolkit/references/api/html/data-option.md +106 -0
  38. package/skills/studiometa-js-toolkit/references/api/html/data-ref.md +63 -0
  39. package/skills/studiometa-js-toolkit/references/api/html/index.md +32 -0
  40. package/skills/studiometa-js-toolkit/references/api/index.md +73 -0
  41. package/skills/studiometa-js-toolkit/references/api/instance-events.md +75 -0
  42. package/skills/studiometa-js-toolkit/references/api/instance-methods.md +267 -0
  43. package/skills/studiometa-js-toolkit/references/api/instance-properties.md +90 -0
  44. package/skills/studiometa-js-toolkit/references/api/methods-hooks-events.md +149 -0
  45. package/skills/studiometa-js-toolkit/references/api/methods-hooks-lifecycle.md +90 -0
  46. package/skills/studiometa-js-toolkit/references/api/methods-hooks-options.md +96 -0
  47. package/skills/studiometa-js-toolkit/references/api/methods-hooks-services.md +164 -0
  48. package/skills/studiometa-js-toolkit/references/api/registry/defineManifest.md +57 -0
  49. package/skills/studiometa-js-toolkit/references/api/registry/fromMetaGlob.md +33 -0
  50. package/skills/studiometa-js-toolkit/references/api/registry/fromWebpackContext.md +37 -0
  51. package/skills/studiometa-js-toolkit/references/api/registry/getInstance.md +52 -0
  52. package/skills/studiometa-js-toolkit/references/api/registry/getInstances.md +48 -0
  53. package/skills/studiometa-js-toolkit/references/api/registry/getMountedInstances.md +39 -0
  54. package/skills/studiometa-js-toolkit/references/api/registry/getUnmountedInstances.md +46 -0
  55. package/skills/studiometa-js-toolkit/references/api/registry/index.md +49 -0
  56. package/skills/studiometa-js-toolkit/references/api/registry/registerComponent.md +93 -0
  57. package/skills/studiometa-js-toolkit/references/api/registry/registerComponents.md +37 -0
  58. package/skills/studiometa-js-toolkit/references/api/registry/registerManifest.md +72 -0
  59. package/skills/studiometa-js-toolkit/references/api/scheduler/defaultScheduler.md +104 -0
  60. package/skills/studiometa-js-toolkit/references/api/scheduler/index.md +27 -0
  61. package/skills/studiometa-js-toolkit/references/api/scheduler/nextFrame.md +31 -0
  62. package/skills/studiometa-js-toolkit/references/api/scheduler/viewTransition.md +53 -0
  63. package/skills/studiometa-js-toolkit/references/api/services/createService.md +95 -0
  64. package/skills/studiometa-js-toolkit/references/api/services/createServiceMixin.md +81 -0
  65. package/skills/studiometa-js-toolkit/references/api/services/index.md +94 -0
  66. package/skills/studiometa-js-toolkit/references/api/services/mixins.md +148 -0
  67. package/skills/studiometa-js-toolkit/references/api/services/perTarget.md +65 -0
  68. package/skills/studiometa-js-toolkit/references/api/services/toggle.md +80 -0
  69. package/skills/studiometa-js-toolkit/references/api/services/until.md +44 -0
  70. package/skills/studiometa-js-toolkit/references/api/services/useBreakpoint.md +42 -0
  71. package/skills/studiometa-js-toolkit/references/api/services/useDrag.md +119 -0
  72. package/skills/studiometa-js-toolkit/references/api/services/useInView.md +64 -0
  73. package/skills/studiometa-js-toolkit/references/api/services/useKey.md +105 -0
  74. package/skills/studiometa-js-toolkit/references/api/services/useMediaQuery.md +45 -0
  75. package/skills/studiometa-js-toolkit/references/api/services/useMutation.md +69 -0
  76. package/skills/studiometa-js-toolkit/references/api/services/usePointer.md +91 -0
  77. package/skills/studiometa-js-toolkit/references/api/services/usePrefersReducedMotion.md +42 -0
  78. package/skills/studiometa-js-toolkit/references/api/services/useRaf.md +91 -0
  79. package/skills/studiometa-js-toolkit/references/api/services/useResize.md +60 -0
  80. package/skills/studiometa-js-toolkit/references/api/services/useScroll.md +88 -0
  81. package/skills/studiometa-js-toolkit/references/api/services/useScrollProgress.md +88 -0
  82. package/skills/studiometa-js-toolkit/references/api/services/useWindowScroll.md +25 -0
  83. package/skills/studiometa-js-toolkit/references/api/services/useWindowSize.md +25 -0
  84. package/skills/studiometa-js-toolkit/references/api/storage/createStorage.md +126 -0
  85. package/skills/studiometa-js-toolkit/references/api/storage/index.md +33 -0
  86. package/skills/studiometa-js-toolkit/references/api/storage/presets.md +58 -0
  87. package/skills/studiometa-js-toolkit/references/api/storage/providers.md +109 -0
  88. package/skills/studiometa-js-toolkit/references/api/test/index.md +214 -0
  89. package/skills/studiometa-js-toolkit/references/guide/concepts/attribute-grammar.md +93 -0
  90. package/skills/studiometa-js-toolkit/references/guide/concepts/mutation-engine.md +71 -0
  91. package/skills/studiometa-js-toolkit/references/guide/concepts/philosophy.md +78 -0
  92. package/skills/studiometa-js-toolkit/references/guide/concepts/the-registry.md +110 -0
  93. package/skills/studiometa-js-toolkit/references/guide/going-further/autoloading.md +104 -0
  94. package/skills/studiometa-js-toolkit/references/guide/going-further/handling-diagnostics.md +104 -0
  95. package/skills/studiometa-js-toolkit/references/guide/going-further/mount-strategies.md +95 -0
  96. package/skills/studiometa-js-toolkit/references/guide/going-further/scheduling-work.md +124 -0
  97. package/skills/studiometa-js-toolkit/references/guide/going-further/sharing-state.md +218 -0
  98. package/skills/studiometa-js-toolkit/references/guide/going-further/swapping-content.md +92 -0
  99. package/skills/studiometa-js-toolkit/references/guide/going-further/testing-components.md +137 -0
  100. package/skills/studiometa-js-toolkit/references/guide/going-further/typing-components.md +190 -0
  101. package/skills/studiometa-js-toolkit/references/guide/going-further/using-decorators.md +168 -0
  102. package/skills/studiometa-js-toolkit/references/guide/going-further/using-storage.md +125 -0
  103. package/skills/studiometa-js-toolkit/references/guide/index.md +163 -0
  104. package/skills/studiometa-js-toolkit/references/guide/introduction/installation.md +144 -0
  105. package/skills/studiometa-js-toolkit/references/guide/introduction/lifecycle-hooks.md +174 -0
  106. package/skills/studiometa-js-toolkit/references/guide/introduction/managing-components.md +229 -0
  107. package/skills/studiometa-js-toolkit/references/guide/introduction/managing-options.md +235 -0
  108. package/skills/studiometa-js-toolkit/references/guide/introduction/managing-refs.md +174 -0
  109. package/skills/studiometa-js-toolkit/references/guide/introduction/using-services.md +218 -0
  110. package/skills/studiometa-js-toolkit/references/guide/introduction/working-with-events.md +251 -0
  111. package/skills/studiometa-js-toolkit/references/guide/migration/v3-to-v4.md +373 -0
  112. package/skills/studiometa-js-toolkit/references/utils/css.md +98 -0
  113. package/skills/studiometa-js-toolkit/references/utils/dom.md +54 -0
  114. package/skills/studiometa-js-toolkit/references/utils/easings.md +217 -0
  115. package/skills/studiometa-js-toolkit/references/utils/focus.md +84 -0
  116. package/skills/studiometa-js-toolkit/references/utils/history.md +69 -0
  117. package/skills/studiometa-js-toolkit/references/utils/index.md +53 -0
  118. package/skills/studiometa-js-toolkit/references/utils/is.md +84 -0
  119. package/skills/studiometa-js-toolkit/references/utils/load.md +59 -0
  120. package/skills/studiometa-js-toolkit/references/utils/math.md +129 -0
  121. package/skills/studiometa-js-toolkit/references/utils/motion.md +218 -0
  122. package/skills/studiometa-js-toolkit/references/utils/objects.md +68 -0
  123. package/skills/studiometa-js-toolkit/references/utils/scroll.md +128 -0
  124. package/skills/studiometa-js-toolkit/references/utils/strings.md +176 -0
  125. package/skills/studiometa-js-toolkit/references/utils/timing.md +111 -0
  126. 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) |