ngx-mq 2.11.3 → 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,182 +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 19–21 (use `ngx-mq@1` for Angular 16–18)
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 [min, max) |
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
- > **Note:** `down` and `between` upper bounds are **exclusive** — epsilon is subtracted from `max` so adjacent ranges (e.g. `down('md')` and `up('md')`) never overlap.
116
+ ## API
98
117
 
99
- > **Tip:** Wrap these APIs into reusable helpers:
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.
100
120
 
101
- ```ts
102
- // viewport-utils.ts
103
- import { Signal } from '@angular/core';
104
- import { up, down, between } from 'ngx-mq';
121
+ ### Configuration
105
122
 
106
- export const isMobile = (): Signal<boolean> => down('md');
107
- export const isTablet = (): Signal<boolean> => between('md', 'lg');
108
- export const isDesktop = (): Signal<boolean> => up('lg');
123
+ Register breakpoints once, then refer to them by name. Use a custom map or a preset:
124
+
125
+ ```ts
126
+ import {
127
+ provideBreakpoints,
128
+ provideTailwindBreakpoints,
129
+ provideBootstrapBreakpoints,
130
+ provideMaterialBreakpoints,
131
+ } from 'ngx-mq';
109
132
  ```
110
133
 
111
- ## 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` |
112
139
 
113
- Utils exposing common CSS media features.
140
+ ### Breakpoints
114
141
 
115
- | Function | Parameters | Returns | Description |
116
- | --------------- | ------------------------------------------------------------------------ | ----------------- | ------------------------------------------------------------------------------- |
117
- | `orientation` | `value: 'portrait' \| 'landscape', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the current screen orientation matches the specified value. |
118
- | `colorScheme` | `value: 'light' \| 'dark', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the system color scheme matches the specified value. |
119
- | `displayMode` | `value: DisplayModeOption, options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the current display mode matches the specified value. |
120
- | `reducedMotion` | `options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user has enabled reduced motion. |
121
- | `hover` | `options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user's primary input device supports hover capability. |
122
- | `anyHover` | `options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when any available input device supports hover capability. |
123
- | `pointer` | `value: 'fine' \| 'coarse' \| 'none', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when the user's primary input device matches the specified pointer type. |
124
- | `anyPointer` | `value: 'fine' \| 'coarse' \| 'none', options?: CreateMediaQueryOptions` | `Signal<boolean>` | `true` when any available input device matches the specified pointer type. |
125
- | `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)` |
126
147
 
127
- ---
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).
128
151
 
129
- ## Generic Media Query API
152
+ ### Media features
130
153
 
131
- 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 |
132
165
 
133
- | Function | Parameters | Returns | Description |
134
- | ------------------ | -------------------------------------------------- | ----------------- | --------------------------------------------------------- |
135
- | `matchMediaSignal` | `query: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | Provides a signal representing the state of a media query |
166
+ ### Custom queries
136
167
 
137
- > **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:
138
169
 
139
170
  ```ts
140
- import { Signal } from '@angular/core';
141
171
  import { matchMediaSignal } from 'ngx-mq';
142
172
 
143
- // Example: track orientation changes
144
- export const isLandscape = (): Signal<boolean> => matchMediaSignal('(orientation: landscape)');
173
+ readonly isRetina = matchMediaSignal('(min-resolution: 2dppx)');
145
174
  ```
146
175
 
147
- ## Providers
176
+ ### Composition
148
177
 
149
- 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.
150
180
 
151
- | Provider | Parameters | Description |
152
- | ------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
153
- | `provideBreakpoints()` | `bps: MqBreakpoints` | Registers a custom set of breakpoints. |
154
- | `provideTailwindBreakpoints()` | none | Registers the default Tailwind CSS breakpoints. |
155
- | `provideBootstrapBreakpoints()` | none | Registers the default Bootstrap breakpoints. |
156
- | `provideMaterialBreakpoints()` | none | Registers the default Material 2 breakpoints. |
157
- | `provideBreakpointEpsilon()` | `epsilon: number` | Sets the epsilon threshold used when comparing breakpoint values. |
158
- | `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` |
159
186
 
160
- > 💡 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';
161
189
 
162
- ## Types
190
+ // Large screen, in landscape, with a hover-capable pointer
191
+ readonly isLandscapeDesktop = and(up('lg'), orientation('landscape'), hover());
163
192
 
164
- ```ts
165
- export type MqBreakpoints = Record<string, number>;
193
+ // Small screens OR a reduced-motion preference
194
+ readonly prefersSimpleUi = or(down('md'), reducedMotion());
166
195
 
167
- export interface CreateMediaQueryOptions {
168
- /**
169
- * Static signal value used during SSR.
170
- */
171
- ssrValue?: boolean;
196
+ // Devices without hover (touch-like): `hover()` has no direct inverse helper
197
+ readonly isTouchLike = not(hover());
198
+ ```
172
199
 
173
- /**
174
- * A debug name for the signal. Used in Angular DevTools to identify the signal.
175
- */
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`). |
212
+
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. */
176
223
  debugName?: string;
177
224
  }
225
+ ```
226
+
227
+ ### Types
228
+
229
+ ```ts
230
+ type MqBreakpoints = Record<string, number>;
178
231
 
179
- export type DisplayModeOption =
232
+ type DisplayModeOption =
180
233
  | 'browser'
181
234
  | 'fullscreen'
182
235
  | 'standalone'
@@ -185,6 +238,33 @@ export type DisplayModeOption =
185
238
  | 'picture-in-picture';
186
239
  ```
187
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
+
188
268
  ## Sponsors
189
269
 
190
270
  <table>
@@ -194,7 +274,7 @@ export type DisplayModeOption =
194
274
  <img
195
275
  src="https://raw.githubusercontent.com/getsentry/sentry/7b65f0f23d7eb5ccc035b12776bf5d8f3d9f8965/static/images/logo-sentry.svg"
196
276
  width="120"
197
- alt="Sentry Logo" />
277
+ alt="Sentry" />
198
278
  </p>
199
279
  <p>
200
280
  <a href="https://sentry.io" target="_blank"><strong>Sentry</strong></a>
@@ -204,7 +284,3 @@ export type DisplayModeOption =
204
284
  </td>
205
285
  </tr>
206
286
  </table>
207
-
208
- ## Contributing
209
-
210
- [CONTRIBUTING.md](https://github.com/martsinlabs/ngx-mq/blob/main/CONTRIBUTING.md)