ngx-mq 2.11.2 → 3.0.0

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright (c) 2025 Myroslav Martsin
3
+ Copyright (c) 2025 Martsin Labs
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,180 +1,235 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/martsinlabs/ngx-mq/refs/heads/main/assets/logo.svg" width="160" alt="ngx-mq logo" />
2
+ <img src="https://raw.githubusercontent.com/martsinlabs/ngx-mq/refs/heads/main/assets/logo.svg" width="140" alt="ngx-mq" />
3
3
  </p>
4
4
 
5
- <h3 align="center">
6
- Signal-Powered Breakpoints & Media Queries
7
- </h3>
5
+ <h3 align="center">Signal-Powered Breakpoints &amp; Media Queries for Angular</h3>
8
6
 
9
- <h5 align="center">
7
+ <p align="center">
8
+ Reactive <code>matchMedia</code> as Angular signals. SSR-safe, zoneless-ready, and free of RxJS.
9
+ </p>
10
+
11
+ <p align="center">
10
12
  <a href="https://github.com/martsinlabs/ngx-mq/actions/workflows/ci.yml">
11
- <img src="https://img.shields.io/github/actions/workflow/status/martsinlabs/ngx-mq/ci.yml?branch=main&label=CI&color=44cc11&logo=github" alt="CI Status" />
13
+ <img src="https://img.shields.io/github/actions/workflow/status/martsinlabs/ngx-mq/ci.yml?branch=main&label=CI&color=44cc11&logo=github" alt="CI status" />
12
14
  </a>
13
15
  <a href="https://codecov.io/gh/martsinlabs/ngx-mq">
14
16
  <img src="https://codecov.io/gh/martsinlabs/ngx-mq/branch/main/graph/badge.svg" alt="coverage" />
15
17
  </a>
16
- <br>
17
18
  <a href="https://www.npmjs.com/package/ngx-mq">
18
19
  <img src="https://img.shields.io/npm/v/ngx-mq.svg?color=007ec6" alt="npm version" />
19
20
  </a>
20
21
  <a href="https://www.npmjs.com/package/ngx-mq">
21
22
  <img src="https://img.shields.io/npm/dm/ngx-mq.svg?color=44cc11" alt="npm downloads" />
22
23
  </a>
24
+ <a href="https://bundlephobia.com/package/ngx-mq">
25
+ <img src="https://img.shields.io/bundlephobia/minzip/ngx-mq.svg?color=44cc11&label=minzip" alt="minzipped size" />
26
+ </a>
23
27
  <a href="https://opensource.org/license/MIT">
24
28
  <img src="https://img.shields.io/npm/l/ngx-mq.svg?color=44cc11" alt="license" />
25
29
  </a>
26
- </h5>
30
+ </p>
27
31
 
28
- <br>
32
+ ---
29
33
 
30
34
  ## Features
31
35
 
32
- - Lightweight
33
- - SSR-safe
34
- - Auto-cleanup
35
- - Angular 16 next
36
- - Well-tested
36
+ - **Signal-native**: every query is a `Signal<boolean>` that updates as the viewport changes.
37
+ - **SSR-safe**: returns a configurable static value on the server, then hydrates on the client.
38
+ - **Auto-cleanup**: listeners are tied to Angular's `DestroyRef`; no manual teardown.
39
+ - **Efficient**: one shared `matchMedia` listener per unique query, reused across the app.
40
+ - **Batteries included**: Tailwind, Bootstrap and Material breakpoint presets out of the box.
41
+ - **Composable**: combine any signals with `and` / `or` / `not`.
42
+ - **Tiny and tested**: ~1.9 kB gzipped, 100% line coverage.
37
43
 
38
- ## Introduction
44
+ ## Why ngx-mq?
39
45
 
40
- Built on the native [matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) API, NGX-MQ brings a signal-based and declarative way to handle breakpoints and media queries in Angular. Lifecycle management is fully automated via `DestroyRef`, removing the need for manual cleanup. Under the hood, it leverages the Multiton and Flyweight patterns for efficient instance reuse and consistent behavior across your app.
46
+ Angular's CDK ships [`BreakpointObserver`](https://material.angular.io/cdk/layout/overview), which
47
+ works well but is built around RxJS and raw query strings. `ngx-mq` is designed for the signals era:
48
+ templates read a value directly, there is nothing to subscribe to, and cleanup is automatic.
41
49
 
42
- > **Tip:** Always call query utilities within Angular’s [injection context](https://angular.dev/guide/di/dependency-injection-context) to keep them in sync with the framework’s lifecycle.
50
+ | | `ngx-mq` | CDK `BreakpointObserver` |
51
+ | --------------------- | ------------------------------------------------- | ----------------------------------------- |
52
+ | Reactivity | `Signal<boolean>` | `Observable<BreakpointState>` |
53
+ | Cleanup | Automatic via `DestroyRef` | Manual (`unsubscribe` / `takeUntilDestroyed`) |
54
+ | Template usage | `@if (isDesktop())` | `async` pipe or manual subscription |
55
+ | Named breakpoints | Tailwind, Bootstrap, Material presets or your own | Material breakpoints or raw strings |
56
+ | Media-feature helpers | `colorScheme`, `hover`, `pointer`, ... | Raw query strings |
57
+ | Composition | `and` / `or` / `not` | RxJS operators |
58
+ | SSR | Configurable static value | Handle it yourself |
59
+ | Footprint | ~1.9 kB standalone | Part of `@angular/cdk` |
43
60
 
44
- ## Live Demo
61
+ If you already pull in `@angular/cdk` and live in RxJS, `BreakpointObserver` is a fine choice. If you
62
+ want a signals-first, zoneless-friendly API with batteries included, reach for `ngx-mq`.
45
63
 
46
- Try it on [StackBlitz](https://stackblitz.com/github/martsinlabs/ngx-mq-demo/tree/demo/v2?file=src%2Fapp%2Fapp.component.ts)
64
+ ## Documentation
47
65
 
48
- ## Installation
66
+ - **API reference and guides:** https://martsinlabs.github.io/ngx-mq
67
+ - **Live demo:** [StackBlitz](https://stackblitz.com/github/martsinlabs/ngx-mq-demo/tree/demo/v2?file=src%2Fapp%2Fapp.component.ts)
49
68
 
50
- Choose the package version that matches your Angular setup:
69
+ ## Installation
51
70
 
52
- ```bash
53
- # For Angular 16–18
54
- npm install ngx-mq@1
71
+ Install the major version that matches your Angular version:
55
72
 
56
- # For Angular 19–21
57
- npm install ngx-mq@2
58
- ```
73
+ | Angular | Install |
74
+ | ------- | ------------------ |
75
+ | 20 - 22 | `npm i ngx-mq@3` |
76
+ | 19 | `npm i ngx-mq@2` |
77
+ | 16 - 18 | `npm i ngx-mq@1` |
59
78
 
60
- ## Breakpoint API
79
+ ## Quick start
61
80
 
62
- ### Configuration
63
-
64
- Create a custom breakpoint map or use one of the built-in presets (e.g. `provideTailwindBreakpoints()`).
81
+ **1. Provide a breakpoint map** at bootstrap (a custom map or a preset):
65
82
 
66
83
  ```ts
67
84
  import { bootstrapApplication } from '@angular/platform-browser';
68
- import { AppComponent } from './app/app.component';
69
85
  import { provideBreakpoints } from 'ngx-mq';
86
+ import { AppComponent } from './app/app.component';
70
87
 
71
88
  bootstrapApplication(AppComponent, {
72
- providers: [
73
- // Define a custom map (keys are named ranges, values are widths in pixels)
74
- provideBreakpoints({
75
- sm: 640,
76
- md: 768,
77
- lg: 1024,
78
- }),
79
- ],
89
+ providers: [provideBreakpoints({ sm: 640, md: 768, lg: 1024 })],
80
90
  });
81
91
  ```
82
92
 
83
- **Available presets**
93
+ **2. Use the helpers** as signals inside any component:
84
94
 
85
- - **Tailwind** → `sm: 640, md: 768, lg: 1024, xl: 1280, 2xl: 1536`
86
- - **Bootstrap** `sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1400`
87
- - **Material** → `sm: 600, md: 905, lg: 1240, xl: 1440`
95
+ ```ts
96
+ import { Component } from '@angular/core';
97
+ import { up, down, between } from 'ngx-mq';
88
98
 
89
- ### BP-related utilities
99
+ @Component({
100
+ selector: 'app-root',
101
+ template: `
102
+ @if (isDesktop()) {
103
+ <app-sidebar />
104
+ }
105
+ `,
106
+ })
107
+ export class AppComponent {
108
+ readonly isMobile = down('md');
109
+ readonly isTablet = between('md', 'lg');
110
+ readonly isDesktop = up('lg');
111
+ }
112
+ ```
90
113
 
91
- | Function | Parameters | Returns | Description |
92
- | --------- | ----------------------------------------------------------------- | ----------------- | --------------------------------------------- |
93
- | `up` | `bp: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when viewport width ≥ breakpoint |
94
- | `down` | `bp: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when viewport width < breakpoint |
95
- | `between` | `minBp: string, maxBp: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when viewport width is in range [a, b] |
114
+ > **Tip:** Call the helpers within Angular's [injection context](https://angular.dev/guide/di/dependency-injection-context) (component fields, `inject`, factories) so their lifecycle stays in sync with the framework.
96
115
 
97
- > **Tip:** Wrap these APIs into reusable helpers:
116
+ ## API
98
117
 
99
- ```ts
100
- // viewport-utils.ts
101
- import { Signal } from '@angular/core';
102
- import { up, down, between } from 'ngx-mq';
118
+ Every query helper returns a `Signal<boolean>` and accepts an optional `options` argument
119
+ ([`CreateMediaQueryOptions`](#options)). The `options` column is omitted below for brevity.
120
+
121
+ ### Configuration
122
+
123
+ Register breakpoints once, then refer to them by name. Use a custom map or a preset:
103
124
 
104
- export const isMobile = (): Signal<boolean> => down('md');
105
- export const isTablet = (): Signal<boolean> => between('md', 'lg');
106
- export const isDesktop = (): Signal<boolean> => up('lg');
125
+ ```ts
126
+ import {
127
+ provideBreakpoints,
128
+ provideTailwindBreakpoints,
129
+ provideBootstrapBreakpoints,
130
+ provideMaterialBreakpoints,
131
+ } from 'ngx-mq';
107
132
  ```
108
133
 
109
- ## Common utilities
134
+ | Preset | Breakpoints |
135
+ | ----------- | ------------------------------------------------ |
136
+ | Tailwind | `sm: 640, md: 768, lg: 1024, xl: 1280, 2xl: 1536` |
137
+ | Bootstrap | `sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1400` |
138
+ | Material | `sm: 600, md: 905, lg: 1240, xl: 1440` |
110
139
 
111
- Utils exposing common CSS media features.
140
+ ### Breakpoints
112
141
 
113
- | Function | Parameters | Returns | Description |
114
- | --------------- | ------------------------------------------------------------------------ | ----------------- | ------------------------------------------------------------------------------- |
115
- | `orientation` | `value: 'portrait' \| 'landscape', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the current screen orientation matches the specified value. |
116
- | `colorScheme` | `value: 'light' \| 'dark', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the system color scheme matches the specified value. |
117
- | `displayMode` | `value: DisplayModeOption, options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the current display mode matches the specified value. |
118
- | `reducedMotion` | `options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user has enabled reduced motion. |
119
- | `hover` | `options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user's primary input device supports hover capability. |
120
- | `anyHover` | `options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when any available input device supports hover capability. |
121
- | `pointer` | `value: 'fine' \| 'coarse' \| 'none', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user's primary input device matches the specified pointer type. |
122
- | `anyPointer` | `value: 'fine' \| 'coarse' \| 'none', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when any available input device matches the specified pointer type. |
123
- | `colorGamut` | `value: 'srgb' \| 'p3' \| 'rec2020', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user's display supports the specified color gamut. |
142
+ | Helper | Arguments | `true` when |
143
+ | --------- | ---------------- | ------------------------------------ |
144
+ | `up` | `bp` | viewport width `>=` `bp` |
145
+ | `down` | `bp` | viewport width `<` `bp` |
146
+ | `between` | `minBp`, `maxBp` | viewport width is in `[minBp, maxBp)` |
124
147
 
125
- ---
148
+ > **Note:** `down` and `between` upper bounds are **exclusive**: a small epsilon is subtracted
149
+ > from the max so adjacent ranges (e.g. `down('md')` and `up('md')`) never overlap. Tune it with
150
+ > [`provideBreakpointEpsilon`](#providers).
126
151
 
127
- ## Generic Media Query API
152
+ ### Media features
128
153
 
129
- Works with any valid [CSS media query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries) and returns a `Signal<boolean>` which automatically updates when the query result changes.
154
+ | Helper | Arguments | `true` when |
155
+ | --------------- | ------------------------------ | ------------------------------------ |
156
+ | `orientation` | `'portrait' \| 'landscape'` | the screen orientation matches |
157
+ | `colorScheme` | `'light' \| 'dark'` | the system color scheme matches |
158
+ | `displayMode` | `DisplayModeOption` | the display mode matches (PWA detection) |
159
+ | `reducedMotion` | none | the user prefers reduced motion |
160
+ | `hover` | none | the primary pointer can hover |
161
+ | `anyHover` | none | any available pointer can hover |
162
+ | `pointer` | `'fine' \| 'coarse' \| 'none'` | the primary pointer matches |
163
+ | `anyPointer` | `'fine' \| 'coarse' \| 'none'` | any available pointer matches |
164
+ | `colorGamut` | `'srgb' \| 'p3' \| 'rec2020'` | the display covers the gamut |
130
165
 
131
- | Function | Parameters | Returns | Description |
132
- | ------------------ | -------------------------------------------------- | ----------------- | --------------------------------------------------------- |
133
- | `matchMediaSignal` | `query: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | Provides a signal representing the state of a media query |
166
+ ### Custom queries
134
167
 
135
- > **Tip:** Use this API for media queries that are not part of your breakpoint map.
168
+ For anything without a dedicated helper, pass a raw CSS media query:
136
169
 
137
170
  ```ts
138
- import { Signal } from '@angular/core';
139
171
  import { matchMediaSignal } from 'ngx-mq';
140
172
 
141
- // Example: track orientation changes
142
- export const isLandscape = (): Signal<boolean> => matchMediaSignal('(orientation: landscape)');
173
+ readonly isRetina = matchMediaSignal('(min-resolution: 2dppx)');
143
174
  ```
144
175
 
145
- ## Providers
176
+ ### Composition
146
177
 
147
- These functions return standard Angular `Provider` instances that can be injected at any level of the application hierarchy.
178
+ Combine boolean signals into derived ones. Combinators work at the signal level, so the underlying
179
+ listeners stay shared and are still cleaned up automatically.
148
180
 
149
- | Provider | Parameters | Description |
150
- | ------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
151
- | `provideBreakpoints()` | `bps: MqBreakpoints` | Registers a custom set of breakpoints. |
152
- | `provideTailwindBreakpoints()` | none | Registers the default Tailwind CSS breakpoints. |
153
- | `provideBootstrapBreakpoints()` | none | Registers the default Bootstrap breakpoints. |
154
- | `provideMaterialBreakpoints()` | none | Registers the default Material 2 breakpoints. |
155
- | `provideBreakpointEpsilon()` | `epsilon: number` | Sets the epsilon threshold used when comparing breakpoint values. |
156
- | `provideSsrValue()` | `value: boolean` | Defines the static signal value used during SSR, since media queries are not available on the server. Defaults to `false`. |
181
+ | Helper | Arguments | `true` when |
182
+ | ------ | ---------------------------------- | ------------------------------------------ |
183
+ | `and` | `...conditions: Signal<boolean>[]` | every condition is `true` (empty: `true`) |
184
+ | `or` | `...conditions: Signal<boolean>[]` | any condition is `true` (empty: `false`) |
185
+ | `not` | `condition: Signal<boolean>` | the condition is `false` |
157
186
 
158
- > 💡 To register these as environment providers, wrap them with [`makeEnvironmentProviders()`](https://angular.dev/api/core/makeEnvironmentProviders).
187
+ ```ts
188
+ import { and, or, not, up, down, hover, orientation, reducedMotion } from 'ngx-mq';
159
189
 
160
- ## Types
190
+ // Large screen, in landscape, with a hover-capable pointer
191
+ readonly isLandscapeDesktop = and(up('lg'), orientation('landscape'), hover());
161
192
 
162
- ```ts
163
- export type MqBreakpoints = Record<string, number>;
193
+ // Small screens OR a reduced-motion preference
194
+ readonly prefersSimpleUi = or(down('md'), reducedMotion());
164
195
 
165
- export interface CreateMediaQueryOptions {
166
- /**
167
- * Static signal value used during SSR.
168
- */
169
- ssrValue?: boolean;
196
+ // Devices without hover (touch-like): `hover()` has no direct inverse helper
197
+ readonly isTouchLike = not(hover());
198
+ ```
199
+
200
+ ### Providers
201
+
202
+ Each returns a standard Angular `Provider` you can register at any injector level.
203
+
204
+ | Provider | Argument | Description |
205
+ | ------------------------------- | -------------------- | ----------------------------------------------------- |
206
+ | `provideBreakpoints` | `bps: MqBreakpoints` | Registers a custom breakpoint map. |
207
+ | `provideTailwindBreakpoints` | none | Registers the Tailwind preset. |
208
+ | `provideBootstrapBreakpoints` | none | Registers the Bootstrap preset. |
209
+ | `provideMaterialBreakpoints` | none | Registers the Material 2 preset. |
210
+ | `provideBreakpointEpsilon` | `epsilon: number` | Sets the exclusive-bound epsilon (default `0.02`). |
211
+ | `provideSsrValue` | `value: boolean` | Sets the value signals report during SSR (default `false`). |
170
212
 
171
- /**
172
- * A debug name for the signal. Used in Angular DevTools to identify the signal.
173
- */
213
+ > **Tip:** To register these as environment providers, wrap them with
214
+ > [`makeEnvironmentProviders`](https://angular.dev/api/core/makeEnvironmentProviders).
215
+
216
+ ### Options
217
+
218
+ ```ts
219
+ interface CreateMediaQueryOptions {
220
+ /** Value the signal reports during SSR. Overrides the app-wide `provideSsrValue`. */
221
+ ssrValue?: boolean;
222
+ /** Debug name shown for the signal in Angular DevTools. */
174
223
  debugName?: string;
175
224
  }
225
+ ```
176
226
 
177
- export type DisplayModeOption =
227
+ ### Types
228
+
229
+ ```ts
230
+ type MqBreakpoints = Record<string, number>;
231
+
232
+ type DisplayModeOption =
178
233
  | 'browser'
179
234
  | 'fullscreen'
180
235
  | 'standalone'
@@ -183,6 +238,33 @@ export type DisplayModeOption =
183
238
  | 'picture-in-picture';
184
239
  ```
185
240
 
241
+ ## Server-side rendering
242
+
243
+ `matchMedia` does not exist on the server, so during SSR every signal returns a static value and
244
+ no listeners are created. Set the default with `provideSsrValue`, or override it per call:
245
+
246
+ ```ts
247
+ provideSsrValue(false); // app-wide default
248
+ up('lg', { ssrValue: true }); // per-call override
249
+ ```
250
+
251
+ Once the app hydrates in the browser, each signal switches to the live query result.
252
+
253
+ ## How it works
254
+
255
+ `ngx-mq` keeps a single `MediaQueryList` and writable signal per unique query in a global registry
256
+ (Multiton + Flyweight). Callers share that signal, and a reference count tied to `DestroyRef`
257
+ removes the listener and registry entry once the last consumer is destroyed. No manual cleanup, no
258
+ duplicate listeners.
259
+
260
+ ## Contributing
261
+
262
+ Contributions are welcome. See [CONTRIBUTING.md](https://github.com/martsinlabs/ngx-mq/blob/main/CONTRIBUTING.md).
263
+
264
+ ## License
265
+
266
+ [MIT](https://github.com/martsinlabs/ngx-mq/blob/main/LICENSE) © Martsin Labs
267
+
186
268
  ## Sponsors
187
269
 
188
270
  <table>
@@ -192,7 +274,7 @@ export type DisplayModeOption =
192
274
  <img
193
275
  src="https://raw.githubusercontent.com/getsentry/sentry/7b65f0f23d7eb5ccc035b12776bf5d8f3d9f8965/static/images/logo-sentry.svg"
194
276
  width="120"
195
- alt="Sentry Logo" />
277
+ alt="Sentry" />
196
278
  </p>
197
279
  <p>
198
280
  <a href="https://sentry.io" target="_blank"><strong>Sentry</strong></a>
@@ -202,7 +284,3 @@ export type DisplayModeOption =
202
284
  </td>
203
285
  </tr>
204
286
  </table>
205
-
206
- ## Contributing
207
-
208
- [CONTRIBUTING.md](https://github.com/martsinlabs/ngx-mq/blob/main/CONTRIBUTING.md)