@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,60 @@
1
+ # useResize
2
+
3
+ ```ts
4
+ useResize(target?: Element): Service<ResizeProps>
5
+ ```
6
+
7
+ The size of one element. With no target it is the document element, which reports the viewport.
8
+
9
+ ## Props
10
+
11
+ ```ts
12
+ interface ResizeProps {
13
+ readonly width: number;
14
+ readonly height: number;
15
+ readonly ratio: number;
16
+ readonly orientation: 'square' | 'landscape' | 'portrait';
17
+ }
18
+ ```
19
+
20
+ `breakpoints` and `activeBreakpoints` are removed — [`useBreakpoint()`](useBreakpoint.md) is their own source now.
21
+
22
+ ## Usage
23
+
24
+ ```js
25
+ import { Base, useResize } from '@studiometa/js-toolkit';
26
+
27
+ class Grid extends Base {
28
+ static config = { name: 'Grid' };
29
+
30
+ mounted() {
31
+ return useResize(this.$el).subscribe(({ width, orientation }) => {
32
+ this.$el.classList.toggle('is-narrow', width < 480);
33
+ });
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## A `ResizeObserver` does not see the viewport
39
+
40
+ It reports the **box of the observed element**, which is what catches a zoom or a scrollbar. For the root element, `clientWidth` and `clientHeight` report the viewport and are decoupled from the observed box — so the viewport service keeps a `resize` listener as well.
41
+
42
+ That is why `useResize()` with no target and `useResize(someElement)` are not the same kind of measurement, even though they share a props shape.
43
+
44
+ ## `{ immediate: true }` works here
45
+
46
+ An element has a current size between deliveries.
47
+
48
+ ## Mixin
49
+
50
+ ```js
51
+ class Grid extends withResize(Base) {
52
+ resized({ width }) {}
53
+ }
54
+ ```
55
+
56
+ `withResize` defaults to the page-wide source, as `withScroll` does. Scope it with `{ target: (instance) => instance.$el }`.
57
+
58
+ ## See also
59
+
60
+ [`useWindowSize()`](useWindowSize.md) — the named default case.
@@ -0,0 +1,88 @@
1
+ # useScroll
2
+
3
+ ```ts
4
+ useScroll(target?: Element | Window): Service<ScrollProps>
5
+ ```
6
+
7
+ Scroll position, movement and direction for one scroller. With no target it is the document element; `useScroll(document.documentElement)` is the window service.
8
+
9
+ ## Props
10
+
11
+ ```ts
12
+ interface ScrollProps {
13
+ readonly x: number;
14
+ readonly y: number;
15
+ readonly deltaX: number;
16
+ readonly deltaY: number;
17
+ readonly maxX: number;
18
+ readonly maxY: number;
19
+ readonly progressX: number;
20
+ readonly progressY: number;
21
+ readonly directionX: -1 | 0 | 1;
22
+ readonly directionY: -1 | 0 | 1;
23
+ readonly isScrolling: boolean;
24
+ }
25
+ ```
26
+
27
+ Nothing derivable is a field: `lastX` is `x - deltaX`, and `changedX` is `deltaX !== 0`.
28
+
29
+ ## Usage
30
+
31
+ ```js
32
+ import { Base, useScroll } from '@studiometa/js-toolkit';
33
+
34
+ class Header extends Base {
35
+ static config = { name: 'Header' };
36
+
37
+ mounted() {
38
+ return useScroll().subscribe(({ y, directionY }) => {
39
+ this.$el.classList.toggle('is-hidden', directionY > 0 && y > 100);
40
+ });
41
+ }
42
+ }
43
+ ```
44
+
45
+ A region rather than the page:
46
+
47
+ ```js
48
+ import { Base, useScroll } from '@studiometa/js-toolkit';
49
+
50
+ class Panel extends Base {
51
+ static config = { name: 'Panel', refs: ['scroller'] };
52
+
53
+ mounted() {
54
+ return useScroll(this.$refs.scroller).subscribe(({ progressY }) => {
55
+ this.$el.style.setProperty('--progress', String(progressY));
56
+ });
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## One `read` per frame
62
+
63
+ The service **coalesces its scroll events into one `read` per frame**, so a page scrolling at speed measures once per frame rather than once per event.
64
+
65
+ ## Extents are observed, not sampled once
66
+
67
+ The service watches the scroller **and its element children** with a `ResizeObserver`, plus a `childList` MutationObserver to keep that set correct: `1 + n` observed boxes per scroller, lazy and released with the last subscriber.
68
+
69
+ That is what keeps `maxY` and `progressY` correct when content is added, removed or resized.
70
+
71
+ ## `{ immediate: true }` works here
72
+
73
+ A scroller has a current position between deliveries, so the first delivery is honoured. The first props of a run carry **no movement**: the deltas are zero and `directionX`/`directionY` are `0`.
74
+
75
+ ## Mixin
76
+
77
+ ```js
78
+ class Header extends withScroll(Base) {
79
+ scrolled({ y, directionY }) {}
80
+ }
81
+ ```
82
+
83
+ `withScroll` defaults to the page-wide source. A region is `withScroll(Base, { target: (instance) => instance.$refs.scroller })`.
84
+
85
+ ## See also
86
+
87
+ - [`useWindowScroll()`](useWindowScroll.md) — the named default case
88
+ - [`useScrollProgress()`](useScrollProgress.md) — an element's progress through the viewport
@@ -0,0 +1,88 @@
1
+ # useScrollProgress
2
+
3
+ ```ts
4
+ useScrollProgress(target: Element, options?: { offset?: string }): Service<ScrollProgressProps>
5
+ ```
6
+
7
+ An element's progress through the viewport as it scrolls.
8
+
9
+ ## Props
10
+
11
+ ```ts
12
+ interface ScrollProgressProps {
13
+ readonly startX: number;
14
+ readonly startY: number;
15
+ readonly endX: number;
16
+ readonly endY: number;
17
+ readonly currentX: number;
18
+ readonly currentY: number;
19
+ readonly progressX: number;
20
+ readonly progressY: number;
21
+ }
22
+ ```
23
+
24
+ `start` and `end` are the scroll positions at which the progress is `0` and `1`; `current` is where the scroller is now.
25
+
26
+ ## Usage
27
+
28
+ ```js
29
+ import { Base, useScrollProgress } from '@studiometa/js-toolkit';
30
+
31
+ class Parallax extends Base {
32
+ static config = { name: 'Parallax' };
33
+
34
+ mounted() {
35
+ return useScrollProgress(this.$el).subscribe(({ progressY }) => {
36
+ this.$el.style.setProperty('--progress', String(progressY));
37
+ });
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## The `offset` option
43
+
44
+ One string holding **two edge pairs**, separated by a `/`:
45
+
46
+ ```
47
+ "<target> <viewport> / <target> <viewport>"
48
+ ```
49
+
50
+ The first pair is where the progress is `0`, the second where it is `1`. In each pair, the first token is an edge of the **target** and the second an edge of the **viewport**.
51
+
52
+ The default is:
53
+
54
+ ```js
55
+ useScrollProgress(el, { offset: 'start end / end start' });
56
+ ```
57
+
58
+ which reads as "0 when the target's start meets the viewport's end, 1 when the target's end meets the viewport's start" — the element travelling the full height of the viewport.
59
+
60
+ Each token is one of:
61
+
62
+ | Token | Means |
63
+ | -------------------------------- | -------------------------------- |
64
+ | `start` | the leading edge |
65
+ | `center` | the middle |
66
+ | `end` | the trailing edge |
67
+ | `50%` | a fraction of the box |
68
+ | `120px` | an absolute offset from the edge |
69
+ | `20vh`, `10vw`, `5vmin`, `5vmax` | a viewport unit |
70
+ | a number | a fraction, as `0.5` |
71
+
72
+ A token that parses as nothing falls back to the edge.
73
+
74
+ The **resolved** offset is part of the service's key, so two callers asking for the same range on the same element share one service.
75
+
76
+ ## Mixin
77
+
78
+ ```js
79
+ class Parallax extends withScrollProgress(Base, { offset: 'center end / center start' }) {
80
+ scrolledInView({ progressY }) {}
81
+ }
82
+ ```
83
+
84
+ The hook is named `scrolledInView`.
85
+
86
+ ## Smoothing a progress value
87
+
88
+ Progress props carry no frame delta, and [`damp()`](../../utils/math.md) takes the elapsed time as a required argument — decay is expressed in time, not in frames. So either take the delta from a [`useRaf()`](useRaf.md) tick, or use [`smoothTo()`](../../utils/motion.md), which owns its own frame subscription and releases it when the value arrives.
@@ -0,0 +1,25 @@
1
+ # useWindowScroll
2
+
3
+ ```ts
4
+ useWindowScroll(): Service<ScrollProps>
5
+ ```
6
+
7
+ The page scroll. It names the default case of [`useScroll()`](useScroll.md), and it is the same instance as `useScroll()` and `useScroll(document.documentElement)`.
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ import { useWindowScroll } from '@studiometa/js-toolkit';
13
+
14
+ const unsubscribe = useWindowScroll().subscribe(({ y, progressY }) => {
15
+ document.documentElement.style.setProperty('--scroll-progress', String(progressY));
16
+ });
17
+ ```
18
+
19
+ Props, coalescing and extent observation are [`useScroll()`](useScroll.md)'s.
20
+
21
+ ## Why it exists
22
+
23
+ `useScroll()` already defaults to the document element, so this export adds no behaviour. It adds a **name**: a call site that means "the page" says so, instead of relying on a reader knowing what the default argument is.
24
+
25
+ `useWindowSize()` is the same idea for [`useResize()`](useResize.md).
@@ -0,0 +1,25 @@
1
+ # useWindowSize
2
+
3
+ ```ts
4
+ useWindowSize(): Service<ResizeProps>
5
+ ```
6
+
7
+ The viewport size. It names the default case of [`useResize()`](useResize.md).
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ import { useWindowSize } from '@studiometa/js-toolkit';
13
+
14
+ const unsubscribe = useWindowSize().subscribe(({ width, height, orientation }) => {
15
+ console.log(width, height, orientation);
16
+ });
17
+ ```
18
+
19
+ Props are [`useResize()`](useResize.md)'s.
20
+
21
+ ## The viewport is not an observed box
22
+
23
+ A `ResizeObserver` reports the box of the element it observes. For the root element, `clientWidth` and `clientHeight` report the **viewport** and are decoupled from that box, so this service keeps a `resize` listener beside the observer.
24
+
25
+ That is the difference between asking for the window and asking for an element that happens to be full-width.
@@ -0,0 +1,126 @@
1
+ # createStorage
2
+
3
+ ```ts
4
+ createStorage<T extends object = Record<string, unknown>>(options?: StorageOptions): StorageInstance<T>
5
+ ```
6
+
7
+ A typed, observable key-value store.
8
+
9
+ ```ts
10
+ interface StorageOptions {
11
+ provider?: StorageProvider;
12
+ serializer?: StorageSerializer;
13
+ prefix?: string;
14
+ }
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { createStorage, memoryStorageProvider } from '@studiometa/js-toolkit';
21
+
22
+ interface Prefs {
23
+ theme: 'light' | 'dark';
24
+ seen: string[];
25
+ }
26
+
27
+ const prefs = createStorage<Prefs>({
28
+ provider: memoryStorageProvider,
29
+ prefix: 'app:',
30
+ });
31
+
32
+ prefs.set('theme', 'dark');
33
+ prefs.get('theme'); // 'light' | 'dark' | undefined
34
+ prefs.get('theme', 'light'); // 'light' | 'dark'
35
+ prefs.has('seen');
36
+ prefs.keys();
37
+ prefs.delete('seen');
38
+ prefs.clear();
39
+ prefs.destroy();
40
+ ```
41
+
42
+ ## The instance
43
+
44
+ ```ts
45
+ interface StorageInstance<T extends object = Record<string, unknown>> {
46
+ get<K extends keyof T>(key: K): T[K] | undefined;
47
+ get<K extends keyof T>(key: K, defaultValue: T[K]): T[K];
48
+ set<K extends keyof T>(key: K, value: T[K]): void;
49
+ delete<K extends keyof T>(key: K): void;
50
+ has<K extends keyof T>(key: K): boolean;
51
+ keys(): (keyof T)[];
52
+ clear(): void;
53
+ subscribe<K extends keyof T>(
54
+ key: K,
55
+ callback: (value: T[K] | undefined) => void,
56
+ options?: { immediate?: boolean },
57
+ ): () => void;
58
+ destroy(): void;
59
+ }
60
+ ```
61
+
62
+ The two `get` overloads are the whole difference between "it may not be there" and "here is what to use if it is not".
63
+
64
+ ## Observing a key
65
+
66
+ ```ts
67
+ import { createStorage } from '@studiometa/js-toolkit';
68
+
69
+ interface Prefs {
70
+ theme: 'light' | 'dark';
71
+ }
72
+ const prefs = createStorage<Prefs>();
73
+ const unsubscribe = prefs.subscribe(
74
+ 'theme',
75
+ (value) => {
76
+ document.documentElement.dataset.theme = value ?? 'light';
77
+ },
78
+ { immediate: true },
79
+ );
80
+ ```
81
+
82
+ A [`Signal`](../context/signal.md) is created **per key, on the first subscription**, so a store nobody observes holds nothing.
83
+
84
+ In a component, hand the unsubscribe back from `mounted()`:
85
+
86
+ ```js
87
+ mounted() {
88
+ return prefs.subscribe('theme', (value) => this.apply(value));
89
+ }
90
+ ```
91
+
92
+ ## `destroy()`
93
+
94
+ Releases every subscription and the shared sync listeners at once. A store that outlives the page needs no call; a store scoped to a component does.
95
+
96
+ ## `prefix`
97
+
98
+ Namespaces every key in the underlying provider, so two stores can share one backing area without colliding. The prefix is the store's business — a provider never sees an unprefixed key and never adds one.
99
+
100
+ ## `serializer`
101
+
102
+ ```ts
103
+ interface StorageSerializer {
104
+ serialize(value: unknown): string;
105
+ deserialize(value: string): unknown;
106
+ }
107
+ ```
108
+
109
+ Defaults to `jsonSerializer`. Failures are reported, not thrown:
110
+
111
+ - `storage.serialize-failed` — **nothing is written**;
112
+ - `storage.deserialize-failed` — the default is returned.
113
+
114
+ ## Syncing with the outside
115
+
116
+ `syncEvents` on the provider is a list of window event names and nothing more. `createStorage()` subscribes **one shared, reference-counted listener per name** while at least one key is observed, and re-reads every observed key when it fires.
117
+
118
+ The event carries no usable state, so the subscriber re-reads rather than trusting a payload.
119
+
120
+ **A known gap**
121
+
122
+ A provider whose changes arrive on a `BroadcastChannel` or through an observer has no way to announce them yet.
123
+
124
+ ## In Node
125
+
126
+ One storage instance runs in Node over the memory provider, which is what `test/package-node-consumer.js` exercises. Nothing in the store touches `window` unless a provider's `syncEvents` asks for it.
@@ -0,0 +1,33 @@
1
+ # Storage
2
+
3
+ **One seam, six adapters.** `createStorage()` is a typed, observable key-value store over a `StorageProvider`.
4
+
5
+ ## The exports
6
+
7
+ - [`createStorage(options?)`](createStorage.md) — the store
8
+ - [Presets](presets.md) — `createLocalStorage()`, `createSessionStorage()`, `createUrlSearchParamsStorage()`, `createUrlSearchParamsInHashStorage()`
9
+ - [Providers](providers.md) — the six adapters, and how to write one
10
+
11
+ ## The division of labour
12
+
13
+ **`createStorage()` owns everything a consumer thinks of as storage**: key namespacing through `prefix`, serialization in both directions, a `Signal` per key created on the first subscription, and the reference-counted wiring that keeps observed keys in sync.
14
+
15
+ **A provider moves strings only** — six synchronous methods, plus an optional list of window event names.
16
+
17
+ ```
18
+ consumer → StorageInstance<T> typed, observable, prefixed, serialized
19
+ ↓
20
+ StorageProvider get/set/remove/has/keys/clear + syncEvents
21
+ ↓
22
+ localStorage, a Map, location.search, …
23
+ ```
24
+
25
+ That is why a custom backend is six small methods, and why one storage instance runs in Node over the memory provider.
26
+
27
+ ## Failures are diagnostics, never throws
28
+
29
+ A built-in provider turns a full quota or a refused area into a `storage.access-failed` diagnostic and returns the method's fallback: `null`, `false`, `[]` or `undefined`. The store's own failures are `storage.serialize-failed` — after which nothing is written — and `storage.deserialize-failed` — after which the default is returned.
30
+
31
+ ## Read next
32
+
33
+ [Storage](../../guide/going-further/using-storage.md).
@@ -0,0 +1,58 @@
1
+ # Storage presets
2
+
3
+ Four presets that each remove an argument from every call site.
4
+
5
+ ```ts
6
+ createLocalStorage<T>(options?): StorageInstance<T>
7
+ createSessionStorage<T>(options?): StorageInstance<T>
8
+ createUrlSearchParamsStorage<T>(options?): StorageInstance<T>
9
+ createUrlSearchParamsInHashStorage<T>(options?): StorageInstance<T>
10
+ ```
11
+
12
+ Their options are [`createStorage()`](createStorage.md)'s **without `provider`** — the preset supplies it. The two URL presets also take `push`.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import {
18
+ createLocalStorage,
19
+ createSessionStorage,
20
+ createUrlSearchParamsInHashStorage,
21
+ createUrlSearchParamsStorage,
22
+ } from '@studiometa/js-toolkit';
23
+
24
+ interface Prefs {
25
+ theme: 'light' | 'dark';
26
+ }
27
+
28
+ createLocalStorage<Prefs>({ prefix: 'app:' });
29
+ createSessionStorage<Prefs>();
30
+ createUrlSearchParamsStorage<Prefs>({ push: true });
31
+ createUrlSearchParamsInHashStorage<Prefs>();
32
+ ```
33
+
34
+ ## `push`
35
+
36
+ The two URL presets choose `history.pushState` over the default `replaceState`:
37
+
38
+ | `push` | Each write |
39
+ | ------- | -------------------------- |
40
+ | `false` | replaces the current entry |
41
+ | `true` | adds a history entry |
42
+
43
+ `true` is what makes a filter state navigable with the back button. `false` is what keeps a scroll position or a tab index out of the history.
44
+
45
+ ## Which URL preset
46
+
47
+ | Preset | Reads and writes | Survives |
48
+ | -------------------------------------- | -------------------------- | ---------------------------------------- |
49
+ | `createUrlSearchParamsStorage()` | `location.search` | a share, a bookmark, the server |
50
+ | `createUrlSearchParamsInHashStorage()` | `location.hash`, as params | a share and a bookmark, never the server |
51
+
52
+ Both rebuild the **whole location** on each write, so a search write keeps the hash and a hash write keeps the query string.
53
+
54
+ ## Why only these four
55
+
56
+ **A factory exists only where its product has state.** These four each replace an argument at every call site, which is worth an export. `createLocalStorageProvider()` and `createSessionStorageProvider()` are **removed** — the instances say the same thing with nothing to call.
57
+
58
+ See [Providers](providers.md).
@@ -0,0 +1,109 @@
1
+ # Storage providers
2
+
3
+ A provider moves **strings only**.
4
+
5
+ ```ts
6
+ interface StorageProvider {
7
+ get(key: string): string | null;
8
+ set(key: string, value: string): void;
9
+ remove(key: string): void;
10
+ has(key: string): boolean;
11
+ keys(): string[];
12
+ clear(): void;
13
+ syncEvents?: readonly string[];
14
+ }
15
+ ```
16
+
17
+ ## The six adapters
18
+
19
+ | Adapter | Backed by | `syncEvents` |
20
+ | --------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------ |
21
+ | `localStorageProvider` | `localStorage` | `storage` |
22
+ | `sessionStorageProvider` | `sessionStorage` | `storage` |
23
+ | `memoryStorageProvider` / `createMemoryStorageProvider()` | a `Map` | — |
24
+ | `createFallbackProvider(...providers)` | reads from the first that holds the key, writes to all | the union |
25
+ | `urlSearchParamsProvider` / `createUrlSearchParamsProvider(options?)` | `location.search` | `popstate` |
26
+ | `urlSearchParamsInHashProvider` / `createUrlSearchParamsInHashProvider(options?)` | `location.hash`, as params | `popstate`, `hashchange` |
27
+
28
+ ```ts
29
+ import {
30
+ createFallbackProvider,
31
+ createMemoryStorageProvider,
32
+ createStorage,
33
+ localStorageProvider,
34
+ memoryStorageProvider,
35
+ } from '@studiometa/js-toolkit';
36
+
37
+ // Try localStorage, fall back to memory when it is refused.
38
+ createStorage({
39
+ provider: createFallbackProvider(localStorageProvider, memoryStorageProvider),
40
+ });
41
+
42
+ // A private Map, not the shared singleton.
43
+ createStorage({ provider: createMemoryStorageProvider() });
44
+ ```
45
+
46
+ **The memory singleton is shared**
47
+
48
+ `memoryStorageProvider` is one `Map` for the whole page. Two stores over it see each other's keys unless they use different prefixes. For a store per component, use `createMemoryStorageProvider()`.
49
+
50
+ ## `createFallbackProvider()`
51
+
52
+ Reads from the **first provider that holds the key** and writes to **all** of them. That is what makes "use localStorage where it works, memory where it does not" one line rather than a branch at every call site.
53
+
54
+ ## A factory exists only where its product has state
55
+
56
+ | Has state or arguments | Does not |
57
+ | ----------------------------------------------- | -------------------------- |
58
+ | `createMemoryStorageProvider()` — a `Map` | `localStorageProvider` |
59
+ | `createFallbackProvider(...)` | `sessionStorageProvider` |
60
+ | `createUrlSearchParamsProvider(options?)` | `memoryStorageProvider` |
61
+ | `createUrlSearchParamsInHashProvider(options?)` | the two bare URL instances |
62
+
63
+ `createLocalStorageProvider()` and `createSessionStorageProvider()` are removed; the instances stay.
64
+
65
+ ## A built-in provider never throws
66
+
67
+ `guard()` turns a full quota or a refused area into a `storage.access-failed` diagnostic and returns the method's fallback:
68
+
69
+ | Method | Fallback |
70
+ | ------------------------ | ----------- |
71
+ | `get` | `null` |
72
+ | `has` | `false` |
73
+ | `keys` | `[]` |
74
+ | `set`, `remove`, `clear` | `undefined` |
75
+
76
+ It reports **once per operation**, not once per area. **The area is resolved per call, inside the guard**, because the getter itself throws when storage is denied — `localStorage` in a blocked third-party frame throws on access, not on use.
77
+
78
+ `types.ts` states the same contract for a custom provider: report, return the fallback, do not throw.
79
+
80
+ ## The URL adapters
81
+
82
+ They **rebuild the whole location on each write**, so a search write keeps the hash and a hash write keeps the query string. Their one option is `push`, which chooses `history.pushState` over the default `replaceState`.
83
+
84
+ ## Writing your own
85
+
86
+ ```ts
87
+ import { createStorage, type StorageProvider } from '@studiometa/js-toolkit';
88
+
89
+ const cookieProvider: StorageProvider = {
90
+ get: (key) => new URLSearchParams(document.cookie.replaceAll('; ', '&')).get(key),
91
+ set: (key, value) => {
92
+ document.cookie = `${key}=${value};path=/`;
93
+ },
94
+ remove: (key) => {
95
+ document.cookie = `${key}=;path=/;max-age=0`;
96
+ },
97
+ has: (key) => document.cookie.includes(`${key}=`),
98
+ keys: () => [...new URLSearchParams(document.cookie.replaceAll('; ', '&')).keys()],
99
+ clear: () => {},
100
+ };
101
+
102
+ const store = createStorage({ provider: cookieProvider });
103
+ ```
104
+
105
+ Six synchronous string methods. Namespacing, serialization, signals and sync wiring are [`createStorage()`](createStorage.md)'s — do none of it here.
106
+
107
+ ## Tested at the seam
108
+
109
+ `providers.spec.ts` drives each adapter through the six methods for real, including a `setItem` that throws, an area getter that throws, `push` against `history.length`, and the `syncEvents` names of each adapter.