ngx-mq 2.11.3 → 3.1.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,190 +1,241 @@
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="130" alt="ngx-mq logo" />
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>
6
+ <br />
8
7
 
9
- <h5 align="center">
8
+ <p align="center">
10
9
  <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" />
10
+ <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
11
  </a>
13
12
  <a href="https://codecov.io/gh/martsinlabs/ngx-mq">
14
13
  <img src="https://codecov.io/gh/martsinlabs/ngx-mq/branch/main/graph/badge.svg" alt="coverage" />
15
14
  </a>
16
- <br>
17
15
  <a href="https://www.npmjs.com/package/ngx-mq">
18
16
  <img src="https://img.shields.io/npm/v/ngx-mq.svg?color=007ec6" alt="npm version" />
19
17
  </a>
20
18
  <a href="https://www.npmjs.com/package/ngx-mq">
21
19
  <img src="https://img.shields.io/npm/dm/ngx-mq.svg?color=44cc11" alt="npm downloads" />
22
20
  </a>
21
+ <a href="https://bundlephobia.com/package/ngx-mq">
22
+ <img src="https://img.shields.io/bundlephobia/minzip/ngx-mq.svg?color=44cc11&label=minzip" alt="minzipped size" />
23
+ </a>
23
24
  <a href="https://opensource.org/license/MIT">
24
25
  <img src="https://img.shields.io/npm/l/ngx-mq.svg?color=44cc11" alt="license" />
25
26
  </a>
26
- </h5>
27
+ </p>
27
28
 
28
- <br>
29
+ <p align="center">
30
+ <a href="https://martsinlabs.github.io/ngx-mq"><b>Documentation</b></a>
31
+ &nbsp;&nbsp;·&nbsp;&nbsp;
32
+ <a href="https://stackblitz.com/github/martsinlabs/ngx-mq-demo/tree/demo/v3"><b>Live demo</b></a>
33
+ &nbsp;&nbsp;·&nbsp;&nbsp;
34
+ <a href="#why-ngx-mq"><b>Why ngx-mq?</b></a>
35
+ </p>
29
36
 
30
- ## Features
37
+ ---
31
38
 
32
- - Lightweight
33
- - SSR-safe
34
- - Auto-cleanup
35
- - Angular 19–21 (use `ngx-mq@1` for Angular 16–18)
36
- - Well-tested
39
+ ## Overview
37
40
 
38
- ## Introduction
41
+ A responsive value is just a signal: read it in the template, compose it, and never wire up cleanup.
39
42
 
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.
43
+ ```ts
44
+ import { Component } from '@angular/core';
45
+ import { up } from 'ngx-mq';
46
+
47
+ @Component({
48
+ selector: 'app-root',
49
+ template: `
50
+ @if (isDesktop()) {
51
+ <app-sidebar />
52
+ }
53
+ `,
54
+ })
55
+ export class AppComponent {
56
+ readonly isDesktop = up('lg');
57
+ }
58
+ ```
41
59
 
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.
60
+ - **Signal-native** so it works anywhere signals do, zoneless apps included.
61
+ - **Zero boilerplate**: no subscriptions, no `unsubscribe`, cleanup is automatic.
62
+ - **SSR-safe** with a value you control on the server.
63
+ - **Batteries included**: Tailwind, Bootstrap and Material presets, plus `and` / `or` / `not`.
64
+ - **Tiny**: ~1.9 kB gzipped, and no RxJS.
43
65
 
44
- ## Live Demo
66
+ ## Install
45
67
 
46
- Try it on [StackBlitz](https://stackblitz.com/github/martsinlabs/ngx-mq-demo/tree/demo/v2?file=src%2Fapp%2Fapp.component.ts)
68
+ ```bash
69
+ npm i ngx-mq # Angular 20-22
70
+ ```
47
71
 
48
- ## Installation
72
+ <sub>Angular 19 -> <code>ngx-mq@2</code> &nbsp;·&nbsp; Angular 16-18 -> <code>ngx-mq@1</code></sub>
49
73
 
50
- Choose the package version that matches your Angular setup:
74
+ Then register your breakpoints once, at bootstrap:
51
75
 
52
- ```bash
53
- # For Angular 16–18
54
- npm install ngx-mq@1
76
+ ```ts
77
+ import { provideBreakpoints } from 'ngx-mq';
55
78
 
56
- # For Angular 19–21
57
- npm install ngx-mq@2
79
+ bootstrapApplication(AppComponent, {
80
+ providers: [provideBreakpoints({ sm: 640, md: 768, lg: 1024 })],
81
+ // or a preset: provideTailwindBreakpoints() / provideBootstrapBreakpoints() / provideMaterialBreakpoints()
82
+ });
58
83
  ```
59
84
 
60
- ## Breakpoint API
85
+ > Call the helpers inside an [injection context](https://angular.dev/guide/di/dependency-injection-context): a component field, a constructor, or a DI factory.
61
86
 
62
- ### Configuration
87
+ ## Examples
63
88
 
64
- Create a custom breakpoint map or use one of the built-in presets (e.g. `provideTailwindBreakpoints()`).
89
+ #### Show different layouts per screen size
65
90
 
66
91
  ```ts
67
- import { bootstrapApplication } from '@angular/platform-browser';
68
- import { AppComponent } from './app/app.component';
69
- import { provideBreakpoints } from 'ngx-mq';
92
+ readonly isMobile = down('md');
93
+ readonly isTablet = between('md', 'lg');
94
+ readonly isDesktop = up('lg');
95
+ ```
70
96
 
71
- 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
- ],
80
- });
97
+ #### Follow the system dark mode
98
+
99
+ ```ts
100
+ readonly prefersDark = colorScheme('dark');
81
101
  ```
82
102
 
83
- **Available presets**
103
+ #### Drop hover styles on touch devices
84
104
 
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`
105
+ ```ts
106
+ // `hover()` has no direct inverse, so compose it
107
+ readonly isTouchLike = not(hover());
108
+ ```
88
109
 
89
- ### BP-related utilities
110
+ #### Combine any conditions
90
111
 
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) |
112
+ ```ts
113
+ // Large screen, in landscape, with a hover-capable pointer
114
+ readonly isLandscapeDesktop = and(up('lg'), orientation('landscape'), hover());
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
+ // Small screens OR a reduced-motion preference
117
+ readonly prefersSimpleUi = or(down('md'), reducedMotion());
118
+ ```
98
119
 
99
- > **Tip:** Wrap these APIs into reusable helpers:
120
+ #### Respect reduced motion
100
121
 
101
122
  ```ts
102
- // viewport-utils.ts
103
- import { Signal } from '@angular/core';
104
- import { up, down, between } from 'ngx-mq';
123
+ readonly reduceMotion = reducedMotion();
124
+ ```
105
125
 
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');
126
+ #### Anything else, with a raw query
127
+
128
+ ```ts
129
+ readonly isRetina = matchMediaSignal('(min-resolution: 2dppx)');
109
130
  ```
110
131
 
111
- ## Common utilities
132
+ ## Why ngx-mq?
112
133
 
113
- Utils exposing common CSS media features.
134
+ Angular's CDK ships [`BreakpointObserver`](https://material.angular.io/cdk/layout/overview), which works well but is built around RxJS and raw query strings. `ngx-mq` is built for the signals era: read a value in the template, subscribe to nothing, clean up automatically.
114
135
 
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. |
136
+ | | `ngx-mq` | CDK `BreakpointObserver` |
137
+ | --------------------- | ------------------------------------------- | ------------------------------------- |
138
+ | Reactivity | `Signal<boolean>` | `Observable<BreakpointState>` |
139
+ | Cleanup | Automatic via `DestroyRef` | Manual (`takeUntilDestroyed`) |
140
+ | Named breakpoints | Tailwind / Bootstrap / Material or your own | Material breakpoints or raw strings |
141
+ | Media-feature helpers | `colorScheme`, `hover`, `pointer`, ... | Raw query strings |
142
+ | Composition | `and` / `or` / `not` | RxJS operators |
143
+ | SSR | Configurable static value | Handle it yourself |
144
+ | Footprint | ~1.9 kB standalone | Part of `@angular/cdk` |
126
145
 
127
- ---
146
+ ## Documentation
128
147
 
129
- ## Generic Media Query API
148
+ Spin it up in seconds on [StackBlitz](https://stackblitz.com/github/martsinlabs/ngx-mq-demo/tree/demo/v3), no setup required.
130
149
 
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.
150
+ Full API reference, guides and recipes live at **[martsinlabs.github.io/ngx-mq](https://martsinlabs.github.io/ngx-mq)**.
132
151
 
133
- | Function | Parameters | Returns | Description |
134
- | ------------------ | -------------------------------------------------- | ----------------- | --------------------------------------------------------- |
135
- | `matchMediaSignal` | `query: string, options?: CreateMediaQueryOptions` | `Signal<boolean>` | Provides a signal representing the state of a media query |
152
+ <details>
153
+ <summary><b>API reference</b> (quick view)</summary>
136
154
 
137
- > **Tip:** Use this API for media queries that are not part of your breakpoint map.
155
+ <br>
138
156
 
139
- ```ts
140
- import { Signal } from '@angular/core';
141
- import { matchMediaSignal } from 'ngx-mq';
157
+ Every query helper returns a `Signal<boolean>` and accepts an optional `options` argument
158
+ ([`CreateMediaQueryOptions`](#options-and-types)).
142
159
 
143
- // Example: track orientation changes
144
- export const isLandscape = (): Signal<boolean> => matchMediaSignal('(orientation: landscape)');
145
- ```
160
+ **Breakpoints**
161
+
162
+ | Helper | Arguments | `true` when |
163
+ | --------- | ---------------- | ------------------------------------ |
164
+ | `up` | `bp` | viewport width `>=` `bp` |
165
+ | `down` | `bp` | viewport width `<` `bp` (exclusive) |
166
+ | `between` | `minBp`, `maxBp` | viewport width is in `[minBp, maxBp)` |
167
+
168
+ `down` and `between` upper bounds are exclusive: a small epsilon (default `0.02`, set via `provideBreakpointEpsilon`) is subtracted from the max so adjacent ranges never overlap.
169
+
170
+ **Media features**
171
+
172
+ | Helper | Arguments | `true` when |
173
+ | --------------- | ------------------------------ | ---------------------------------------- |
174
+ | `orientation` | `'portrait' \| 'landscape'` | the screen orientation matches |
175
+ | `colorScheme` | `'light' \| 'dark'` | the system color scheme matches |
176
+ | `displayMode` | `DisplayModeOption` | the display mode matches (PWA detection) |
177
+ | `reducedMotion` | none | the user prefers reduced motion |
178
+ | `prefersContrast` | `'more' \| 'less' \| 'no-preference' \| 'custom'` | the user's contrast preference matches |
179
+ | `hover` | none | the primary pointer can hover |
180
+ | `anyHover` | none | any available pointer can hover |
181
+ | `pointer` | `'fine' \| 'coarse' \| 'none'` | the primary pointer matches |
182
+ | `anyPointer` | `'fine' \| 'coarse' \| 'none'` | any available pointer matches |
183
+ | `colorGamut` | `'srgb' \| 'p3' \| 'rec2020'` | the display covers the gamut |
184
+
185
+ **Composition**
146
186
 
147
- ## Providers
187
+ | Helper | Arguments | `true` when |
188
+ | ------ | ---------------------------------- | ----------------------------------------- |
189
+ | `and` | `...conditions: Signal<boolean>[]` | every condition is `true` (empty: `true`) |
190
+ | `or` | `...conditions: Signal<boolean>[]` | any condition is `true` (empty: `false`) |
191
+ | `not` | `condition: Signal<boolean>` | the condition is `false` |
148
192
 
149
- These functions return standard Angular `Provider` instances that can be injected at any level of the application hierarchy.
193
+ **Custom queries**
150
194
 
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`. |
195
+ | Helper | Arguments | Description |
196
+ | ------------------ | --------------- | -------------------------------------------- |
197
+ | `matchMediaSignal` | `query: string` | A signal for any raw CSS media query |
159
198
 
160
- > 💡 To register these as environment providers, wrap them with [`makeEnvironmentProviders()`](https://angular.dev/api/core/makeEnvironmentProviders).
199
+ **Providers**
161
200
 
162
- ## Types
201
+ | Provider | Argument | Description |
202
+ | ----------------------------- | -------------------- | ------------------------------------------------------- |
203
+ | `provideBreakpoints` | `bps: MqBreakpoints` | Registers a custom breakpoint map |
204
+ | `provideTailwindBreakpoints` | none | Registers the Tailwind preset |
205
+ | `provideBootstrapBreakpoints` | none | Registers the Bootstrap preset |
206
+ | `provideMaterialBreakpoints` | none | Registers the Material 2 preset |
207
+ | `provideBreakpointEpsilon` | `epsilon: number` | Sets the exclusive-bound epsilon (default `0.02`) |
208
+ | `provideSsrValue` | `value: boolean` | Sets the value signals report during SSR (default `false`) |
209
+
210
+ **Options and types**
163
211
 
164
212
  ```ts
165
- export type MqBreakpoints = Record<string, number>;
166
-
167
- export interface CreateMediaQueryOptions {
168
- /**
169
- * Static signal value used during SSR.
170
- */
171
- ssrValue?: boolean;
172
-
173
- /**
174
- * A debug name for the signal. Used in Angular DevTools to identify the signal.
175
- */
176
- debugName?: string;
213
+ interface CreateMediaQueryOptions {
214
+ ssrValue?: boolean; // value reported during SSR; overrides provideSsrValue
215
+ debugName?: string; // shown for the signal in Angular DevTools
177
216
  }
178
217
 
179
- export type DisplayModeOption =
180
- | 'browser'
181
- | 'fullscreen'
182
- | 'standalone'
183
- | 'minimal-ui'
184
- | 'window-controls-overlay'
185
- | 'picture-in-picture';
218
+ type MqBreakpoints = Record<string, number>;
219
+
220
+ type DisplayModeOption =
221
+ | 'browser' | 'fullscreen' | 'standalone'
222
+ | 'minimal-ui' | 'window-controls-overlay' | 'picture-in-picture';
186
223
  ```
187
224
 
225
+ </details>
226
+
227
+ ## Server-side rendering
228
+
229
+ `matchMedia` does not exist on the server, so each signal returns a static value during SSR and switches to the live result after hydration. Set the default with `provideSsrValue(true)`, or override per call with `up('lg', { ssrValue: true })`.
230
+
231
+ ## Contributing
232
+
233
+ Contributions are welcome. See [CONTRIBUTING.md](https://github.com/martsinlabs/ngx-mq/blob/main/CONTRIBUTING.md) and [ARCHITECTURE.md](https://github.com/martsinlabs/ngx-mq/blob/main/ARCHITECTURE.md).
234
+
235
+ ## License
236
+
237
+ [MIT](https://github.com/martsinlabs/ngx-mq/blob/main/LICENSE) © Martsin Labs
238
+
188
239
  ## Sponsors
189
240
 
190
241
  <table>
@@ -194,7 +245,7 @@ export type DisplayModeOption =
194
245
  <img
195
246
  src="https://raw.githubusercontent.com/getsentry/sentry/7b65f0f23d7eb5ccc035b12776bf5d8f3d9f8965/static/images/logo-sentry.svg"
196
247
  width="120"
197
- alt="Sentry Logo" />
248
+ alt="Sentry" />
198
249
  </p>
199
250
  <p>
200
251
  <a href="https://sentry.io" target="_blank"><strong>Sentry</strong></a>
@@ -204,7 +255,3 @@ export type DisplayModeOption =
204
255
  </td>
205
256
  </tr>
206
257
  </table>
207
-
208
- ## Contributing
209
-
210
- [CONTRIBUTING.md](https://github.com/martsinlabs/ngx-mq/blob/main/CONTRIBUTING.md)