@studiometa/js-toolkit 4.0.0-alpha.1 → 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 (125) hide show
  1. package/README.md +10 -0
  2. package/package.json +5 -2
  3. package/skills/studiometa-js-toolkit/SKILL.md +87 -0
  4. package/skills/studiometa-js-toolkit/references/api/configuration.md +169 -0
  5. package/skills/studiometa-js-toolkit/references/api/context/createContext.md +46 -0
  6. package/skills/studiometa-js-toolkit/references/api/context/createGroup.md +67 -0
  7. package/skills/studiometa-js-toolkit/references/api/context/index.md +69 -0
  8. package/skills/studiometa-js-toolkit/references/api/context/injectContext.md +58 -0
  9. package/skills/studiometa-js-toolkit/references/api/context/injectContextSync.md +44 -0
  10. package/skills/studiometa-js-toolkit/references/api/context/provideContext.md +61 -0
  11. package/skills/studiometa-js-toolkit/references/api/context/provideRootContext.md +42 -0
  12. package/skills/studiometa-js-toolkit/references/api/context/signal.md +85 -0
  13. package/skills/studiometa-js-toolkit/references/api/context/subscribeContext.md +71 -0
  14. package/skills/studiometa-js-toolkit/references/api/decorators/children.md +92 -0
  15. package/skills/studiometa-js-toolkit/references/api/decorators/component.md +75 -0
  16. package/skills/studiometa-js-toolkit/references/api/decorators/index.md +41 -0
  17. package/skills/studiometa-js-toolkit/references/api/decorators/inject.md +73 -0
  18. package/skills/studiometa-js-toolkit/references/api/decorators/on.md +149 -0
  19. package/skills/studiometa-js-toolkit/references/api/decorators/provide.md +77 -0
  20. package/skills/studiometa-js-toolkit/references/api/decorators/read-write.md +93 -0
  21. package/skills/studiometa-js-toolkit/references/api/diagnostics/DIAGNOSTICS.md +137 -0
  22. package/skills/studiometa-js-toolkit/references/api/diagnostics/EVENTS.md +45 -0
  23. package/skills/studiometa-js-toolkit/references/api/diagnostics/index.md +58 -0
  24. package/skills/studiometa-js-toolkit/references/api/diagnostics/reportDiagnostic.md +67 -0
  25. package/skills/studiometa-js-toolkit/references/api/diagnostics/warn.md +54 -0
  26. package/skills/studiometa-js-toolkit/references/api/dom/breakpoints.md +81 -0
  27. package/skills/studiometa-js-toolkit/references/api/dom/domUpdate.md +70 -0
  28. package/skills/studiometa-js-toolkit/references/api/dom/emitExtendable.md +68 -0
  29. package/skills/studiometa-js-toolkit/references/api/dom/index.md +26 -0
  30. package/skills/studiometa-js-toolkit/references/api/dom/swap.md +105 -0
  31. package/skills/studiometa-js-toolkit/references/api/dom/watchAttributeNamespace.md +96 -0
  32. package/skills/studiometa-js-toolkit/references/api/dom/watchAttributes.md +56 -0
  33. package/skills/studiometa-js-toolkit/references/api/dom/whenDOMSettled.md +41 -0
  34. package/skills/studiometa-js-toolkit/references/api/html/data-component.md +105 -0
  35. package/skills/studiometa-js-toolkit/references/api/html/data-mount.md +69 -0
  36. package/skills/studiometa-js-toolkit/references/api/html/data-option.md +106 -0
  37. package/skills/studiometa-js-toolkit/references/api/html/data-ref.md +63 -0
  38. package/skills/studiometa-js-toolkit/references/api/html/index.md +32 -0
  39. package/skills/studiometa-js-toolkit/references/api/index.md +73 -0
  40. package/skills/studiometa-js-toolkit/references/api/instance-events.md +75 -0
  41. package/skills/studiometa-js-toolkit/references/api/instance-methods.md +267 -0
  42. package/skills/studiometa-js-toolkit/references/api/instance-properties.md +90 -0
  43. package/skills/studiometa-js-toolkit/references/api/methods-hooks-events.md +149 -0
  44. package/skills/studiometa-js-toolkit/references/api/methods-hooks-lifecycle.md +90 -0
  45. package/skills/studiometa-js-toolkit/references/api/methods-hooks-options.md +96 -0
  46. package/skills/studiometa-js-toolkit/references/api/methods-hooks-services.md +164 -0
  47. package/skills/studiometa-js-toolkit/references/api/registry/defineManifest.md +57 -0
  48. package/skills/studiometa-js-toolkit/references/api/registry/fromMetaGlob.md +33 -0
  49. package/skills/studiometa-js-toolkit/references/api/registry/fromWebpackContext.md +37 -0
  50. package/skills/studiometa-js-toolkit/references/api/registry/getInstance.md +52 -0
  51. package/skills/studiometa-js-toolkit/references/api/registry/getInstances.md +48 -0
  52. package/skills/studiometa-js-toolkit/references/api/registry/getMountedInstances.md +39 -0
  53. package/skills/studiometa-js-toolkit/references/api/registry/getUnmountedInstances.md +46 -0
  54. package/skills/studiometa-js-toolkit/references/api/registry/index.md +49 -0
  55. package/skills/studiometa-js-toolkit/references/api/registry/registerComponent.md +93 -0
  56. package/skills/studiometa-js-toolkit/references/api/registry/registerComponents.md +37 -0
  57. package/skills/studiometa-js-toolkit/references/api/registry/registerManifest.md +72 -0
  58. package/skills/studiometa-js-toolkit/references/api/scheduler/defaultScheduler.md +104 -0
  59. package/skills/studiometa-js-toolkit/references/api/scheduler/index.md +27 -0
  60. package/skills/studiometa-js-toolkit/references/api/scheduler/nextFrame.md +31 -0
  61. package/skills/studiometa-js-toolkit/references/api/scheduler/viewTransition.md +53 -0
  62. package/skills/studiometa-js-toolkit/references/api/services/createService.md +95 -0
  63. package/skills/studiometa-js-toolkit/references/api/services/createServiceMixin.md +81 -0
  64. package/skills/studiometa-js-toolkit/references/api/services/index.md +94 -0
  65. package/skills/studiometa-js-toolkit/references/api/services/mixins.md +148 -0
  66. package/skills/studiometa-js-toolkit/references/api/services/perTarget.md +65 -0
  67. package/skills/studiometa-js-toolkit/references/api/services/toggle.md +80 -0
  68. package/skills/studiometa-js-toolkit/references/api/services/until.md +44 -0
  69. package/skills/studiometa-js-toolkit/references/api/services/useBreakpoint.md +42 -0
  70. package/skills/studiometa-js-toolkit/references/api/services/useDrag.md +119 -0
  71. package/skills/studiometa-js-toolkit/references/api/services/useInView.md +64 -0
  72. package/skills/studiometa-js-toolkit/references/api/services/useKey.md +105 -0
  73. package/skills/studiometa-js-toolkit/references/api/services/useMediaQuery.md +45 -0
  74. package/skills/studiometa-js-toolkit/references/api/services/useMutation.md +69 -0
  75. package/skills/studiometa-js-toolkit/references/api/services/usePointer.md +91 -0
  76. package/skills/studiometa-js-toolkit/references/api/services/usePrefersReducedMotion.md +42 -0
  77. package/skills/studiometa-js-toolkit/references/api/services/useRaf.md +91 -0
  78. package/skills/studiometa-js-toolkit/references/api/services/useResize.md +60 -0
  79. package/skills/studiometa-js-toolkit/references/api/services/useScroll.md +88 -0
  80. package/skills/studiometa-js-toolkit/references/api/services/useScrollProgress.md +88 -0
  81. package/skills/studiometa-js-toolkit/references/api/services/useWindowScroll.md +25 -0
  82. package/skills/studiometa-js-toolkit/references/api/services/useWindowSize.md +25 -0
  83. package/skills/studiometa-js-toolkit/references/api/storage/createStorage.md +126 -0
  84. package/skills/studiometa-js-toolkit/references/api/storage/index.md +33 -0
  85. package/skills/studiometa-js-toolkit/references/api/storage/presets.md +58 -0
  86. package/skills/studiometa-js-toolkit/references/api/storage/providers.md +109 -0
  87. package/skills/studiometa-js-toolkit/references/api/test/index.md +214 -0
  88. package/skills/studiometa-js-toolkit/references/guide/concepts/attribute-grammar.md +93 -0
  89. package/skills/studiometa-js-toolkit/references/guide/concepts/mutation-engine.md +71 -0
  90. package/skills/studiometa-js-toolkit/references/guide/concepts/philosophy.md +78 -0
  91. package/skills/studiometa-js-toolkit/references/guide/concepts/the-registry.md +110 -0
  92. package/skills/studiometa-js-toolkit/references/guide/going-further/autoloading.md +104 -0
  93. package/skills/studiometa-js-toolkit/references/guide/going-further/handling-diagnostics.md +104 -0
  94. package/skills/studiometa-js-toolkit/references/guide/going-further/mount-strategies.md +95 -0
  95. package/skills/studiometa-js-toolkit/references/guide/going-further/scheduling-work.md +124 -0
  96. package/skills/studiometa-js-toolkit/references/guide/going-further/sharing-state.md +218 -0
  97. package/skills/studiometa-js-toolkit/references/guide/going-further/swapping-content.md +92 -0
  98. package/skills/studiometa-js-toolkit/references/guide/going-further/testing-components.md +137 -0
  99. package/skills/studiometa-js-toolkit/references/guide/going-further/typing-components.md +190 -0
  100. package/skills/studiometa-js-toolkit/references/guide/going-further/using-decorators.md +168 -0
  101. package/skills/studiometa-js-toolkit/references/guide/going-further/using-storage.md +125 -0
  102. package/skills/studiometa-js-toolkit/references/guide/index.md +163 -0
  103. package/skills/studiometa-js-toolkit/references/guide/introduction/installation.md +144 -0
  104. package/skills/studiometa-js-toolkit/references/guide/introduction/lifecycle-hooks.md +174 -0
  105. package/skills/studiometa-js-toolkit/references/guide/introduction/managing-components.md +229 -0
  106. package/skills/studiometa-js-toolkit/references/guide/introduction/managing-options.md +235 -0
  107. package/skills/studiometa-js-toolkit/references/guide/introduction/managing-refs.md +174 -0
  108. package/skills/studiometa-js-toolkit/references/guide/introduction/using-services.md +218 -0
  109. package/skills/studiometa-js-toolkit/references/guide/introduction/working-with-events.md +251 -0
  110. package/skills/studiometa-js-toolkit/references/guide/migration/v3-to-v4.md +373 -0
  111. package/skills/studiometa-js-toolkit/references/utils/css.md +98 -0
  112. package/skills/studiometa-js-toolkit/references/utils/dom.md +54 -0
  113. package/skills/studiometa-js-toolkit/references/utils/easings.md +217 -0
  114. package/skills/studiometa-js-toolkit/references/utils/focus.md +84 -0
  115. package/skills/studiometa-js-toolkit/references/utils/history.md +69 -0
  116. package/skills/studiometa-js-toolkit/references/utils/index.md +53 -0
  117. package/skills/studiometa-js-toolkit/references/utils/is.md +84 -0
  118. package/skills/studiometa-js-toolkit/references/utils/load.md +59 -0
  119. package/skills/studiometa-js-toolkit/references/utils/math.md +129 -0
  120. package/skills/studiometa-js-toolkit/references/utils/motion.md +218 -0
  121. package/skills/studiometa-js-toolkit/references/utils/objects.md +68 -0
  122. package/skills/studiometa-js-toolkit/references/utils/scroll.md +128 -0
  123. package/skills/studiometa-js-toolkit/references/utils/strings.md +176 -0
  124. package/skills/studiometa-js-toolkit/references/utils/timing.md +111 -0
  125. package/skills/studiometa-js-toolkit/references/utils/transitions.md +122 -0
@@ -0,0 +1,42 @@
1
+ # useBreakpoint
2
+
3
+ ```ts
4
+ useBreakpoint(): Service<{ readonly name: string }>
5
+ ```
6
+
7
+ The active breakpoint name, as a service. It is its **own source** — it is not derived from a resize.
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ import { useBreakpoint } from '@studiometa/js-toolkit';
13
+
14
+ const unsubscribe = useBreakpoint().subscribe(({ name }) => {
15
+ document.documentElement.dataset.breakpoint = name;
16
+ });
17
+ ```
18
+
19
+ ## Why it is not part of the resize service
20
+
21
+ It is backed by `matchMedia` `change` listeners, so it emits **on crossings** and it reports a change of the reader's font size — which a `ResizeObserver` on a box cannot see.
22
+
23
+ The values are in `rem`, and in a media query `rem` resolves against the **initial** font size, not against the root element.
24
+
25
+ `ResizeProps` therefore dropped `breakpoints` and `activeBreakpoints`: they were a different measurement wearing the same shape.
26
+
27
+ ## The named set
28
+
29
+ See [Breakpoints](../dom/breakpoints.md) for `BREAKPOINTS`, `getBreakpoints()` and `setBreakpoints()`.
30
+
31
+ `setBreakpoints()` replaces the set and emits at once. The `MediaQueryList` objects are built once, and the active name is memoised for the length of one task — `setBreakpoints()` and the `change` handler of a running service clear that memo immediately.
32
+
33
+ ## Who else uses it
34
+
35
+ - Every responsive **option** resolves through the same active-name read, but a `matchMedia` subscription only opens for a component that declares [`option<Name>Changed()`](../methods-hooks-options.md).
36
+ - Connected elements with a scoped `data-component` declaration share **one** reference-counted subscription to this service. A page with plain declarations opens none.
37
+
38
+ The breakpoint service is part of the core graph on every page. What it costs is a subscription, and a page that asks for nothing opens none.
39
+
40
+ ## `{ immediate: true }` works here
41
+
42
+ There is always a current breakpoint.
@@ -0,0 +1,119 @@
1
+ # useDrag
2
+
3
+ ```ts
4
+ useDrag(target: HTMLElement | SVGElement, options?: DragOptions): Service<DragProps>
5
+ ```
6
+
7
+ A drag gesture, with an optional inertia phase.
8
+
9
+ ## Props
10
+
11
+ ```ts
12
+ interface DragProps {
13
+ readonly mode: DragMode;
14
+ readonly x: number;
15
+ readonly y: number;
16
+ readonly deltaX: number;
17
+ readonly deltaY: number;
18
+ readonly originX: number;
19
+ readonly originY: number;
20
+ readonly distanceX: number;
21
+ readonly distanceY: number;
22
+ readonly finalX: number;
23
+ readonly finalY: number;
24
+ }
25
+ ```
26
+
27
+ `isGrabbing`, `hasInertia` and `target` are removed, and `props.MODES` with them.
28
+
29
+ ## The modes
30
+
31
+ ```js
32
+ import { DRAG_MODES } from '@studiometa/js-toolkit';
33
+
34
+ DRAG_MODES.IDLE; // 'idle'
35
+ DRAG_MODES.START; // 'start'
36
+ DRAG_MODES.DRAG; // 'drag'
37
+ DRAG_MODES.DROP; // 'drop'
38
+ DRAG_MODES.INERTIA; // 'inertia'
39
+ DRAG_MODES.STOP; // 'stop'
40
+ ```
41
+
42
+ `idle` is new in v4. `DragMode` is derived from the frozen object — that is the pattern for every closed set of strings in the framework.
43
+
44
+ ## Options
45
+
46
+ ```ts
47
+ interface DragOptions {
48
+ axis?: 'x' | 'y' | 'both';
49
+ dampFactor?: number;
50
+ dragThreshold?: number;
51
+ inertia?: boolean;
52
+ }
53
+ ```
54
+
55
+ The v3 spelling `dragTreshold` is fixed.
56
+
57
+ ## Usage
58
+
59
+ ```js
60
+ import { Base, DRAG_MODES, useDrag } from '@studiometa/js-toolkit';
61
+
62
+ class Slide extends Base {
63
+ static config = { name: 'Slide' };
64
+
65
+ mounted() {
66
+ return useDrag(this.$el, { axis: 'x' }).subscribe(({ mode, x, finalX }) => {
67
+ if (mode === DRAG_MODES.DROP) {
68
+ this.settle(finalX);
69
+ return;
70
+ }
71
+ this.$el.style.transform = `translateX(${x}px)`;
72
+ });
73
+ }
74
+
75
+ settle(x) {}
76
+ }
77
+ ```
78
+
79
+ ## A gesture the browser can steal is not a gesture
80
+
81
+ `useDrag` owns both axes by default and writes `touch-action: none`.
82
+
83
+ | `axis` | writes | and |
84
+ | -------- | ------- | ----------------------------- |
85
+ | `'both'` | `none` | — |
86
+ | `'x'` | `pan-y` | every Y movement prop is zero |
87
+ | `'y'` | `pan-x` | every X movement prop is zero |
88
+
89
+ It writes **only when the computed value is `auto`**, and it restores the previous inline value when the last service of those options leaves. Services on one target share that ownership.
90
+
91
+ The click that ends a drag is suppressed from a flag that movement on the owned axis arms and the next `pointerdown` disarms — and only for a **trusted** click with a non-zero `detail`.
92
+
93
+ ## Inertia
94
+
95
+ With `{ inertia: false }` a drag publishes the **exact projected destination** at `drop`, then goes through `stop` to `idle` with no tick subscription at all.
96
+
97
+ With inertia, the coast subscribes to `scheduler.tick()`. Decay is expressed in time, not in frames:
98
+
99
+ - `INERTIA_FRAME` (16.67 ms) is the reference of every factor;
100
+ - `inertiaStep()` integrates the decay across the step, so any sequence of frames sums to `velocity · τ` exactly;
101
+ - the settle position is `value + velocity · τ` with `τ = INERTIA_FRAME / ln(1 / damp)`.
102
+
103
+ The velocity is sampled as a distance over the interval between events, smoothed, with the interval clamped at both ends. **At the drop, the velocity is decayed by the idle time through the same law** — so a pointer that stopped moving before release does not fling.
104
+
105
+ ## Keying
106
+
107
+ `useDrag()` keys its axis, inertia, damping and threshold, so two callers asking for the same gesture on the same element share one service. See [`perTarget()`](perTarget.md).
108
+
109
+ ## `{ immediate: true }`
110
+
111
+ Does nothing outside a gesture: there is no current value between gestures.
112
+
113
+ ## Mixin
114
+
115
+ ```js
116
+ class Slide extends withDrag(Base, { axis: 'x', inertia: false }) {
117
+ dragged({ mode, x }) {}
118
+ }
119
+ ```
@@ -0,0 +1,64 @@
1
+ # useInView
2
+
3
+ ```ts
4
+ useInView(target: Element, init?: IntersectionObserverInit): Service<InViewProps>
5
+ ```
6
+
7
+ An `IntersectionObserver` as a service.
8
+
9
+ ## Props
10
+
11
+ ```ts
12
+ interface InViewProps {
13
+ readonly isInView: boolean;
14
+ readonly entry: IntersectionObserverEntry | null;
15
+ }
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```js
21
+ import { Base, useInView } from '@studiometa/js-toolkit';
22
+
23
+ class Reveal extends Base {
24
+ static config = { name: 'Reveal' };
25
+
26
+ mounted() {
27
+ return useInView(this.$el, { threshold: 0.25 }).subscribe(({ isInView }) => {
28
+ this.$el.classList.toggle('is-in-view', isInView);
29
+ });
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Keying
35
+
36
+ `useInView()` keys **every `IntersectionObserverInit` field**, and gives an object root a stable weak identity so two callers passing the same root element share one service.
37
+
38
+ The options are read by meaning, not by spelling: keys are sorted at every depth and the ones holding `undefined` are dropped, so `{ threshold: 0.5, rootMargin: '0px' }` and `{ rootMargin: '0px', threshold: 0.5 }` are the same service.
39
+
40
+ **Nothing groups observers across targets.**
41
+
42
+ ## `{ immediate: true }` is the default here
43
+
44
+ `withInView` defaults it to `true`, because "am I in view" has a current answer and a consumer that has to wait for the first crossing to find out would be wrong on load.
45
+
46
+ ## This is not a mount strategy
47
+
48
+ `useInView` and `withInView` observe a component that is **already mounted**. They do not replace `data-mount="visible"` or `data-mount="in-view"`, which decide whether the instance exists at all.
49
+
50
+ | Want | Use |
51
+ | ---------------------------------------------- | ---------------------------- |
52
+ | the component to exist only once seen | `data-mount="visible"` |
53
+ | the component to come and go with the viewport | `data-mount="in-view"` |
54
+ | a mounted component to react to crossings | `useInView()` / `withInView` |
55
+
56
+ See [Mount strategies](../../guide/going-further/mount-strategies.md).
57
+
58
+ ## Mixin
59
+
60
+ ```js
61
+ class Reveal extends withInView(Base, { threshold: 0.25 }) {
62
+ intersected({ isInView }) {}
63
+ }
64
+ ```
@@ -0,0 +1,105 @@
1
+ # useKey
2
+
3
+ ```ts
4
+ useKey(target?: Document | Element | Window): Service<KeyProps>
5
+ ```
6
+
7
+ The keyboard as a service, one instance per target, defaulting to the document.
8
+
9
+ ## Props
10
+
11
+ ```ts
12
+ interface KeyProps {
13
+ readonly event: KeyboardEvent | null;
14
+ readonly triggered: number;
15
+ readonly isDown: boolean;
16
+ readonly isUp: boolean;
17
+ // plus one boolean per named key
18
+ readonly ENTER: boolean;
19
+ readonly SPACE: boolean;
20
+ readonly TAB: boolean;
21
+ readonly ESC: boolean;
22
+ readonly LEFT: boolean;
23
+ readonly UP: boolean;
24
+ readonly RIGHT: boolean;
25
+ readonly DOWN: boolean;
26
+ }
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```js
32
+ import { Base, useKey } from '@studiometa/js-toolkit';
33
+
34
+ class Dialog extends Base {
35
+ static config = { name: 'Dialog' };
36
+
37
+ mounted() {
38
+ return useKey().subscribe(({ ESC, isDown }) => {
39
+ if (ESC && isDown) this.$el.removeAttribute('data-option-open');
40
+ });
41
+ }
42
+ }
43
+ ```
44
+
45
+ A region rather than the document:
46
+
47
+ ```js
48
+ import { Base, useKey } from '@studiometa/js-toolkit';
49
+
50
+ class Menu extends Base {
51
+ static config = { name: 'Menu' };
52
+
53
+ mounted() {
54
+ return useKey(this.$el).subscribe(({ DOWN, isDown }) => {
55
+ if (DOWN && isDown) this.focusNext();
56
+ });
57
+ }
58
+
59
+ focusNext() {}
60
+ }
61
+ ```
62
+
63
+ **An element target is what removes the `hasFocus` bookkeeping** a document-only service forces on a consumer.
64
+
65
+ ## The eight names
66
+
67
+ The eight names of v3 are kept, resolved from `KeyboardEvent.key` rather than from the deprecated `keyCode`:
68
+
69
+ | Name | `event.key` |
70
+ | ------- | ------------ |
71
+ | `ENTER` | `Enter` |
72
+ | `SPACE` | `' '` |
73
+ | `TAB` | `Tab` |
74
+ | `ESC` | `Escape` |
75
+ | `LEFT` | `ArrowLeft` |
76
+ | `UP` | `ArrowUp` |
77
+ | `RIGHT` | `ArrowRight` |
78
+ | `DOWN` | `ArrowDown` |
79
+
80
+ The constant that maps them is **module-internal**: the names reach a consumer as props, so nothing is left to compare against. The flags are a mapped type over that constant, so the props cannot drift from it.
81
+
82
+ **Why they are an exception**
83
+
84
+ Props are flat and nothing derivable is a field — and each of these compares `event.key` against a named value, so nothing about them is unavailable from `event`. They are kept for call-site parity with v3, where `keyed({ ESC })` is how components read the keyboard.
85
+
86
+ ## `triggered` counts repeats of _one_ key
87
+
88
+ A keydown whose key matches the previous event's **while that key is still down** increments it. A different key, or any keyup, sets it back to `1`.
89
+
90
+ v3 incremented on any two consecutive keydowns, so holding `A` and then pressing `B` reported `2`.
91
+
92
+ ## The listeners are neither passive nor capturing
93
+
94
+ - **Not passive**, because `trapFocus()` and every keyboard shortcut call `preventDefault()` on the event the subscriber is handed, and a passive listener cannot.
95
+ - **No capture**, unlike the pointer service, because a descendant that handles its own keys and stops the propagation is **respected** rather than overheard.
96
+
97
+ ## Mixin
98
+
99
+ ```js
100
+ class Dialog extends withKey(Base) {
101
+ keyed({ ESC, isDown }) {}
102
+ }
103
+ ```
104
+
105
+ `withKey` takes the document default too, as `withScroll` and `withResize` do for their own page-wide source. A region is `withKey(Base, { target: (instance) => instance.$refs.wrapper })`.
@@ -0,0 +1,45 @@
1
+ # useMediaQuery
2
+
3
+ ```ts
4
+ useMediaQuery(query: string): Service<{ readonly matches: boolean }>
5
+ ```
6
+
7
+ The `matchMedia` engine, exposed. One instance per query string.
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ import { useMediaQuery } from '@studiometa/js-toolkit';
13
+
14
+ const wide = useMediaQuery('(min-width: 64rem)');
15
+
16
+ // `props()` answers with no subscription, because asking a MediaQueryList is a read.
17
+ if (wide.props().matches) {
18
+ // …
19
+ }
20
+
21
+ const unsubscribe = wide.subscribe(({ matches }) => {
22
+ console.log(matches);
23
+ });
24
+ ```
25
+
26
+ **Parameters**
27
+
28
+ - `query` — any media query string.
29
+
30
+ ## `props()` needs no subscription
31
+
32
+ Asking a `MediaQueryList` whether it matches **is a read**, so this service can answer without starting anything. Emissions are crossings.
33
+
34
+ That makes it the one service where `props()` outside a subscription is not a compromise.
35
+
36
+ ## Where it is used
37
+
38
+ `data-mount="media:<query>"` is this service, which is why that strategy is **reversible**: a crossing out unmounts the instance and a crossing back mounts it again.
39
+
40
+ See [Mount strategies](../../guide/going-further/mount-strategies.md).
41
+
42
+ ## See also
43
+
44
+ - [`usePrefersReducedMotion()`](usePrefersReducedMotion.md) — the named case
45
+ - [`useBreakpoint()`](useBreakpoint.md) — the named breakpoint set
@@ -0,0 +1,69 @@
1
+ # useMutation
2
+
3
+ ```ts
4
+ useMutation(target: Node, init?: MutationObserverInit): Service<MutationProps>
5
+ ```
6
+
7
+ A general `MutationObserver` as a service, for "tell me when anything under this node changes".
8
+
9
+ ## Props
10
+
11
+ ```ts
12
+ interface MutationProps {
13
+ readonly records: readonly MutationRecord[];
14
+ }
15
+ ```
16
+
17
+ The default observation is `{ childList: true, subtree: true }`.
18
+
19
+ ## Usage
20
+
21
+ ```js
22
+ import { Base, useMutation } from '@studiometa/js-toolkit';
23
+
24
+ class Counter extends Base {
25
+ static config = { name: 'Counter', refs: ['list'] };
26
+
27
+ mounted() {
28
+ return useMutation(this.$el, { childList: true }).subscribe(({ records }) => {
29
+ this.$el.dataset.count = String(this.$el.children.length);
30
+ });
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Reach for something narrower first
36
+
37
+ | Want | Use |
38
+ | -------------------------------------------- | -------------------------------------------------------------------- |
39
+ | one attribute of one element | [`watchAttributes()`](../dom/watchAttributes.md) |
40
+ | a whole namespace of attributes | [`watchAttributeNamespace()`](../dom/watchAttributeNamespace.md) |
41
+ | what the framework already reconciles | the registry — it is already watching |
42
+ | a subtree, character data, or a foreign node | **this** |
43
+
44
+ ## It keeps nothing after the delivery
45
+
46
+ `props()` is **empty between deliveries**, so `{ immediate: true }` waits for a real batch rather than inventing one. That is the honest answer for a service whose value _is_ a batch.
47
+
48
+ ## Platform timing, not framework order
49
+
50
+ This service delivers on the **platform's** timing. A subscriber that needs the framework's order awaits `whenDOMSettled()` in its callback:
51
+
52
+ ```js
53
+ useMutation(el).subscribe(async () => {
54
+ await whenDOMSettled();
55
+ // the new components have mounted
56
+ });
57
+ ```
58
+
59
+ ## Keying
60
+
61
+ Its key is a **canonical init**, through `resolveInit()`, which keeps the DOM contract rather than sorting the object blindly. See [`perTarget()`](perTarget.md).
62
+
63
+ ## Mixin
64
+
65
+ ```js
66
+ class Counter extends withMutation(Base, { childList: true }) {
67
+ mutated({ records }) {}
68
+ }
69
+ ```
@@ -0,0 +1,91 @@
1
+ # usePointer
2
+
3
+ ```ts
4
+ usePointer(): Service<PointerProps>
5
+ usePointer(target: Element): Service<ElementPointerProps>
6
+ ```
7
+
8
+ The pointer, in the viewport or in a box.
9
+
10
+ ## Props
11
+
12
+ ```ts
13
+ interface PointerProps {
14
+ readonly event: PointerEvent | null;
15
+ readonly isDown: boolean;
16
+ readonly x: number;
17
+ readonly y: number;
18
+ readonly deltaX: number;
19
+ readonly deltaY: number;
20
+ readonly maxX: number;
21
+ readonly maxY: number;
22
+ readonly progressX: number;
23
+ readonly progressY: number;
24
+ }
25
+
26
+ interface ElementPointerProps extends PointerProps {
27
+ readonly relativeX: number;
28
+ readonly relativeY: number;
29
+ readonly relativeProgressX: number;
30
+ readonly relativeProgressY: number;
31
+ }
32
+ ```
33
+
34
+ `isGrabbing` is removed. `usePointer()` uses **pointer events only** and follows one `pointerId` at a time.
35
+
36
+ ## Usage
37
+
38
+ ```js
39
+ import { Base, usePointer } from '@studiometa/js-toolkit';
40
+
41
+ class Cursor extends Base {
42
+ static config = { name: 'Cursor' };
43
+
44
+ mounted() {
45
+ return usePointer().subscribe(({ x, y }) => {
46
+ this.$el.style.transform = `translate(${x}px, ${y}px)`;
47
+ });
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## The pointer in a box
53
+
54
+ `usePointer(el)` is one lazy service per target, and its props add the four `relative*` fields beside the viewport ones:
55
+
56
+ ```js
57
+ import { Base, usePointer } from '@studiometa/js-toolkit';
58
+
59
+ class Tilt extends Base {
60
+ static config = { name: 'Tilt' };
61
+
62
+ mounted() {
63
+ return usePointer(this.$el).subscribe(({ relativeProgressX, relativeProgressY }) => {
64
+ this.$el.style.setProperty('--tilt-x', String(relativeProgressX * 2 - 1));
65
+ this.$el.style.setProperty('--tilt-y', String(relativeProgressY * 2 - 1));
66
+ });
67
+ }
68
+ }
69
+ ```
70
+
71
+ **The targeted service subscribes to the singleton**, so one set of document listeners serves every target however many boxes are being watched.
72
+
73
+ ## When the box is measured
74
+
75
+ On demand, and kept until a `scroll` (captured at the document), a `resize`, or the target's `ResizeObserver` can have moved it.
76
+
77
+ **The layout box is the frame of reference**, so a transform the consumer applies from its own callback does not invalidate it — which is exactly what a tilt or a parallax needs.
78
+
79
+ ## `{ immediate: true }`
80
+
81
+ Does nothing before the pointer has been seen: there is no current value to deliver, which is what `hasProps()` says.
82
+
83
+ ## Mixin
84
+
85
+ ```js
86
+ class Tilt extends withPointer(Base) {
87
+ moved({ relativeProgressX }) {}
88
+ }
89
+ ```
90
+
91
+ `withPointer` defaults its target to `$el`.
@@ -0,0 +1,42 @@
1
+ # usePrefersReducedMotion
2
+
3
+ ```ts
4
+ usePrefersReducedMotion(): Service<{ readonly matches: boolean }>
5
+ ```
6
+
7
+ `(prefers-reduced-motion: reduce)` as a named service.
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ import { Base, usePrefersReducedMotion } from '@studiometa/js-toolkit';
13
+
14
+ class Carousel extends Base {
15
+ static config = { name: 'Carousel' };
16
+
17
+ mounted() {
18
+ return usePrefersReducedMotion().subscribe(({ matches }) => {
19
+ matches ? this.stopAutoplay() : this.startAutoplay();
20
+ });
21
+ }
22
+
23
+ startAutoplay() {}
24
+ stopAutoplay() {}
25
+ }
26
+ ```
27
+
28
+ ## Why it is a service and not a read
29
+
30
+ **The preference changes while the page is open.** A read at load time is a snapshot that goes stale, and the user who turns reduced motion on mid-visit is exactly the user who most wants it honoured.
31
+
32
+ `props()` answers with no subscription, like every [`useMediaQuery()`](useMediaQuery.md) instance — so a one-off check is still one line. Subscribe when the component should follow the change.
33
+
34
+ ## Mounting on the preference instead
35
+
36
+ To decide whether a component exists at all, use the mount strategy:
37
+
38
+ ```html
39
+ <div data-component="Parallax" data-mount="media:(prefers-reduced-motion: no-preference)"></div>
40
+ ```
41
+
42
+ That strategy is **reversible**, so turning the preference on unmounts the component and turning it off mounts it again. `withMountWhenPrefersMotion` is deleted.
@@ -0,0 +1,91 @@
1
+ # useRaf
2
+
3
+ ```ts
4
+ useRaf(): Service<RafProps, void | RafRender>
5
+ ```
6
+
7
+ The frame clock as a service. It has nothing to scope, so there is one instance.
8
+
9
+ ## Props
10
+
11
+ ```ts
12
+ interface RafProps {
13
+ readonly time: DOMHighResTimeStamp;
14
+ readonly delta: number;
15
+ }
16
+ ```
17
+
18
+ `delta` is clamped to `[1, 40]` ms, and the first tick after the loop wakes reports `1000/60`. `time` stays the raw rAF timestamp.
19
+
20
+ ## Usage
21
+
22
+ ```js
23
+ import { Base, useRaf } from '@studiometa/js-toolkit';
24
+
25
+ class Ticker extends Base {
26
+ static config = { name: 'Ticker' };
27
+
28
+ mounted() {
29
+ return useRaf().subscribe(({ delta }) => {
30
+ // …
31
+ });
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## A callback can return a render function
37
+
38
+ `useRaf` is the one service whose callback return value is used. Return a function and it runs in the frame's `write` phase:
39
+
40
+ ```js
41
+ import { Base, useRaf } from '@studiometa/js-toolkit';
42
+
43
+ class Follower extends Base {
44
+ static config = { name: 'Follower' };
45
+
46
+ mounted() {
47
+ return useRaf().subscribe(({ delta }) => {
48
+ const x = this.$el.offsetLeft + delta;
49
+ return () => {
50
+ this.$el.style.transform = `translateX(${x}px)`;
51
+ };
52
+ });
53
+ }
54
+ }
55
+ ```
56
+
57
+ The service collects the render functions of its callbacks and **cancels a render whose subscriber left between the two phases**.
58
+
59
+ ## It does not own a loop
60
+
61
+ It subscribes to `scheduler.tick()`. The scheduler requests the next frame while a tick subscriber stays, so there is no permanent rAF loop and no loop of the service's own.
62
+
63
+ `{ immediate: true }` does nothing here: the frame tick has **no current value between two frames**, which is what its `hasProps()` says.
64
+
65
+ ## Switching it on and off
66
+
67
+ ```js
68
+ import { Base, toggle, useRaf } from '@studiometa/js-toolkit';
69
+
70
+ class SliderItem extends Base {
71
+ static config = { name: 'SliderItem' };
72
+
73
+ #frame = toggle(() => useRaf().subscribe(({ delta }) => this.follow(delta)));
74
+
75
+ mounted() {
76
+ return this.#frame.stop;
77
+ }
78
+
79
+ follow(delta) {}
80
+ }
81
+ ```
82
+
83
+ See [`toggle()`](toggle.md), and `withRaf(Base, { manual: true })` in [Mixins](mixins.md).
84
+
85
+ ## Mixin
86
+
87
+ ```js
88
+ class Follower extends withRaf(Base) {
89
+ ticked({ delta }) {}
90
+ }
91
+ ```