@mmstack/primitives 19.2.0 → 19.2.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.
- package/README.md +121 -3
- package/fesm2022/mmstack-primitives.mjs +282 -10
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +1 -0
- package/lib/element-visibility.d.ts +66 -0
- package/lib/sensors/index.d.ts +2 -0
- package/lib/sensors/mouse-position.d.ts +5 -0
- package/lib/sensors/scroll-position.d.ts +84 -0
- package/lib/sensors/sensor.d.ts +20 -0
- package/lib/sensors/window-size.d.ts +75 -0
- package/lib/until.d.ts +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,8 +23,10 @@ This library provides the following primitives:
|
|
|
23
23
|
- `mapArray` - Maps a reactive array efficently into an array of stable derivations.
|
|
24
24
|
- `toWritable` - Converts a read-only signal to writable using custom write logic.
|
|
25
25
|
- `derived` - Creates a signal with two-way binding to a source signal.
|
|
26
|
-
- `sensor` - A facade function to create various reactive sensor signals (e.g., mouse position, network status, page visibility, dark mode preference)." (This
|
|
26
|
+
- `sensor` - A facade function to create various reactive sensor signals (e.g., mouse position, network status, page visibility, dark mode preference)." (This was the suggestion from before; it just reads a little smoother and more accurately reflects what the facade creates directly).
|
|
27
27
|
- `until` - Creates a Promise that resolves when a signal's value meets a specific condition.
|
|
28
|
+
- `mediaQuery` - A generic primitive that tracks a CSS media query (forms the basis for `prefersDarkMode` and `prefersReducedMotion`).
|
|
29
|
+
- `elementVisibility` - Tracks if an element is intersecting the viewport using IntersectionObserver.
|
|
28
30
|
|
|
29
31
|
---
|
|
30
32
|
|
|
@@ -289,6 +291,8 @@ Enhances any WritableSignal with a complete undo/redo history stack. This is use
|
|
|
289
291
|
```typescript
|
|
290
292
|
import { FormsModule } from '@angular/forms';
|
|
291
293
|
import { JsonPipe } from '@angular/common';
|
|
294
|
+
import { withHistory } from '@mmstack/primitives';
|
|
295
|
+
import { Component, signal, effect } from '@angular/core';
|
|
292
296
|
|
|
293
297
|
@Component({
|
|
294
298
|
selector: 'app-history-demo',
|
|
@@ -321,9 +325,13 @@ export class HistoryDemoComponent {
|
|
|
321
325
|
|
|
322
326
|
### sensor
|
|
323
327
|
|
|
324
|
-
|
|
328
|
+
### sensor
|
|
329
|
+
|
|
330
|
+
The `sensor()` facade provides a unified way to create various reactive sensor signals that track browser events, states, and user preferences. You specify the type of sensor you want (e.g., `'mousePosition'`, `'networkStatus'`, `'windowSize'`, `'dark-mode'`), and it returns the corresponding signal, often with specific properties or methods. These primitives are generally SSR-safe and handle their own event listener cleanup.
|
|
331
|
+
|
|
332
|
+
You can either use the `sensor('sensorType', options)` facade or import the specific sensor functions directly if you prefer.
|
|
325
333
|
|
|
326
|
-
|
|
334
|
+
**Facade Usage Example:**
|
|
327
335
|
|
|
328
336
|
```typescript
|
|
329
337
|
import { sensor } from '@mmstack/primitives';
|
|
@@ -331,10 +339,12 @@ import { effect } from '@angular/core';
|
|
|
331
339
|
|
|
332
340
|
const network = sensor('networkStatus');
|
|
333
341
|
const mouse = sensor('mousePosition', { throttle: 50, coordinateSpace: 'page' });
|
|
342
|
+
const winSize = sensor('windowSize', { throttle: 150 });
|
|
334
343
|
const isDark = sensor('dark-mode');
|
|
335
344
|
|
|
336
345
|
effect(() => console.log('Online:', network().isOnline));
|
|
337
346
|
effect(() => console.log('Mouse X:', mouse().x));
|
|
347
|
+
effect(() => console.log('Window Width:', winSize().width));
|
|
338
348
|
effect(() => console.log('Dark Mode Preferred:', isDark()));
|
|
339
349
|
```
|
|
340
350
|
|
|
@@ -439,6 +449,73 @@ export class VisibilityLoggerComponent {
|
|
|
439
449
|
}
|
|
440
450
|
```
|
|
441
451
|
|
|
452
|
+
#### windowSize
|
|
453
|
+
|
|
454
|
+
Tracks the browser window's inner dimensions (width and height). Updates are throttled by default (100ms). It provides the main throttled signal and an .unthrottled property to access raw updates.
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { Component, effect, computed } from '@angular/core';
|
|
458
|
+
import { sensor } from '@mmstack/primitives'; // Or import { windowSize }
|
|
459
|
+
|
|
460
|
+
@Component({
|
|
461
|
+
selector: 'app-responsive-display',
|
|
462
|
+
standalone: true,
|
|
463
|
+
template: `
|
|
464
|
+
<p>Current Window Size: {{ winSize().width }}px x {{ winSize().height }}px</p>
|
|
465
|
+
<p>Unthrottled: W: {{ winSize.unthrottled().width }} H: {{ winSize.unthrottled().height }}</p>
|
|
466
|
+
@if (isMobileDisplay()) {
|
|
467
|
+
<p>Displaying mobile layout.</p>
|
|
468
|
+
} @else {
|
|
469
|
+
<p>Displaying desktop layout.</p>
|
|
470
|
+
}
|
|
471
|
+
`,
|
|
472
|
+
})
|
|
473
|
+
export class ResponsiveDisplayComponent {
|
|
474
|
+
readonly winSize = sensor('windowSize', { throttle: 150 });
|
|
475
|
+
// Or: readonly winSize = windowSize({ throttle: 150 });
|
|
476
|
+
|
|
477
|
+
readonly isMobileDisplay = computed(() => this.winSize().width < 768);
|
|
478
|
+
|
|
479
|
+
constructor() {
|
|
480
|
+
effect(() => console.log('Window Size (Throttled):', this.winSize()));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
#### scrollPosition
|
|
486
|
+
|
|
487
|
+
Tracks the scroll position (x, y) of the window or a specified HTML element. Updates are throttled by default (100ms). It provides the main throttled signal and an .unthrottled property to access raw updates.
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
import { Component, effect, ElementRef, viewChild } from '@angular/core';
|
|
491
|
+
import { sensor } from '@mmstack/primitives'; // Or import { scrollPosition }
|
|
492
|
+
import { JsonPipe } from '@angular/common';
|
|
493
|
+
|
|
494
|
+
@Component({
|
|
495
|
+
selector: 'app-scroll-indicator',
|
|
496
|
+
standalone: true,
|
|
497
|
+
imports: [JsonPipe],
|
|
498
|
+
template: `
|
|
499
|
+
<div style="height: 100px; border-bottom: 2px solid red; position: fixed; top: 0; left: 0; width: 100%; background: white; z-index: 10;">
|
|
500
|
+
Page Scroll Y: {{ pageScroll().y }}px
|
|
501
|
+
<p>Unthrottled Y: {{ pageScroll.unthrottled().y }}</p>
|
|
502
|
+
</div>
|
|
503
|
+
<div #scrollableContent style="height: 2000px; padding-top: 120px;">Scroll down...</div>
|
|
504
|
+
`,
|
|
505
|
+
})
|
|
506
|
+
export class ScrollIndicatorComponent {
|
|
507
|
+
readonly pageScroll = sensor('scrollPosition', { throttle: 50 });
|
|
508
|
+
// Or: readonly pageScroll = scrollPosition({ throttle: 50 });
|
|
509
|
+
|
|
510
|
+
constructor() {
|
|
511
|
+
effect(() => {
|
|
512
|
+
// Example: Change header style based on scroll
|
|
513
|
+
console.log('Page scroll Y (Throttled):', this.pageScroll().y);
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
442
519
|
#### mediaQuery, prefersDarkMode() & prefersReducedMotion()
|
|
443
520
|
|
|
444
521
|
A generic mediaQuery primitive, you can use directly for any CSS media query. Two specific versions have been created for `prefersDarkMode()` & `prefersReducedMotion()`.
|
|
@@ -499,3 +576,44 @@ it('should reject on timeout if the condition is not met in time', async () => {
|
|
|
499
576
|
await expect(untilPromise).toThrow(`until: Timeout after ${timeoutDuration}ms.`);
|
|
500
577
|
});
|
|
501
578
|
```
|
|
579
|
+
|
|
580
|
+
### elementVisibility
|
|
581
|
+
|
|
582
|
+
Tracks if a target DOM element is intersecting with the viewport (or a specified root element) using the `IntersectionObserver` API. This is highly performant for use cases like lazy-loading content or triggering animations when elements scroll into view.
|
|
583
|
+
|
|
584
|
+
It can observe a static `ElementRef`/`Element` or a `Signal` that resolves to one, allowing for dynamic targets. The returned signal emits the full `IntersectionObserverEntry` object (or `undefined`) & exposes a sub-signal `.visible` which is just a boolean signal for ease of use
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
import { Component, effect, ElementRef, viewChild, computed } from '@angular/core';
|
|
588
|
+
import { elementVisibility } from '@mmstack/primitives';
|
|
589
|
+
|
|
590
|
+
@Component({
|
|
591
|
+
selector: 'app-lazy-load-item',
|
|
592
|
+
standalone: true,
|
|
593
|
+
template: `
|
|
594
|
+
<div #itemToObserve style="height: 300px; margin-top: 100vh; border: 2px solid green;">
|
|
595
|
+
@if (intersectionEntry.isVisible()) {
|
|
596
|
+
<p>This content was lazy-loaded because it became visible!</p>
|
|
597
|
+
} @else {
|
|
598
|
+
<p>Item is off-screen. Scroll down to load it.</p>
|
|
599
|
+
}
|
|
600
|
+
</div>
|
|
601
|
+
`,
|
|
602
|
+
})
|
|
603
|
+
export class LazyLoadItemComponent {
|
|
604
|
+
readonly itemRef = viewChild.required<ElementRef<HTMLDivElement>>('itemToObserve');
|
|
605
|
+
|
|
606
|
+
// Observe the element, get the full IntersectionObserverEntry
|
|
607
|
+
readonly intersectionEntry = elementVisibility(this.itemRef);
|
|
608
|
+
|
|
609
|
+
constructor() {
|
|
610
|
+
effect(() => {
|
|
611
|
+
if (this.isVisible()) {
|
|
612
|
+
console.log('Item is now visible!', this.intersectionEntry());
|
|
613
|
+
} else {
|
|
614
|
+
console.log('Item is no longer visible or not yet visible.');
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { untracked, signal, inject, DestroyRef, computed,
|
|
1
|
+
import { untracked, signal, inject, DestroyRef, computed, PLATFORM_ID, isSignal, effect, ElementRef, linkedSignal, isDevMode, Injector, runInInjectionContext } from '@angular/core';
|
|
2
2
|
import { isPlatformServer } from '@angular/common';
|
|
3
3
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
4
4
|
|
|
@@ -201,6 +201,108 @@ function isDerivation(sig) {
|
|
|
201
201
|
return 'from' in sig;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
function observerSupported() {
|
|
205
|
+
return typeof IntersectionObserver !== 'undefined';
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Creates a read-only signal that tracks the intersection status of a target DOM element
|
|
209
|
+
* with the viewport or a specified root element, using the `IntersectionObserver` API.
|
|
210
|
+
*
|
|
211
|
+
* It can observe a static `ElementRef`/`Element` or a `Signal` that resolves to one,
|
|
212
|
+
* allowing for dynamic targets.
|
|
213
|
+
*
|
|
214
|
+
* @param target The DOM element (or `ElementRef`, or a `Signal` resolving to one) to observe.
|
|
215
|
+
* If the signal resolves to `null`, observation stops.
|
|
216
|
+
* @param options Optional `IntersectionObserverInit` options (e.g., `root`, `rootMargin`, `threshold`)
|
|
217
|
+
* and an optional `debugName`.
|
|
218
|
+
* @returns A `Signal<IntersectionObserverEntry | undefined>`. It emits `undefined` initially,
|
|
219
|
+
* on the server, or if the target is `null`. Otherwise, it emits the latest
|
|
220
|
+
* `IntersectionObserverEntry`. Consumers can derive a boolean `isVisible` from
|
|
221
|
+
* this entry's `isIntersecting` property.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* import { Component, effect, ElementRef, viewChild } from '@angular/core';
|
|
226
|
+
* import { elementVisibility } from '@mmstack/primitives';
|
|
227
|
+
* import { computed } from '@angular/core'; // For derived boolean
|
|
228
|
+
*
|
|
229
|
+
* @Component({
|
|
230
|
+
* selector: 'app-lazy-image',
|
|
231
|
+
* template: `
|
|
232
|
+
* <div #imageContainer style="height: 200px; border: 1px dashed grey;">
|
|
233
|
+
* @if (isVisible()) {
|
|
234
|
+
* <img src="your-image-url.jpg" alt="Lazy loaded image" />
|
|
235
|
+
* <p>Image is VISIBLE!</p>
|
|
236
|
+
* } @else {
|
|
237
|
+
* <p>Scroll down to see the image...</p>
|
|
238
|
+
* }
|
|
239
|
+
* </div>
|
|
240
|
+
* `
|
|
241
|
+
* })
|
|
242
|
+
* export class LazyImageComponent {
|
|
243
|
+
* readonly imageContainer = viewChild.required<ElementRef<HTMLDivElement>>('imageContainer');
|
|
244
|
+
*
|
|
245
|
+
* // Observe the element, get the full IntersectionObserverEntry
|
|
246
|
+
* readonly intersectionEntry = elementVisibility(this.imageContainer);
|
|
247
|
+
*
|
|
248
|
+
* // Derive a simple boolean for visibility
|
|
249
|
+
* readonly isVisible = computed(() => this.intersectionEntry()?.isIntersecting ?? false);
|
|
250
|
+
*
|
|
251
|
+
* constructor() {
|
|
252
|
+
* effect(() => {
|
|
253
|
+
* console.log('Intersection Entry:', this.intersectionEntry());
|
|
254
|
+
* console.log('Is Visible:', this.isVisible());
|
|
255
|
+
* });
|
|
256
|
+
* }
|
|
257
|
+
* }
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
function elementVisibility(target, opt) {
|
|
261
|
+
if (isPlatformServer(inject(PLATFORM_ID)) || !observerSupported()) {
|
|
262
|
+
const base = computed(() => undefined, {
|
|
263
|
+
debugName: opt?.debugName,
|
|
264
|
+
});
|
|
265
|
+
base.visible = computed(() => false);
|
|
266
|
+
return base;
|
|
267
|
+
}
|
|
268
|
+
const state = signal(undefined, {
|
|
269
|
+
debugName: opt?.debugName,
|
|
270
|
+
equal: (a, b) => {
|
|
271
|
+
if (!a && !b)
|
|
272
|
+
return true;
|
|
273
|
+
if (!a || !b)
|
|
274
|
+
return false;
|
|
275
|
+
return (a.target === b.target &&
|
|
276
|
+
a.isIntersecting === b.isIntersecting &&
|
|
277
|
+
a.intersectionRatio === b.intersectionRatio &&
|
|
278
|
+
a.boundingClientRect.top === b.boundingClientRect.top &&
|
|
279
|
+
a.boundingClientRect.left === b.boundingClientRect.left &&
|
|
280
|
+
a.boundingClientRect.width === b.boundingClientRect.width &&
|
|
281
|
+
a.boundingClientRect.height === b.boundingClientRect.height);
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
const targetSignal = isSignal(target) ? target : computed(() => target);
|
|
285
|
+
effect((cleanup) => {
|
|
286
|
+
const el = targetSignal();
|
|
287
|
+
if (!el)
|
|
288
|
+
return state.set(undefined);
|
|
289
|
+
let observer = null;
|
|
290
|
+
observer = new IntersectionObserver(([entry]) => state.set(entry), opt);
|
|
291
|
+
observer.observe(el instanceof ElementRef ? el.nativeElement : el);
|
|
292
|
+
cleanup(() => {
|
|
293
|
+
observer?.disconnect();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
const base = state.asReadonly();
|
|
297
|
+
base.visible = computed(() => {
|
|
298
|
+
const s = state();
|
|
299
|
+
if (!s)
|
|
300
|
+
return false;
|
|
301
|
+
return s.isIntersecting;
|
|
302
|
+
});
|
|
303
|
+
return base;
|
|
304
|
+
}
|
|
305
|
+
|
|
204
306
|
/**
|
|
205
307
|
* Reactively maps items from a source array (or signal of an array) using a provided mapping function.
|
|
206
308
|
*
|
|
@@ -712,6 +814,169 @@ function pageVisibility(debugName) {
|
|
|
712
814
|
return visibility.asReadonly();
|
|
713
815
|
}
|
|
714
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Creates a read-only signal that tracks the scroll position (x, y) of the window
|
|
819
|
+
* or a specified HTML element.
|
|
820
|
+
*
|
|
821
|
+
* Updates are throttled by default to optimize performance. An `unthrottled`
|
|
822
|
+
* property is available on the returned signal for direct access to raw updates.
|
|
823
|
+
* The primitive is SSR-safe and automatically cleans up its event listeners.
|
|
824
|
+
*
|
|
825
|
+
* @param options Optional configuration for the scroll sensor.
|
|
826
|
+
* @returns A `ScrollPositionSignal`. On the server, it returns a static
|
|
827
|
+
* signal with `{ x: 0, y: 0 }`.
|
|
828
|
+
*
|
|
829
|
+
* @example
|
|
830
|
+
* ```ts
|
|
831
|
+
* import { Component, effect, ElementRef, viewChild } from '@angular/core';
|
|
832
|
+
* import { scrollPosition } from '@mmstack/primitives';
|
|
833
|
+
*
|
|
834
|
+
* @Component({
|
|
835
|
+
* selector: 'app-scroll-tracker',
|
|
836
|
+
* template: `
|
|
837
|
+
* <p>Window Scroll: X: {{ windowScroll().x }}, Y: {{ windowScroll().y }}</p>
|
|
838
|
+
* <div #scrollableDiv style="height: 200px; width: 200px; overflow: auto; border: 1px solid black;">
|
|
839
|
+
* <div style="height: 400px; width: 400px;">Scroll me!</div>
|
|
840
|
+
* </div>
|
|
841
|
+
* @if (divScroll()) {
|
|
842
|
+
* <p>Div Scroll: X: {{ divScroll().x }}, Y: {{ divScroll().y }}</p>
|
|
843
|
+
* }
|
|
844
|
+
* `
|
|
845
|
+
* })
|
|
846
|
+
* export class ScrollTrackerComponent {
|
|
847
|
+
* readonly windowScroll = scrollPosition(); // Defaults to window
|
|
848
|
+
* readonly scrollableDiv = viewChild<ElementRef<HTMLDivElement>>('scrollableDiv');
|
|
849
|
+
* readonly divScroll = scrollPosition({ target: this.scrollableDiv() }); // Example with element target
|
|
850
|
+
*
|
|
851
|
+
* constructor() {
|
|
852
|
+
* effect(() => {
|
|
853
|
+
* console.log('Window scrolled to:', this.windowScroll());
|
|
854
|
+
* if (this.divScroll()) {
|
|
855
|
+
* console.log('Div scrolled to:', this.divScroll());
|
|
856
|
+
* }
|
|
857
|
+
* });
|
|
858
|
+
* }
|
|
859
|
+
* }
|
|
860
|
+
* ```
|
|
861
|
+
*/
|
|
862
|
+
function scrollPosition(opt) {
|
|
863
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
864
|
+
const base = computed(() => ({
|
|
865
|
+
x: 0,
|
|
866
|
+
y: 0,
|
|
867
|
+
}), {
|
|
868
|
+
debugName: opt?.debugName,
|
|
869
|
+
});
|
|
870
|
+
base.unthrottled = base;
|
|
871
|
+
return base;
|
|
872
|
+
}
|
|
873
|
+
const { target = window, throttle = 100, debugName } = opt || {};
|
|
874
|
+
let element;
|
|
875
|
+
let getScrollPosition;
|
|
876
|
+
if (target instanceof Window) {
|
|
877
|
+
element = target;
|
|
878
|
+
getScrollPosition = () => {
|
|
879
|
+
return { x: target.scrollX, y: target.scrollY };
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
else if (target instanceof ElementRef) {
|
|
883
|
+
element = target.nativeElement;
|
|
884
|
+
getScrollPosition = () => {
|
|
885
|
+
return {
|
|
886
|
+
x: target.nativeElement.scrollLeft,
|
|
887
|
+
y: target.nativeElement.scrollTop,
|
|
888
|
+
};
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
element = target;
|
|
893
|
+
getScrollPosition = () => {
|
|
894
|
+
return {
|
|
895
|
+
x: target.scrollLeft,
|
|
896
|
+
y: target.scrollTop,
|
|
897
|
+
};
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
const state = throttled(getScrollPosition(), {
|
|
901
|
+
debugName,
|
|
902
|
+
equal: (a, b) => a.x === b.x && a.y === b.y,
|
|
903
|
+
ms: throttle,
|
|
904
|
+
});
|
|
905
|
+
const onScroll = () => state.set(getScrollPosition());
|
|
906
|
+
element.addEventListener('scroll', onScroll, { passive: true });
|
|
907
|
+
inject(DestroyRef).onDestroy(() => element.removeEventListener('scroll', onScroll));
|
|
908
|
+
const base = state.asReadonly();
|
|
909
|
+
base.unthrottled = state.original;
|
|
910
|
+
return base;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Creates a read-only signal that tracks the browser window's inner dimensions (width and height).
|
|
915
|
+
*
|
|
916
|
+
* Updates are throttled by default (100ms) to optimize performance during resize events.
|
|
917
|
+
* An `unthrottled` property is available on the returned signal for direct access to raw updates.
|
|
918
|
+
* The primitive is SSR-safe (returns a default size on the server) and automatically
|
|
919
|
+
* cleans up its event listeners.
|
|
920
|
+
*
|
|
921
|
+
* @param opt Optional configuration, including `throttle` (ms) and `debugName`.
|
|
922
|
+
* @returns A `WindowSizeSignal` (a `Signal<WindowSize>` with an `unthrottled` property).
|
|
923
|
+
*
|
|
924
|
+
* @example
|
|
925
|
+
* ```ts
|
|
926
|
+
* import { Component, effect } from '@angular/core';
|
|
927
|
+
* import { windowSize } from '@mmstack/primitives';
|
|
928
|
+
*
|
|
929
|
+
* @Component({
|
|
930
|
+
* selector: 'app-responsive-header',
|
|
931
|
+
* template: `
|
|
932
|
+
* <header>
|
|
933
|
+
* Current Window Size: {{ size().width }}px x {{ size().height }}px
|
|
934
|
+
* @if (isMobile()) {
|
|
935
|
+
* <p>Mobile Menu</p>
|
|
936
|
+
* } @else {
|
|
937
|
+
* <p>Desktop Menu</p>
|
|
938
|
+
* }
|
|
939
|
+
* </header>
|
|
940
|
+
* `
|
|
941
|
+
* })
|
|
942
|
+
* export class ResponsiveHeaderComponent {
|
|
943
|
+
* readonly size = windowSize();
|
|
944
|
+
* readonly isMobile = computed(() => this.size().width < 768);
|
|
945
|
+
*
|
|
946
|
+
* constructor() {
|
|
947
|
+
* effect(() => {
|
|
948
|
+
* console.log('Window resized to:', this.size());
|
|
949
|
+
* });
|
|
950
|
+
* }
|
|
951
|
+
* }
|
|
952
|
+
* ```
|
|
953
|
+
*/
|
|
954
|
+
function windowSize(opt) {
|
|
955
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
956
|
+
const base = computed(() => ({
|
|
957
|
+
width: 1024,
|
|
958
|
+
height: 768,
|
|
959
|
+
}), { debugName: opt?.debugName });
|
|
960
|
+
base.unthrottled = base;
|
|
961
|
+
return base;
|
|
962
|
+
}
|
|
963
|
+
const sizeSignal = throttled({ width: window.innerWidth, height: window.innerHeight }, {
|
|
964
|
+
debugName: opt?.debugName,
|
|
965
|
+
equal: (a, b) => a.width === b.width && a.height === b.height,
|
|
966
|
+
ms: opt?.throttle ?? 100,
|
|
967
|
+
});
|
|
968
|
+
const onResize = () => {
|
|
969
|
+
sizeSignal.set({ width: window.innerWidth, height: window.innerHeight });
|
|
970
|
+
};
|
|
971
|
+
window.addEventListener('resize', onResize);
|
|
972
|
+
inject(DestroyRef).onDestroy(() => {
|
|
973
|
+
window.removeEventListener('resize', onResize);
|
|
974
|
+
});
|
|
975
|
+
const base = sizeSignal.asReadonly();
|
|
976
|
+
base.unthrottled = sizeSignal.original;
|
|
977
|
+
return base;
|
|
978
|
+
}
|
|
979
|
+
|
|
715
980
|
/**
|
|
716
981
|
* Implementation for sensor overloads.
|
|
717
982
|
* Users should refer to the specific overloads for detailed documentation.
|
|
@@ -729,6 +994,10 @@ function sensor(type, options) {
|
|
|
729
994
|
return prefersDarkMode(options?.debugName);
|
|
730
995
|
case 'reduced-motion':
|
|
731
996
|
return prefersReducedMotion(options?.debugName);
|
|
997
|
+
case 'windowSize':
|
|
998
|
+
return windowSize(options);
|
|
999
|
+
case 'scrollPosition':
|
|
1000
|
+
return scrollPosition(options);
|
|
732
1001
|
default:
|
|
733
1002
|
throw new Error(`Unknown sensor type: ${type}`);
|
|
734
1003
|
}
|
|
@@ -928,6 +1197,7 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
928
1197
|
* ```
|
|
929
1198
|
*/
|
|
930
1199
|
function until(sourceSignal, predicate, options = {}) {
|
|
1200
|
+
const injector = options.injector ?? inject(Injector);
|
|
931
1201
|
return new Promise((resolve, reject) => {
|
|
932
1202
|
let effectRef;
|
|
933
1203
|
let timeoutId;
|
|
@@ -967,14 +1237,16 @@ function until(sourceSignal, predicate, options = {}) {
|
|
|
967
1237
|
if (options?.timeout !== undefined) {
|
|
968
1238
|
timeoutId = setTimeout(() => cleanupAndReject(`until: Timeout after ${options.timeout}ms.`), options.timeout);
|
|
969
1239
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1240
|
+
runInInjectionContext(injector, () => {
|
|
1241
|
+
effectRef = effect(() => {
|
|
1242
|
+
if (settled) {
|
|
1243
|
+
return effectRef?.destroy();
|
|
1244
|
+
}
|
|
1245
|
+
const currentValue = sourceSignal();
|
|
1246
|
+
if (predicate(currentValue)) {
|
|
1247
|
+
cleanupAndResolve(currentValue);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
978
1250
|
});
|
|
979
1251
|
});
|
|
980
1252
|
}
|
|
@@ -1118,5 +1390,5 @@ function withHistory(source, opt) {
|
|
|
1118
1390
|
* Generated bundle index. Do not edit.
|
|
1119
1391
|
*/
|
|
1120
1392
|
|
|
1121
|
-
export { debounce, debounced, derived, isDerivation, isMutable, mapArray, mediaQuery, mousePosition, mutable, networkStatus, pageVisibility, prefersDarkMode, prefersReducedMotion, sensor, stored, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, withHistory };
|
|
1393
|
+
export { debounce, debounced, derived, elementVisibility, isDerivation, isMutable, mapArray, mediaQuery, mousePosition, mutable, networkStatus, pageVisibility, prefersDarkMode, prefersReducedMotion, scrollPosition, sensor, stored, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, windowSize, withHistory };
|
|
1122
1394
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|