@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
+ # Services
2
+
3
+ A service is a shared source of props that components subscribe to. It is **lazy and reference-counted**: the source starts on the first subscriber and stops on the last, so with no subscriber there is no listener, no observer and no frame.
4
+
5
+ ## The sources
6
+
7
+ | Service | Hook | Props |
8
+ | ----------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------- |
9
+ | [`useRaf()`](../../api/services/useRaf.md) | `ticked` | `time`, `delta` |
10
+ | [`useScroll(target?)`](../../api/services/useScroll.md) | `scrolled` | `x`, `y`, `deltaX/Y`, `maxX/Y`, `progressX/Y`, `directionX/Y`, `isScrolling` |
11
+ | [`useResize(target?)`](../../api/services/useResize.md) | `resized` | `width`, `height`, `ratio`, `orientation` |
12
+ | [`usePointer(target?)`](../../api/services/usePointer.md) | `moved` | `event`, `isDown`, `x`, `y`, `deltaX/Y`, `maxX/Y`, `progressX/Y` |
13
+ | [`useDrag(target, options?)`](../../api/services/useDrag.md) | `dragged` | `mode`, `x`, `y`, `deltaX/Y`, `originX/Y`, `distanceX/Y`, `finalX/Y` |
14
+ | [`useKey(target?)`](../../api/services/useKey.md) | `keyed` | `event`, `triggered`, `isDown`, `isUp`, plus one boolean per named key |
15
+ | [`useInView(target, init?)`](../../api/services/useInView.md) | `intersected` | `isInView`, `entry` |
16
+ | [`useMutation(target, init?)`](../../api/services/useMutation.md) | `mutated` | `records` |
17
+ | [`useScrollProgress(target, options?)`](../../api/services/useScrollProgress.md) | `scrolledInView` | `startX/Y`, `endX/Y`, `currentX/Y`, `progressX/Y` |
18
+ | [`useBreakpoint()`](../../api/services/useBreakpoint.md) | — | `name` |
19
+ | [`useMediaQuery(query)`](../../api/services/useMediaQuery.md) | — | `matches` |
20
+
21
+ `useWindowScroll()` and `useWindowSize()` name the default cases. `usePrefersReducedMotion()` is the named media query.
22
+
23
+ ## Subscribing by hand
24
+
25
+ Every service has the same two-method surface, so subscribing is one line and the release is what `mounted()` returns:
26
+
27
+ ```js
28
+ import { Base, useScroll } from '@studiometa/js-toolkit';
29
+
30
+ class Header extends Base {
31
+ static config = { name: 'Header' };
32
+
33
+ mounted() {
34
+ return useScroll().subscribe(({ directionY, y }) => {
35
+ this.$el.classList.toggle('is-hidden', directionY > 0 && y > 100);
36
+ });
37
+ }
38
+ }
39
+ ```
40
+
41
+ `subscribe(callback, options?)` returns the unsubscribe function. `props()` reads the current props with no subscription.
42
+
43
+ ### Asking for the first delivery
44
+
45
+ ```js
46
+ useScroll().subscribe(callback, { immediate: true });
47
+ ```
48
+
49
+ The sources that have a current value honour it; the ones that do not, do nothing. The frame tick has no current value between two frames, the pointer has none before it is seen, and a drag has none outside a gesture. Only the new subscriber is called, and the first props of a run carry no movement.
50
+
51
+ ## Mixins — the declarative form
52
+
53
+ A mixin binds one subscription per mount cycle, under the one method name the service owns:
54
+
55
+ ```js
56
+ import { Base, withScroll } from '@studiometa/js-toolkit';
57
+
58
+ class Header extends withScroll(Base) {
59
+ static config = { name: 'Header' };
60
+
61
+ scrolled({ directionY }) {
62
+ this.$el.classList.toggle('is-hidden', directionY > 0);
63
+ }
64
+ }
65
+ ```
66
+
67
+ The mixins are `withRaf`, `withScroll`, `withResize`, `withScrollProgress`, `withPointer`, `withDrag`, `withInView`, `withMutation` and `withKey`.
68
+
69
+ **A mixin never occupies a lifecycle hook**
70
+
71
+ `mounted()` and `unmounted()` belong to the component author. The subscription rides on the framework's own `$mount()`/`$unmount()` pair, so a class that mixes a service in and writes its own `mounted()` **without** `super.mounted()` still subscribes.
72
+
73
+ The subscription therefore starts once the whole of `mounted()` has run — including an `immediate` first delivery, which reaches a component that is fully set up — and is released before `unmounted()`.
74
+
75
+ ### Scoping a mixin to a ref
76
+
77
+ ```js
78
+ import { Base, withResize } from '@studiometa/js-toolkit';
79
+
80
+ class Panel extends withResize(Base, { target: (instance) => instance.$refs.inner }) {
81
+ static config = { name: 'Panel' };
82
+
83
+ resized({ width }) {
84
+ console.log(width);
85
+ }
86
+ }
87
+ ```
88
+
89
+ A resolver that comes back with `undefined` or `null` reports `service.missing-target` and starts **no** subscription — because a renamed ref arrives here as nothing, and a service with a default target would otherwise observe the wrong thing and look like it worked.
90
+
91
+ ### Suspending a hook
92
+
93
+ `{ manual: true }` declares the hook without running it. `$services.<hook>` is the switch:
94
+
95
+ ```js
96
+ import { Base, withRaf } from '@studiometa/js-toolkit';
97
+
98
+ class SliderItem extends withRaf(Base, { manual: true }) {
99
+ static config = { name: 'SliderItem' };
100
+
101
+ ticked({ delta }) {
102
+ // declared, not running
103
+ }
104
+
105
+ start() {
106
+ this.$services.ticked.start();
107
+ }
108
+
109
+ stop() {
110
+ this.$services.ticked.stop();
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### One hook per class, and that is the limit
116
+
117
+ A mixin binds **one** subscription, under one name, per mount cycle. A component whose subscriptions are one per markup declaration — an attribute-driven set, with its own modifiers and its own threshold, known only when the element is read — has no method to name and no fixed count. It calls `subscribe()` itself and returns the release from `mounted()`. That is the intended path, not a workaround.
118
+
119
+ `on<Event>` has the same shape: a handler name belongs to the class, while a set of events can be data.
120
+
121
+ ## Two combinators
122
+
123
+ ### `toggle()` — a subscription you can switch
124
+
125
+ ```js
126
+ import { Base, toggle, useRaf } from '@studiometa/js-toolkit';
127
+
128
+ class SliderItem extends Base {
129
+ static config = { name: 'SliderItem' };
130
+
131
+ #frame = toggle(() => useRaf().subscribe(({ delta }) => this.follow(delta)));
132
+
133
+ mounted() {
134
+ // `stop` is bound, so it is a cleanup as it is.
135
+ return this.#frame.stop;
136
+ }
137
+
138
+ follow(delta) {}
139
+
140
+ onSelected() {
141
+ this.#frame.start();
142
+ }
143
+
144
+ onSettled() {
145
+ this.#frame.stop();
146
+ }
147
+ }
148
+ ```
149
+
150
+ `start()` is idempotent and `stop()` is safe to repeat. It works on a `Signal`, on a bare listener, and outside a component.
151
+
152
+ ### `until()` — a one-shot wait
153
+
154
+ ```js
155
+ import { until, useScroll } from '@studiometa/js-toolkit';
156
+
157
+ async function afterScroll() {
158
+ const props = await until(useScroll(), ({ isScrolling }) => !isScrolling);
159
+ console.log(props.y);
160
+ }
161
+ ```
162
+
163
+ It resolves on the first update that matches, releases the subscription **before** it resolves, and resolves with a copy of the props. It resolves at once when the current props already match.
164
+
165
+ ## The props contract
166
+
167
+ - **Props are flat, one field per axis, and nothing derivable is a field.** `lastX` is `x - deltaX`; `changedX` is `deltaX !== 0`. The grouped objects of v3 (`last`, `delta`, `max`, `progress`, `direction`, `changed`) are gone.
168
+ - **`directionX` and `directionY` are `-1 | 0 | 1`** — one signed value that multiplies.
169
+ - **Every field is `readonly`, and the props object belongs to its service.** It is valid for the duration of the call that received it. Use `{ ...props }` to keep one.
170
+
171
+ ## One instance per target and per options
172
+
173
+ Services are keyed in a `WeakMap` by target **and** by the meaning of their options, so two callers asking for the same thing share one source:
174
+
175
+ ```js
176
+ import { useInView } from '@studiometa/js-toolkit';
177
+
178
+ const el = document.body;
179
+
180
+ // The same service — the key is read by meaning, not by spelling.
181
+ useInView(el, { threshold: 0.5, rootMargin: '0px' });
182
+ useInView(el, { rootMargin: '0px', threshold: 0.5 });
183
+ ```
184
+
185
+ Nothing groups observers across targets.
186
+
187
+ ## No service owns a loop
188
+
189
+ The raf service and an active drag inertia subscribe to `scheduler.tick()`. The scroll service coalesces its events into one `read` per frame. The resize service is a `ResizeObserver`. See [The scheduler](../going-further/scheduling-work.md).
190
+
191
+ ## Writing your own
192
+
193
+ `createService()` is the whole primitive. Give it a `props` reader and a `start` function that returns its teardown:
194
+
195
+ ```ts
196
+ import { createService, perTarget } from '@studiometa/js-toolkit';
197
+
198
+ interface FocusProps {
199
+ readonly hasFocus: boolean;
200
+ }
201
+
202
+ const useFocus = perTarget((target: Element) =>
203
+ createService<FocusProps>({
204
+ props: () => ({ hasFocus: target.contains(document.activeElement) }),
205
+ start(emit) {
206
+ const onChange = () => emit({ hasFocus: target.contains(document.activeElement) });
207
+ document.addEventListener('focusin', onChange);
208
+ document.addEventListener('focusout', onChange);
209
+ return () => {
210
+ document.removeEventListener('focusin', onChange);
211
+ document.removeEventListener('focusout', onChange);
212
+ };
213
+ },
214
+ }),
215
+ );
216
+ ```
217
+
218
+ `perTarget()` gives it the one-instance-per-target caching every built-in service has. See [`createService()`](../../api/services/createService.md) and [`createServiceMixin()`](../../api/services/createServiceMixin.md).
@@ -0,0 +1,251 @@
1
+ # Events
2
+
3
+ Everything a component announces is a real DOM event, and everything it listens to is delegated from its own root element.
4
+
5
+ ## The conventions
6
+
7
+ | Method name | Listens to | Payload |
8
+ | ------------------- | --------------------------------------------- | ---------------------------- |
9
+ | `on<Event>` | the component's own element | the raw event |
10
+ | `on<Ref><Event>` | a declared ref, delegated | `{ event, target, index }` |
11
+ | `on<Child><Event>` | a component in `config.components`, delegated | `{ event, target, payload }` |
12
+ | `onWindow<Event>` | `window` | `{ event, target }` |
13
+ | `onDocument<Event>` | `document` | `{ event, target }` |
14
+
15
+ Every one of them is bound for the **mount cycle** and removed by `$unmount()`.
16
+
17
+ ## The component's own element
18
+
19
+ ```js
20
+ import { Base } from '@studiometa/js-toolkit';
21
+
22
+ class Toggle extends Base {
23
+ static config = { name: 'Toggle' };
24
+
25
+ onClick(event) {
26
+ event.preventDefault();
27
+ this.$el.classList.toggle('is-active');
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## Refs
33
+
34
+ See [Refs](managing-refs.md#event-handlers). The payload's `target` is the ref element the handler matched, not `event.target`, and `index` is its position in a list ref.
35
+
36
+ ## `$emit()` — a native event
37
+
38
+ `$emit(name, payload?)` dispatches a bubbling, cancelable `CustomEvent`. `detail` **is** the payload:
39
+
40
+ ```js
41
+ import { Base } from '@studiometa/js-toolkit';
42
+
43
+ class Dialog extends Base {
44
+ static config = { name: 'Dialog' };
45
+
46
+ open() {
47
+ // No payload: `detail` is the platform value `null`.
48
+ this.$emit('open');
49
+ }
50
+
51
+ goto(index) {
52
+ // One optional object, and `detail` is that object.
53
+ this.$emit('goto', { index });
54
+ }
55
+ }
56
+ ```
57
+
58
+ - It **bubbles**, so any ancestor hears it — a component, or a plain `addEventListener`.
59
+ - It is **cancelable**, and `$emit()` returns the event, so the emitter can read `event.defaultPrevented`.
60
+ - The payload is **one object**, or nothing. A value that is not an object is refused by the type and reported at runtime as `event.invalid-emit-payload`. The event still dispatches.
61
+
62
+ Nothing in the framework is gated on cancellation. It is a channel for component code.
63
+
64
+ ```js
65
+ import { Base } from '@studiometa/js-toolkit';
66
+
67
+ class Dialog extends Base {
68
+ static config = { name: 'Dialog' };
69
+
70
+ close() {
71
+ const event = this.$emit('close');
72
+ if (event.defaultPrevented) return;
73
+ this.$el.removeAttribute('data-option-open');
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### Typing the events a component emits
79
+
80
+ `$emits` in the props type maps each name to its payload object, or to `void` for an event with no payload:
81
+
82
+ ```ts
83
+ import { Base } from '@studiometa/js-toolkit';
84
+
85
+ class Slider extends Base<{
86
+ $emits: {
87
+ goto: { index: number };
88
+ stop: void;
89
+ };
90
+ }> {
91
+ static config = { name: 'Slider' };
92
+
93
+ mounted() {
94
+ this.$emit('goto', { index: 2 });
95
+ this.$emit('stop');
96
+ }
97
+ }
98
+ ```
99
+
100
+ `$emits` replaces the runtime `config.emits` of v3. Nothing of it stays in the bundle.
101
+
102
+ ## Child events {#child-events}
103
+
104
+ A parent hears a child through `on<Child><Event>`, resolved against the names in `config.components`:
105
+
106
+ ```ts
107
+ import { Base, type DelegatedEvent } from '@studiometa/js-toolkit';
108
+
109
+ class AccordionItem extends Base<{ $emits: { open: { height: number } } }> {
110
+ static config = { name: 'AccordionItem' };
111
+
112
+ onClick() {
113
+ this.$emit('open', { height: this.$el.scrollHeight });
114
+ }
115
+ }
116
+
117
+ class Accordion extends Base {
118
+ static config = {
119
+ name: 'Accordion',
120
+ components: { AccordionItem },
121
+ };
122
+
123
+ onAccordionItemOpen({ target, payload }: DelegatedEvent<AccordionItem, 'open'>) {
124
+ console.log(`${target.$id} opened to ${payload.height}px`);
125
+ }
126
+ }
127
+ ```
128
+
129
+ How it works:
130
+
131
+ - **One listener per event type** on the parent's root element.
132
+ - The handler walks from `event.target` up to `this.$el`, reads the instance map of each element, and calls `on<Name><Event>` for the **first mounted instance** that matches.
133
+ - A child inserted later needs no new binding.
134
+ - Events that do not bubble, `mouseenter` and `mouseleave` included, are delegated from the **capture** phase.
135
+
136
+ **`config.components` is what disambiguates the name**
137
+
138
+ A method name alone is ambiguous: `onSliderDragStart` is `SliderDrag` + `start`, or `Slider` + `drag-start`. The name set from `config.components` is what decides. A child that is not declared there is not resolved.
139
+
140
+ A lazy child works the same way — the string key is the name, so nothing is downloaded to resolve a handler:
141
+
142
+ ```js
143
+ static config = {
144
+ name: 'Accordion',
145
+ components: { AccordionItem: () => import('./AccordionItem.js') },
146
+ };
147
+ ```
148
+
149
+ ## Global handlers
150
+
151
+ ```js
152
+ import { Base } from '@studiometa/js-toolkit';
153
+
154
+ class ClickOutside extends Base {
155
+ static config = { name: 'ClickOutside' };
156
+
157
+ onDocumentClick({ event }) {
158
+ if (!event.composedPath().includes(this.$el)) {
159
+ this.$emit('click-outside', { event });
160
+ }
161
+ }
162
+
163
+ onWindowResize() {
164
+ this.$el.removeAttribute('data-option-open');
165
+ }
166
+ }
167
+ ```
168
+
169
+ - **Scope: the mount cycle.** `$unmount()` removes the listener and a new mount binds it again.
170
+ - **Phase: bubble, always.** `onDocumentClick` hears what `document.addEventListener('click', …)` hears. To hear a descendant's non-bubbling event, use `on<Ref><Event>`.
171
+ - **The two prefixes are reserved**, and they match before children and refs. `onWindowResize` binds to `window` even in a component whose `config.components` holds a `Window`. To reach a child with that name, use `@on('Window', 'resize')`.
172
+ - The rule is about method names only. `onClick` and `onDocumentClick` are different names and both can exist; a click on the element fires both.
173
+ - The payload is `{ event, target }`, where `target` is the global the handler names. There is no `payload` and no `index`.
174
+
175
+ ## `$on()` and `$off()`
176
+
177
+ For an event whose name is data rather than a method name, listen by hand and return the cleanup:
178
+
179
+ ```js
180
+ import { Base } from '@studiometa/js-toolkit';
181
+
182
+ class Watcher extends Base {
183
+ static config = { name: 'Watcher', options: { events: Array } };
184
+
185
+ mounted() {
186
+ // `$on()` returns its own remover.
187
+ return this.$options.events.map((type) => this.$on(type, () => console.log(type)));
188
+ }
189
+ }
190
+ ```
191
+
192
+ ## Negotiated events
193
+
194
+ Two helpers let a component announce a step **before** it happens so an ancestor can take part. They are not `Base` methods and they are absent from `$emits`:
195
+
196
+ | mode | asks for | registers with | keeps | on failure |
197
+ | ------------- | ---------- | -------------- | --------------------- | ------------------------------ |
198
+ | **take over** | the action | `wrap(runner)` | one runner, last wins | the mutation is applied anyway |
199
+ | **delay** | the moment | `waitUntil(x)` | many, all are awaited | the step happens anyway |
200
+
201
+ ```js
202
+ import { Base, EVENTS, domUpdate, emitExtendable, viewTransition } from '@studiometa/js-toolkit';
203
+
204
+ class Panel extends Base {
205
+ static config = { name: 'Panel' };
206
+
207
+ // Take over: the code that mutates announces instead of mutating.
208
+ async render(fragment) {
209
+ await domUpdate(this.$el, () => this.$el.replaceChildren(fragment));
210
+ }
211
+
212
+ // Delay: the choreography announces its step and waits.
213
+ async close() {
214
+ await emitExtendable(this.$el, 'close');
215
+ this.$el.removeAttribute('data-option-open');
216
+ }
217
+
218
+ mounted() {
219
+ return [
220
+ this.$on(EVENTS.dom.update, (event) => event.detail.wrap(viewTransition)),
221
+ this.$on('close', (event) => event.detail.waitUntil(this.leave())),
222
+ ];
223
+ }
224
+
225
+ leave() {
226
+ return Promise.resolve();
227
+ }
228
+ }
229
+ ```
230
+
231
+ - **`defaultPrevented` is ignored.** The step is announced, not proposed.
232
+ - **A registration is valid only while the event dispatches.** A listener that keeps the function and calls it later is warned (`protocol.late-registration`) and ignored.
233
+ - **The work of the emitter always completes.** A runner that throws, rejects, or never calls `apply` loses the animation, never the change.
234
+ - **An unclaimed `domUpdate()` is synchronous.** With no listener the mutation runs before the returned promise exists.
235
+
236
+ See [`domUpdate()`](../../api/dom/domUpdate.md) and [`emitExtendable()`](../../api/dom/emitExtendable.md).
237
+
238
+ ## Framework events
239
+
240
+ `EVENTS` is a deeply frozen object of the events the framework itself dispatches, all namespaced `js-toolkit:`:
241
+
242
+ ```js
243
+ import { EVENTS } from '@studiometa/js-toolkit';
244
+
245
+ EVENTS.component.mounted; // 'js-toolkit:component:mounted'
246
+ EVENTS.component.unmounted; // 'js-toolkit:component:unmounted'
247
+ EVENTS.dom.update; // 'js-toolkit:dom:update'
248
+ EVENTS.diagnostic; // 'js-toolkit:diagnostic'
249
+ ```
250
+
251
+ Component events are typed lower-kebab string literals declared through `$emits`. Private framework transports — the context request, for one — use module-local constants and are deliberately **not** in `EVENTS`.