@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,90 @@
1
+ # Instance properties
2
+
3
+ ## `$el`
4
+
5
+ ```ts
6
+ readonly $el: HTMLElement
7
+ ```
8
+
9
+ The element the instance is bound to. Typed by the props type's `$el` key.
10
+
11
+ ## `$id` {#id}
12
+
13
+ ```ts
14
+ readonly $id: string
15
+ ```
16
+
17
+ `<ComponentName>-<sequence>`, where the name comes from the resolved config and the sequence increases once for each constructed instance.
18
+
19
+ The id exists **before the field initializers of derived classes run**, and it does not change through unmount and mount cycles. Core never copies it to a DOM `id`.
20
+
21
+ ```js
22
+ // 'Slider-3'
23
+ ```
24
+
25
+ ## `$refs`
26
+
27
+ ```ts
28
+ readonly $refs: Refs<T>
29
+ ```
30
+
31
+ A live view over the declared refs. **Each property reads the DOM on access**, so markup put into the component is found with no refresh and no detached element stays in a list. There is no `$update()`.
32
+
33
+ A single declaration gives the first match; a `name[]` declaration gives an array. The property name never carries the suffix.
34
+
35
+ Lookups are cached, and the cache is invalidated by a counter the framework's MutationObserver increases; reading that counter drains the pending records with `takeRecords()`. Detached elements are never cached.
36
+
37
+ See [Refs](../guide/introduction/managing-refs.md).
38
+
39
+ ## `$options`
40
+
41
+ ```ts
42
+ readonly $options: Readonly<Options<T>>
43
+ ```
44
+
45
+ A **read-only view** over the declared options. Every property is defined with a getter and no setter, so the value is derived from the element and the viewport on each access, and an assignment throws:
46
+
47
+ ```js
48
+ this.$options.open = true;
49
+ // TypeError: Cannot set property open of #<Object> which has only a getter
50
+ ```
51
+
52
+ Every option is responsive, resolved by walking from the active breakpoint down to the base value.
53
+
54
+ The one exception to "nothing is stored": a **default** built by a factory lives on the instance, so mutating it sticks for that instance. Replacing the option itself still throws.
55
+
56
+ See [Options](../guide/introduction/managing-options.md).
57
+
58
+ ## `$config`
59
+
60
+ ```ts
61
+ get $config(): BaseConfig
62
+ ```
63
+
64
+ The merged config, walked along the prototype chain. `refs`, `options` and `components` merge; scalar keys are the most derived class's.
65
+
66
+ See [Configuration](configuration.md).
67
+
68
+ ## `$isMounted`
69
+
70
+ ```ts
71
+ get $isMounted(): boolean
72
+ ```
73
+
74
+ `true` between `mounted()` and the next `$unmount()`.
75
+
76
+ There is deliberately no `getMountedInstance()`: a caller holding one instance reads this instead.
77
+
78
+ ## `$services`
79
+
80
+ ```ts
81
+ readonly $services: { readonly [K in Hook]: Toggle }
82
+ ```
83
+
84
+ Present only on a class built with a [service mixin](methods-hooks-services.md). One [`Toggle`](services/toggle.md) per bound hook, which is what `{ manual: true }` is for. Stacked mixins accumulate their keys.
85
+
86
+ ## Fixed, not fields
87
+
88
+ `$el`, `$id`, `$options` and `$refs` are **fixed properties**, defined non-writable in the constructor. An assignment throws in a module rather than replacing what every other part of the framework reads: the element, the id, and the two live views.
89
+
90
+ `readonly` states it for a reader with a build step; the property descriptor states it for everyone else. They stay enumerable, so an instance still reads as one in a console or a `JSON.stringify`.
@@ -0,0 +1,149 @@
1
+ # Events hooks
2
+
3
+ Five naming conventions bind a method to an event source. All five are bound for the **mount cycle** and removed by `$unmount()`.
4
+
5
+ | Method name | Listens to | Payload |
6
+ | ------------------- | --------------------------------------------- | ---------------------------- |
7
+ | `on<Event>` | the component's own element | the raw event |
8
+ | `on<Ref><Event>` | a declared ref, delegated | `{ event, target, index }` |
9
+ | `on<Child><Event>` | a component in `config.components`, delegated | `{ event, target, payload }` |
10
+ | `onWindow<Event>` | `window` | `{ event, target }` |
11
+ | `onDocument<Event>` | `document` | `{ event, target }` |
12
+
13
+ ## Resolution order
14
+
15
+ `onWindow` and `onDocument` are **reserved prefixes** and match first. After them a name is resolved **children-first**, then refs.
16
+
17
+ `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')`.
18
+
19
+ The rule is about method names only. `onClick` and `onDocumentClick` are different names and both can exist on one component; a click on the element fires both.
20
+
21
+ ## `on<Event>`
22
+
23
+ ```js
24
+ import { Base } from '@studiometa/js-toolkit';
25
+
26
+ class Toggle extends Base {
27
+ static config = { name: 'Toggle' };
28
+
29
+ onClick(event) {
30
+ event.preventDefault();
31
+ }
32
+ }
33
+ ```
34
+
35
+ The event name is the method name after `on`, in lower case: `onPointerDown` is `pointerdown`.
36
+
37
+ ## `on<Ref><Event>`
38
+
39
+ ```ts
40
+ import { Base, type RefEvent } from '@studiometa/js-toolkit';
41
+
42
+ class Tabs extends Base {
43
+ static config = { name: 'Tabs', refs: ['tabs[]'] };
44
+
45
+ onTabsClick({ event, target, index }: RefEvent<HTMLButtonElement>) {
46
+ console.log(index, target.textContent);
47
+ }
48
+ }
49
+ ```
50
+
51
+ ```ts
52
+ interface RefEvent<T extends HTMLElement = HTMLElement> {
53
+ event: Event;
54
+ target: T;
55
+ index: number;
56
+ }
57
+ ```
58
+
59
+ `target` is the **ref element** the handler matched, not `event.target`. `index` is the position in a list ref, or `0` for a single ref.
60
+
61
+ The ref is named as it is **declared**: `onTabsClick` for `config.refs: ['tabs[]']`.
62
+
63
+ ## `on<Child><Event>`
64
+
65
+ ```ts
66
+ import { Base, type DelegatedEvent } from '@studiometa/js-toolkit';
67
+
68
+ class AccordionItem extends Base<{ $emits: { open: { height: number } } }> {
69
+ static config = { name: 'AccordionItem' };
70
+ }
71
+
72
+ class Accordion extends Base {
73
+ static config = { name: 'Accordion', components: { AccordionItem } };
74
+
75
+ onAccordionItemOpen({ event, target, payload }: DelegatedEvent<AccordionItem, 'open'>) {
76
+ console.log(target.$id, payload.height);
77
+ }
78
+ }
79
+ ```
80
+
81
+ ```ts
82
+ interface DelegatedEvent<T extends Base = Base, K extends string = string> {
83
+ event: Event;
84
+ target: T;
85
+ payload: EmitDetail<PropsOf<T>, K>;
86
+ }
87
+ ```
88
+
89
+ `target` is the child **instance**. `payload` is `event.detail`, typed from the child's `$emits`.
90
+
91
+ **`config.components` is what disambiguates the name**
92
+
93
+ `onSliderDragStart` is `SliderDrag` + `start`, or `Slider` + `drag-start`. The name set from `config.components` decides. A child that is not declared there is not resolved.
94
+
95
+ A lazy child works the same way, because the key is the name and nothing has to be downloaded to resolve a handler.
96
+
97
+ ## `onWindow<Event>` and `onDocument<Event>`
98
+
99
+ ```ts
100
+ import { Base, type GlobalEvent } from '@studiometa/js-toolkit';
101
+
102
+ class ClickOutside extends Base {
103
+ static config = { name: 'ClickOutside' };
104
+
105
+ onDocumentClick({ event }: GlobalEvent<MouseEvent>) {
106
+ if (!event.composedPath().includes(this.$el)) {
107
+ this.$el.removeAttribute('data-option-open');
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ ```ts
114
+ interface GlobalEvent<T extends Event = Event> {
115
+ event: T;
116
+ target: Window | Document;
117
+ }
118
+ ```
119
+
120
+ - **Phase: bubble, always.** `onDocumentClick` hears what `document.addEventListener('click', …)` hears. To hear a descendant's non-bubbling event, use `on<Ref><Event>`.
121
+ - `target` is the global the handler names. There is no `payload` and no `index`.
122
+ - Listener options such as `once` and `passive` are not part of this. Use [`$on()`](instance-methods.md#on) for those.
123
+
124
+ ## Delegation
125
+
126
+ Ref and child handlers are delegated from `this.$el`:
127
+
128
+ - **One listener per event type** on the root element.
129
+ - 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.
130
+ - A ref or a child inserted later needs **no new binding**.
131
+ - Events that do not bubble — `focus`, `blur`, `scroll`, `mouseenter`, `mouseleave` — are delegated from the **capture** phase.
132
+
133
+ ## Typing
134
+
135
+ A method named by convention is **not typed by convention**: the name is resolved at runtime, so annotate the payload with `RefEvent`, `DelegatedEvent` or `GlobalEvent`.
136
+
137
+ The [`@on` decorator](decorators/on.md) checks that annotation against a real target instead of leaving it unverified.
138
+
139
+ ## Dynamic sets of events
140
+
141
+ A handler name belongs to the class. When the set of events is **data** — one subscription per markup declaration, with its own modifiers — bind it yourself and own the cleanup:
142
+
143
+ ```js
144
+ mounted() {
145
+ return this.$options.events.map((type) => this.$on(type, this.handle));
146
+ }
147
+ ```
148
+
149
+ That is the intended path, not a workaround.
@@ -0,0 +1,90 @@
1
+ # Lifecycle hooks
2
+
3
+ ```ts
4
+ mounted(): MountedReturn
5
+ unmounted(): void
6
+ ```
7
+
8
+ ```ts
9
+ type MountedReturn = void | (() => void) | MountedReturn[] | Promise<MountedReturn>;
10
+ ```
11
+
12
+ Both are meant to be overridden and neither needs a `super` call.
13
+
14
+ ## `mounted()`
15
+
16
+ Runs when the element is in the document and the component's mount conditions are met. It runs **after** every `option<Name>Changed()` hook of the same cycle.
17
+
18
+ ```js
19
+ import { Base } from '@studiometa/js-toolkit';
20
+
21
+ class Player extends Base {
22
+ static config = { name: 'Player' };
23
+
24
+ mounted() {
25
+ this.$el.setAttribute('aria-live', 'polite');
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### It returns its cleanup
31
+
32
+ A function, an array of functions, sync or async. They run on the next `$unmount()`:
33
+
34
+ ```js
35
+ import { Base, useScroll } from '@studiometa/js-toolkit';
36
+
37
+ class Header extends Base {
38
+ static config = { name: 'Header' };
39
+
40
+ mounted() {
41
+ return useScroll().subscribe(({ directionY }) => {
42
+ this.$el.classList.toggle('is-hidden', directionY > 0);
43
+ });
44
+ }
45
+ }
46
+ ```
47
+
48
+ If an async `mounted()` resolves **after** the unmount, the cleanup runs immediately.
49
+
50
+ ## `unmounted()`
51
+
52
+ Runs at the end of `$unmount()`, after the cycle's listeners are unbound, the `mounted()` cleanups have run and the scheduled tasks are cancelled.
53
+
54
+ ```js
55
+ import { Base } from '@studiometa/js-toolkit';
56
+
57
+ class Player extends Base {
58
+ static config = { name: 'Player' };
59
+
60
+ unmounted() {
61
+ this.$el.removeAttribute('aria-live');
62
+ }
63
+ }
64
+ ```
65
+
66
+ It stays available for the cases the returned cleanup does not fit. When both exist, the returned cleanup runs first.
67
+
68
+ ## What triggers each
69
+
70
+ | Cause | Calls |
71
+ | ------------------------------------------- | --------------------------------------------- |
72
+ | the element enters the document | `mounted()` |
73
+ | a mount strategy's condition becomes true | `mounted()` |
74
+ | the element leaves the document | `unmounted()` |
75
+ | the component token leaves `data-component` | `unmounted()`, then the instance is dropped |
76
+ | a breakpoint withdraws the declaration | `unmounted()`, then the instance is dropped |
77
+ | a reversible strategy's condition ends | `unmounted()` |
78
+ | the node is **moved** | `unmounted()` then `mounted()`, same identity |
79
+
80
+ **Unmounting a parent does not unmount its children.**
81
+
82
+ ## What is not a lifecycle hook
83
+
84
+ - **`updated()` does not exist.** For an option that chooses a resource, use [`option<Name>Changed()`](methods-hooks-options.md). For an attribute the framework does not read, use [`watchAttributes()`](dom/watchAttributes.md).
85
+ - **There is no permanent state.** A component never declares that its work is over. "Once per element" is a plain field — see [Lifecycle](../guide/introduction/lifecycle-hooks.md#do-this-once-per-element).
86
+ - **A service mixin never occupies either hook.** It overrides `$mount()`/`$unmount()`, so a class that writes its own `mounted()` without `super.mounted()` still subscribes.
87
+
88
+ ## Failures
89
+
90
+ A hook that throws is reported once as `component.lifecycle-failed` and the cycle continues. It does not stop the other instances in the same batch.
@@ -0,0 +1,96 @@
1
+ # Options hooks
2
+
3
+ ```ts
4
+ option<Name>Changed(change: OptionChange): OptionChangedReturn
5
+ ```
6
+
7
+ A declared method named after an option makes that option a **live effect**. A component without the convention pays no setup cost and reads its options directly.
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ import { Base } from '@studiometa/js-toolkit';
13
+
14
+ function connect(url) {
15
+ return { dispose() {} };
16
+ }
17
+
18
+ class Feed extends Base {
19
+ static config = {
20
+ name: 'Feed',
21
+ options: { source: String },
22
+ };
23
+
24
+ optionSourceChanged({ value, previousValue, initial }) {
25
+ const connection = connect(value);
26
+ // The cleanup for *this* value.
27
+ return () => connection.dispose();
28
+ }
29
+ }
30
+ ```
31
+
32
+ The method name is the option name in pascal case: `dragThreshold` is `optionDragThresholdChanged`.
33
+
34
+ ## The payload
35
+
36
+ ```ts
37
+ interface OptionChange<T = unknown> {
38
+ name: string;
39
+ value: T;
40
+ previousValue: T | undefined;
41
+ rawValue: string | null;
42
+ previousRawValue: string | null;
43
+ initial: boolean;
44
+ }
45
+ ```
46
+
47
+ | Field | Meaning |
48
+ | ------------------ | ----------------------------------------------------- |
49
+ | `name` | the option name |
50
+ | `value` | the parsed value now |
51
+ | `previousValue` | the parsed value before, `undefined` on the first run |
52
+ | `rawValue` | the raw attribute string, `null` when absent |
53
+ | `previousRawValue` | the raw string before, `null` when it was absent |
54
+ | `initial` | `true` on the first run of a mount cycle |
55
+
56
+ ```ts
57
+ import { Base, type OptionChange } from '@studiometa/js-toolkit';
58
+
59
+ class Feed extends Base {
60
+ static config = { name: 'Feed', options: { source: String } };
61
+
62
+ optionSourceChanged({ value, previousValue, initial }: OptionChange<string>) {
63
+ console.log(value, previousValue, initial);
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## The return value
69
+
70
+ ```ts
71
+ type OptionChangedReturn = void | (() => void);
72
+ ```
73
+
74
+ Return a function to release what this value acquired. The previous cleanup runs **before** an update, and every active cleanup runs on `$unmount()`.
75
+
76
+ ## When it runs
77
+
78
+ - **Before `mounted()`**, on each mount cycle, with `initial: true`.
79
+ - On an attribute change, once the batch is processed.
80
+ - On a **breakpoint crossing** that changes the resolved raw value.
81
+ - Removal of the attribute applies the declared default, which is a change like any other.
82
+ - A new mount starts each effect again with `initial: true`.
83
+
84
+ ## Coalescing
85
+
86
+ Several writes to one attribute in one batch give **one** change, from the first old raw value to the final DOM value. A write that ends where it started is **not** a change.
87
+
88
+ The comparison uses raw strings, so a breakpoint crossing and an attribute write are the same kind of event. A crossing to the same resolved value announces nothing, and a write to `data-option-columns:s` while the viewport is at `l` announces nothing.
89
+
90
+ ## What it costs
91
+
92
+ **A `matchMedia` subscription opens only for a component that declares one of these hooks.** `$unmount()` releases it. A page that only reads options holds no listener at all.
93
+
94
+ ## Not for arbitrary attributes
95
+
96
+ This hook is for **declared options**. For an attribute the framework does not read, use [`watchAttributes()`](dom/watchAttributes.md), which shares the same coalescing rule and the same batch.
@@ -0,0 +1,164 @@
1
+ # Services hooks
2
+
3
+ A service mixin binds one subscription per mount cycle, under the **one method name the service owns**. There is no `hook` option.
4
+
5
+ | Mixin | Hook | Props type |
6
+ | -------------------- | ---------------- | --------------------- |
7
+ | `withRaf` | `ticked` | `RafProps` |
8
+ | `withScroll` | `scrolled` | `ScrollProps` |
9
+ | `withResize` | `resized` | `ResizeProps` |
10
+ | `withPointer` | `moved` | `PointerProps` |
11
+ | `withDrag` | `dragged` | `DragProps` |
12
+ | `withKey` | `keyed` | `KeyProps` |
13
+ | `withInView` | `intersected` | `InViewProps` |
14
+ | `withMutation` | `mutated` | `MutationProps` |
15
+ | `withScrollProgress` | `scrolledInView` | `ScrollProgressProps` |
16
+
17
+ ## Usage
18
+
19
+ ```js
20
+ import { Base, withScroll } from '@studiometa/js-toolkit';
21
+
22
+ class Header extends withScroll(Base) {
23
+ static config = { name: 'Header' };
24
+
25
+ scrolled({ y, directionY, isScrolling }) {
26
+ this.$el.classList.toggle('is-hidden', directionY > 0 && y > 100);
27
+ }
28
+ }
29
+ ```
30
+
31
+ Mixins stack, and their `$services` keys accumulate:
32
+
33
+ ```js
34
+ import { Base, withRaf, withResize } from '@studiometa/js-toolkit';
35
+
36
+ class Parallax extends withRaf(withResize(Base)) {
37
+ static config = { name: 'Parallax' };
38
+
39
+ #height = 0;
40
+
41
+ resized({ height }) {
42
+ this.#height = height;
43
+ }
44
+
45
+ ticked({ delta }) {
46
+ // …
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## A mixin never occupies a lifecycle hook
52
+
53
+ `mounted()` and `unmounted()` belong to the component author, so **nothing has to be chained**. A class that mixes a service in and writes its own `mounted()` without `super.mounted()` still subscribes: the framework's own `$mount()`/`$unmount()` pair carries the subscription.
54
+
55
+ The consequences are worth knowing:
56
+
57
+ - The subscription starts **once the whole of `mounted()` has run** — including an `immediate` first delivery, which therefore reaches a component that is fully set up.
58
+ - It is released **before `unmounted()`**, exactly where the mount cleanup used to release it.
59
+ - `$unmount()` releases unconditionally, so a manual subscription started outside a mount cycle is released too.
60
+
61
+ **A userland mixin still chains**
62
+
63
+ The rule is about what the mixin **overrides**, not about who wrote it. A mixin that puts its work in `mounted()` needs its subclasses to call `super.mounted()` — and the way not to need that is to override `$mount()`.
64
+
65
+ ## Mixin options
66
+
67
+ ```ts
68
+ interface ServiceMixinOptions<Target, Host = Base> {
69
+ target?: (instance: Host) => Target;
70
+ manual?: boolean;
71
+ immediate?: boolean;
72
+ }
73
+ ```
74
+
75
+ **The options of a mixin are not the options of the service.** `target`, `manual` and `immediate` describe the subscription. They are removed before the underlying `use*()` call and they are absent from its options type.
76
+
77
+ A service's own options are passed in the same object and forwarded:
78
+
79
+ ```js
80
+ class Slide extends withDrag(Base, { axis: 'x', inertia: false, immediate: true }) {}
81
+ ```
82
+
83
+ ### `target`
84
+
85
+ ```js
86
+ withResize(Base, { target: (instance) => instance.$refs.inner });
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
+ A service whose **own** default target is nothing, like `withRaf`, is untouched.
92
+
93
+ **The resolver is typed against the host as declared**
94
+
95
+ A mixin is applied while the extends clause of its class is still being evaluated, so `withDrag(Base, …)` types its resolver against `Base`. A component reaching further names the shape it needs — `(instance as Base & { readonly target: HTMLElement }).target` — which is an assertion rather than a check. That is why core checks the result at runtime.
96
+
97
+ ### `manual`
98
+
99
+ Declares the hook without running it. `$services.<hook>` is the switch:
100
+
101
+ ```js
102
+ import { Base, withRaf } from '@studiometa/js-toolkit';
103
+
104
+ class SliderItem extends withRaf(Base, { manual: true }) {
105
+ static config = { name: 'SliderItem' };
106
+
107
+ ticked({ delta }) {}
108
+
109
+ onSelected() {
110
+ this.$services.ticked.start();
111
+ }
112
+
113
+ onSettled() {
114
+ this.$services.ticked.stop();
115
+ }
116
+ }
117
+ ```
118
+
119
+ `$services` is declared in the type as `ServiceHandles<'ticked'>`, so the key completes and a wrong name is a type error. Each handle is a [`Toggle`](services/toggle.md).
120
+
121
+ ### `immediate`
122
+
123
+ Asks for the first delivery at subscribe time. The sources with a current value honour it; the ones without do nothing. `withInView` defaults it to `true`.
124
+
125
+ ## `ticked` can return a render function
126
+
127
+ `withRaf` is the one hook whose return value is used: return a function and it runs in the `write` phase of the same frame.
128
+
129
+ ```js
130
+ import { Base, withRaf } from '@studiometa/js-toolkit';
131
+
132
+ class Follower extends withRaf(Base) {
133
+ static config = { name: 'Follower' };
134
+
135
+ ticked({ delta }) {
136
+ const x = this.measure(delta);
137
+ return () => {
138
+ this.$el.style.transform = `translateX(${x}px)`;
139
+ };
140
+ }
141
+
142
+ measure(delta) {
143
+ return delta;
144
+ }
145
+ }
146
+ ```
147
+
148
+ The raf service collects the render functions of its callbacks and cancels a render whose subscriber left between the two phases.
149
+
150
+ ## One hook per class, and that is the limit
151
+
152
+ A mixin binds one subscription, under one name, for each mount cycle. A component whose subscriptions are **one per markup declaration** — one per attribute, 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:
153
+
154
+ ```js
155
+ mounted() {
156
+ return useScroll(this.$refs.panel).subscribe((props) => {});
157
+ }
158
+ ```
159
+
160
+ `on<Event>` has the same shape: a handler name belongs to the class, while a set of events can be data. Neither limit is about a build step — `withRaf(Base)` is an ordinary call, and the name is fixed by how the class is written.
161
+
162
+ ## Failures
163
+
164
+ A subscriber that throws is skipped and reported as `callback.service-failed`. Core dispatches the diagnostic first and calls `reportError()` only when no listener cancelled the event.
@@ -0,0 +1,57 @@
1
+ # defineManifest
2
+
3
+ ```ts
4
+ defineManifest({ modules, mountStrategy }: DefineManifestOptions): ComponentManifest
5
+ ```
6
+
7
+ Turns a map of module paths into a [manifest](registerManifest.md), deriving each `data-component` token from its path.
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ import { defineManifest, fromMetaGlob, registerManifest } from '@studiometa/js-toolkit';
13
+
14
+ registerManifest(
15
+ defineManifest({
16
+ modules: fromMetaGlob(import.meta.glob('./components/*/index.js')),
17
+ }),
18
+ );
19
+ ```
20
+
21
+ **Parameters**
22
+
23
+ - `modules` (`ModuleRecord`) — a map of path to importer, from [`fromMetaGlob()`](fromMetaGlob.md), [`fromWebpackContext()`](fromWebpackContext.md), or written by hand.
24
+ - `mountStrategy` (`MountStrategy`, optional) — a package-wide default.
25
+
26
+ **Return value**
27
+
28
+ - `ComponentManifest`, ready for `registerManifest()`.
29
+
30
+ ## Token derivation
31
+
32
+ The token is the file's base name, and **`index` falls back to the parent directory**:
33
+
34
+ | Path | Token |
35
+ | ------------------------------ | -------- |
36
+ | `./components/Slider.js` | `Slider` |
37
+ | `./components/Slider/index.js` | `Slider` |
38
+ | `./components/nav/Menu.js` | `Menu` |
39
+
40
+ Duplicate tokens give a warning and **keep the first path**.
41
+
42
+ ## The package-wide strategy
43
+
44
+ ```js
45
+ defineManifest({
46
+ modules: fromMetaGlob(import.meta.glob('./components/*/index.js')),
47
+ mountStrategy: 'visible',
48
+ });
49
+ ```
50
+
51
+ Entry wrappers are emitted **only when the strategy is not `eager`**, so the default costs nothing in the output.
52
+
53
+ Any element still overrides it with `data-mount`, and a class's own `config.mountStrategy` takes over once it has loaded.
54
+
55
+ ## What it is not
56
+
57
+ It carries the importer and the strategy, and nothing else. Informational metadata — `packageName`, `subpath`, `exportName`, `group`, `styles`, `integrations` — belongs in the output type of a generator, not in core.
@@ -0,0 +1,33 @@
1
+ # fromMetaGlob
2
+
3
+ ```ts
4
+ fromMetaGlob(glob: Record<string, unknown>): ModuleRecord
5
+ ```
6
+
7
+ Adapts a Vite `import.meta.glob()` result for [`defineManifest()`](defineManifest.md).
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ import { defineManifest, fromMetaGlob, registerManifest } from '@studiometa/js-toolkit';
13
+
14
+ registerManifest(
15
+ defineManifest({
16
+ modules: fromMetaGlob(import.meta.glob('./components/*/index.js')),
17
+ }),
18
+ );
19
+ ```
20
+
21
+ **Parameters**
22
+
23
+ - `glob` — the object `import.meta.glob()` returns, mapping each path to a lazy importer.
24
+
25
+ **Return value**
26
+
27
+ - `ModuleRecord` — a map of path to importer.
28
+
29
+ **Use the lazy form**
30
+
31
+ `import.meta.glob(pattern)` is lazy and gives one importer per path, which is the point. `import.meta.glob(pattern, { eager: true })` downloads everything up front, so a manifest built from it defers nothing — and the adapter **throws a `TypeError`** naming the first path that is not an importer function, rather than building a manifest that silently does nothing lazily.
32
+
33
+ The adapter adds **no bundler dependency**. It reads a plain object of functions, and core names Vite nowhere.