@nrg-ui/ember-media 0.1.1

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.md ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # @nrg-ui/ember-media
2
+
3
+ An Ember addon for safe media query handling.
4
+
5
+ ## Compatibility
6
+
7
+ - Ember.js v5.12 or above
8
+ - Embroider or ember-auto-import v2
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ ember install @nrg-ui/ember-media
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### Breakpoints
19
+
20
+ There are 6 breakpoints available, with the following media queries:
21
+
22
+ | Breakpoint | Media Query |
23
+ | ---------- | --------------------------------------------- |
24
+ | xsmall | `(min-width: 0px) and (max-width: 575px)` |
25
+ | small | `(min-width: 576px) and (max-width: 767px)` |
26
+ | medium | `(min-width: 768px) and (max-width: 991px)` |
27
+ | large | `(min-width: 992px) and (max-width: 1199px)` |
28
+ | xlarge | `(min-width: 1200px) and (max-width: 1399px)` |
29
+ | xxlarge | `(min-width: 1400px)` |
30
+
31
+ There is a corresponding property on the media service for each breakpoint, e.g. `isSmall`, `isMedium`, etc.
32
+
33
+ The queries can be adjusted via the Embroider build config, if needed:
34
+
35
+ ```js
36
+ // ember-cli-build.js
37
+ module.exports = function (defaults) {
38
+ let app = new EmberApp(defaults, {
39
+ '@embroider/macros': {
40
+ '@nrg-ui/ember-media': {
41
+ breakpoints: {
42
+ small: '(min-width: 600px) and (max-width: 799px)',
43
+ // other breakpoints...
44
+ },
45
+ },
46
+ },
47
+ });
48
+
49
+ return app;
50
+ };
51
+ ```
52
+
53
+ ### Events
54
+
55
+ The media service emits a `change` event whenever the matched media queries change. You can listen for this event to react to changes in screen size.
56
+
57
+ ```ts
58
+ export default class MyComponent extends Component {
59
+ @service
60
+ declare media: MediaService;
61
+
62
+ constructor(owner: Owner, args: unknown) {
63
+ super(owner, args);
64
+
65
+ this.media.on('change', () => {
66
+ console.log('Matched media queries:', Array.from(this.media.matches));
67
+ });
68
+ }
69
+ }
70
+ ```
71
+
72
+ ### Testing
73
+
74
+ When testing components or services that depend on the media service, you can use the provided test helpers to simulate different screen sizes.
75
+
76
+ ```ts
77
+ import { setupRenderingTest } from 'ember-qunit';
78
+ import { module, test } from 'qunit';
79
+ import { setBreakpoint } from '@nrg-ui/ember-media/test-support';
80
+
81
+ module('Integration | Component | my-component', function (hooks) {
82
+ setupRenderingTest(hooks);
83
+
84
+ test('my test', async function (assert) {
85
+ const service = this.owner.lookup('service:media');
86
+
87
+ await setBreakpoint('small');
88
+ // ...
89
+ });
90
+ });
91
+ ```
92
+
93
+ ### Example
94
+
95
+ Inject the media service into your component, controller, or route:
96
+
97
+ ```ts
98
+ import Component from '@glimmer/component';
99
+ import { service } from '@ember/service';
100
+
101
+ import type { Owner } from '@ember/owner';
102
+ import type MediaService from '@nrg-ui/ember-media/services/media';
103
+
104
+ export default class MyComponent extends Component {
105
+ @service
106
+ declare media: MediaService;
107
+
108
+ constructor(owner: unknown, args: unknown) {
109
+ super(owner, args);
110
+
111
+ this.media.on('change', this.handleMediaChange);
112
+ }
113
+
114
+ onClick = () => {
115
+ if (this.media.isLarge) {
116
+ // Do something for large screens
117
+ }
118
+ };
119
+
120
+ handleMediaChange = () => {
121
+ console.log('Matched media queries:', Array.from(this.media.matches));
122
+ };
123
+ }
124
+ ```
125
+
126
+ ## Contributing
127
+
128
+ See the [Contributing](CONTRIBUTING.md) guide for details.
129
+
130
+ ## License
131
+
132
+ This project is licensed under the [MIT License](LICENSE.md).
package/addon-main.cjs ADDED
@@ -0,0 +1,5 @@
1
+ /* eslint-disable @typescript-eslint/no-require-imports */
2
+ 'use strict';
3
+
4
+ const { addonV1Shim } = require('@embroider/addon-shim');
5
+ module.exports = addonV1Shim(__dirname);
@@ -0,0 +1,4 @@
1
+ import type MediaService from './services/media.ts';
2
+ export type { MediaService };
3
+ export { defaultBreakpoints } from './services/media.ts';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,YAAY,MAAM,qBAAqB,CAAC;AAEpD,YAAY,EAAE,YAAY,EAAE,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,50 @@
1
+ import Service from '@ember/service';
2
+ import { TrackedSet } from 'tracked-built-ins';
3
+ import type Owner from '@ember/owner';
4
+ type Fn = () => unknown | Promise<unknown>;
5
+ type Callbacks = {
6
+ change: Set<Fn>;
7
+ };
8
+ export declare const defaultBreakpoints: Readonly<{
9
+ xsmall: "(min-width: 0px) and (max-width: 575px)";
10
+ small: "(min-width: 576px) and (max-width: 767px)";
11
+ medium: "(min-width: 768px) and (max-width: 991px)";
12
+ large: "(min-width: 992px) and (max-width: 1199px)";
13
+ xlarge: "(min-width: 1200px) and (max-width: 1399px)";
14
+ xxlarge: "(min-width: 1400px)";
15
+ }>;
16
+ export default class Media extends Service {
17
+ #private;
18
+ _mockedBreakpoint: string;
19
+ _matches: TrackedSet<string>;
20
+ mocked: boolean;
21
+ callbacks: Callbacks;
22
+ breakpoints: {
23
+ xsmall: string;
24
+ small: string;
25
+ medium: string;
26
+ large: string;
27
+ xlarge: string;
28
+ xxlarge: string;
29
+ };
30
+ constructor(owner: Owner);
31
+ get matches(): Set<string>;
32
+ private set matches(value);
33
+ get isXSmall(): boolean;
34
+ get isSmall(): boolean;
35
+ get isMedium(): boolean;
36
+ get isLarge(): boolean;
37
+ get isXLarge(): boolean;
38
+ get isXXLarge(): boolean;
39
+ on(name: keyof Callbacks, callback: Fn): void;
40
+ off(name: keyof Callbacks, callback: Fn): void;
41
+ trigger(name: keyof Callbacks): void;
42
+ match(name: string, query: string): void;
43
+ }
44
+ declare module '@ember/service' {
45
+ interface Registry {
46
+ media: Media;
47
+ }
48
+ }
49
+ export {};
50
+ //# sourceMappingURL=media.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/services/media.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,MAAM,gBAAgB,CAAC;AASrC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,KAAK,EAAE,GAAG,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAC3C,KAAK,SAAS,GAAG;IACf,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;CACjB,CAAC;AAMF,eAAO,MAAM,kBAAkB;;;;;;;EAO7B,CAAC;AAEH,MAAM,CAAC,OAAO,OAAO,KAAM,SAAQ,OAAO;;IACxC,iBAAiB,SAAa;IAG9B,QAAQ,qBAA4B;IAGpC,MAAM,UAAmE;IAEzE,SAAS,EAAE,SAAS,CAElB;IAEF,WAAW;;;;;;;MAGT;gBAEU,KAAK,EAAE,KAAK;IAQxB,IAAI,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,CASzB;IAED,OAAO,KAAK,OAAO,QAElB;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAUD,EAAE,CAAC,IAAI,EAAE,MAAM,SAAS,EAAE,QAAQ,EAAE,EAAE;IAMtC,GAAG,CAAC,IAAI,EAAE,MAAM,SAAS,EAAE,QAAQ,EAAE,EAAE;IAMvC,OAAO,CAAC,IAAI,EAAE,MAAM,SAAS;IAW7B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CA+BlC;AAED,OAAO,QAAQ,gBAAgB,CAAC;IAC9B,UAAU,QAAQ;QAChB,KAAK,EAAE,KAAK,CAAC;KACd;CACF"}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=template-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-registry.d.ts","sourceRoot":"","sources":["../src/template-registry.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export declare function setBreakpoint(breakpoint: string): Promise<void>;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-support/index.ts"],"names":[],"mappings":"AAKA,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,iBAkBrD"}
@@ -0,0 +1 @@
1
+ export { default } from "@nrg-ui/ember-media/services/media";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { defaultBreakpoints } from './services/media.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,122 @@
1
+ import { assert } from '@ember/debug';
2
+ import Service from '@ember/service';
3
+ import { macroCondition, isTesting, isDevelopingApp, getOwnConfig } from '@embroider/macros';
4
+ import { tracked } from '@glimmer/tracking';
5
+ import { runTask } from 'ember-lifeline';
6
+ import { TrackedSet } from 'tracked-built-ins';
7
+ import { g, i } from 'decorator-transforms/runtime-esm';
8
+
9
+ const defaultBreakpoints = Object.freeze({
10
+ xsmall: '(min-width: 0px) and (max-width: 575px)',
11
+ small: '(min-width: 576px) and (max-width: 767px)',
12
+ medium: '(min-width: 768px) and (max-width: 991px)',
13
+ large: '(min-width: 992px) and (max-width: 1199px)',
14
+ xlarge: '(min-width: 1200px) and (max-width: 1399px)',
15
+ xxlarge: '(min-width: 1400px)'
16
+ });
17
+ class Media extends Service {
18
+ _mockedBreakpoint = 'desktop';
19
+ static {
20
+ g(this.prototype, "_matches", [tracked], function () {
21
+ return new TrackedSet();
22
+ });
23
+ }
24
+ #_matches = (i(this, "_matches"), void 0);
25
+ static {
26
+ g(this.prototype, "mocked", [tracked], function () {
27
+ return macroCondition(isTesting() || isDevelopingApp()) ? true : false;
28
+ });
29
+ }
30
+ #mocked = (i(this, "mocked"), void 0);
31
+ callbacks = {
32
+ change: new Set()
33
+ };
34
+ breakpoints = {
35
+ ...defaultBreakpoints,
36
+ ...getOwnConfig()?.breakpoints
37
+ };
38
+ constructor(owner) {
39
+ super(owner);
40
+ for (const [name, query] of Object.entries(this.breakpoints)) {
41
+ this.match(name, query);
42
+ }
43
+ }
44
+ get matches() {
45
+ if ((macroCondition(isTesting() || isDevelopingApp()) ? true : false) && this.mocked) {
46
+ return new TrackedSet([this._mockedBreakpoint]);
47
+ }
48
+ return this._matches;
49
+ }
50
+ set matches(value) {
51
+ this._matches = new TrackedSet(value);
52
+ }
53
+ get isXSmall() {
54
+ return this.matches.has('xsmall');
55
+ }
56
+ get isSmall() {
57
+ return this.matches.has('small');
58
+ }
59
+ get isMedium() {
60
+ return this.matches.has('medium');
61
+ }
62
+ get isLarge() {
63
+ return this.matches.has('large');
64
+ }
65
+ get isXLarge() {
66
+ return this.matches.has('xlarge');
67
+ }
68
+ get isXXLarge() {
69
+ return this.matches.has('xxlarge');
70
+ }
71
+ #getCallbackList(name) {
72
+ const callbackList = this.callbacks[name];
73
+ assert(`Callback '${name}' is not valid`, callbackList !== undefined);
74
+ return callbackList;
75
+ }
76
+ on(name, callback) {
77
+ const callbackList = this.#getCallbackList(name);
78
+ callbackList.add(callback);
79
+ }
80
+ off(name, callback) {
81
+ const callbackList = this.#getCallbackList(name);
82
+ callbackList.delete(callback);
83
+ }
84
+ trigger(name) {
85
+ const callbackList = this.#getCallbackList(name);
86
+ for (const callback of callbackList) {
87
+ try {
88
+ callback();
89
+ } catch {
90
+ // Ignore
91
+ }
92
+ }
93
+ }
94
+ match(name, query) {
95
+ if (macroCondition(isTesting() || isDevelopingApp())) {
96
+ return;
97
+ }
98
+ const mediaQueryList = matchMedia(query);
99
+ const listener = matcher => {
100
+ let changed = false;
101
+ if (matcher.matches) {
102
+ if (!this.matches.has(name)) {
103
+ this.matches.add(name);
104
+ changed = true;
105
+ }
106
+ } else {
107
+ changed = this.matches.has(name);
108
+ this.matches.delete(name);
109
+ }
110
+ if (changed) {
111
+ runTask(this, () => this.trigger('change'));
112
+ }
113
+ };
114
+ mediaQueryList.addEventListener('change', event => {
115
+ runTask(this, () => listener(event));
116
+ });
117
+ listener(mediaQueryList);
118
+ }
119
+ }
120
+
121
+ export { Media as default, defaultBreakpoints };
122
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.js","sources":["../../src/services/media.ts"],"sourcesContent":["import { assert } from '@ember/debug';\nimport Service from '@ember/service';\nimport {\n getOwnConfig,\n isDevelopingApp,\n isTesting,\n macroCondition,\n} from '@embroider/macros';\nimport { tracked } from '@glimmer/tracking';\nimport { runTask } from 'ember-lifeline';\nimport { TrackedSet } from 'tracked-built-ins';\n\nimport type Owner from '@ember/owner';\n\ntype Fn = () => unknown | Promise<unknown>;\ntype Callbacks = {\n change: Set<Fn>;\n};\ntype Matcher = {\n matches: boolean;\n media: string;\n};\n\nexport const defaultBreakpoints = Object.freeze({\n xsmall: '(min-width: 0px) and (max-width: 575px)',\n small: '(min-width: 576px) and (max-width: 767px)',\n medium: '(min-width: 768px) and (max-width: 991px)',\n large: '(min-width: 992px) and (max-width: 1199px)',\n xlarge: '(min-width: 1200px) and (max-width: 1399px)',\n xxlarge: '(min-width: 1400px)',\n});\n\nexport default class Media extends Service {\n _mockedBreakpoint = 'desktop';\n\n @tracked\n _matches = new TrackedSet<string>();\n\n @tracked\n mocked = macroCondition(isTesting() || isDevelopingApp()) ? true : false;\n\n callbacks: Callbacks = {\n change: new Set(),\n };\n\n breakpoints = {\n ...defaultBreakpoints,\n ...getOwnConfig()?.breakpoints,\n };\n\n constructor(owner: Owner) {\n super(owner);\n\n for (const [name, query] of Object.entries(this.breakpoints)) {\n this.match(name, query);\n }\n }\n\n get matches(): Set<string> {\n if (\n (macroCondition(isTesting() || isDevelopingApp()) ? true : false) &&\n this.mocked\n ) {\n return new TrackedSet([this._mockedBreakpoint]);\n }\n\n return this._matches;\n }\n\n private set matches(value: Iterable<string>) {\n this._matches = new TrackedSet(value);\n }\n\n get isXSmall(): boolean {\n return this.matches.has('xsmall');\n }\n\n get isSmall(): boolean {\n return this.matches.has('small');\n }\n\n get isMedium(): boolean {\n return this.matches.has('medium');\n }\n\n get isLarge(): boolean {\n return this.matches.has('large');\n }\n\n get isXLarge(): boolean {\n return this.matches.has('xlarge');\n }\n\n get isXXLarge(): boolean {\n return this.matches.has('xxlarge');\n }\n\n #getCallbackList(name: keyof Callbacks) {\n const callbackList = this.callbacks[name];\n\n assert(`Callback '${name}' is not valid`, callbackList !== undefined);\n\n return callbackList;\n }\n\n on(name: keyof Callbacks, callback: Fn) {\n const callbackList = this.#getCallbackList(name);\n\n callbackList.add(callback);\n }\n\n off(name: keyof Callbacks, callback: Fn) {\n const callbackList = this.#getCallbackList(name);\n\n callbackList.delete(callback);\n }\n\n trigger(name: keyof Callbacks) {\n const callbackList = this.#getCallbackList(name);\n for (const callback of callbackList) {\n try {\n callback();\n } catch {\n // Ignore\n }\n }\n }\n\n match(name: string, query: string) {\n if (macroCondition(isTesting() || isDevelopingApp())) {\n return;\n }\n\n const mediaQueryList = matchMedia(query);\n\n const listener = (matcher: Matcher) => {\n let changed = false;\n\n if (matcher.matches) {\n if (!this.matches.has(name)) {\n this.matches.add(name);\n changed = true;\n }\n } else {\n changed = this.matches.has(name);\n this.matches.delete(name);\n }\n\n if (changed) {\n runTask(this, () => this.trigger('change'));\n }\n };\n\n mediaQueryList.addEventListener('change', (event) => {\n runTask(this, () => listener(event));\n });\n\n listener(mediaQueryList);\n }\n}\n\ndeclare module '@ember/service' {\n interface Registry {\n media: Media;\n }\n}\n"],"names":["defaultBreakpoints","Object","freeze","xsmall","small","medium","large","xlarge","xxlarge","Media","Service","_mockedBreakpoint","g","prototype","tracked","TrackedSet","i","macroCondition","isTesting","isDevelopingApp","callbacks","change","Set","breakpoints","getOwnConfig","constructor","owner","name","query","entries","match","matches","mocked","_matches","value","isXSmall","has","isSmall","isMedium","isLarge","isXLarge","isXXLarge","#getCallbackList","callbackList","assert","undefined","on","callback","add","off","delete","trigger","mediaQueryList","matchMedia","listener","matcher","changed","runTask","addEventListener","event"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+
2
+ //# sourceMappingURL=template-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-registry.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ import { getContext, settled } from '@ember/test-helpers';
2
+
3
+ async function setBreakpoint(breakpoint) {
4
+ const {
5
+ owner
6
+ } = getContext();
7
+ const media = owner.lookup('service:media');
8
+ if (breakpoint === 'auto') {
9
+ media.mocked = false;
10
+ return;
11
+ }
12
+ if (Object.keys(media.breakpoints).indexOf(breakpoint) === -1) {
13
+ throw new Error(`Breakpoint "${breakpoint}" not defined as a breakpoint`);
14
+ }
15
+ media.mocked = true;
16
+ media._mockedBreakpoint = breakpoint;
17
+ media.trigger('change');
18
+ await settled();
19
+ }
20
+
21
+ export { setBreakpoint };
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/test-support/index.ts"],"sourcesContent":["import { getContext, settled } from '@ember/test-helpers';\n\nimport type Media from '../services/media.ts';\nimport type { TestContext } from '@ember/test-helpers';\n\nexport async function setBreakpoint(breakpoint: string) {\n const { owner } = getContext() as TestContext;\n const media = owner.lookup('service:media') as Media;\n\n if (breakpoint === 'auto') {\n media.mocked = false;\n return;\n }\n\n if (Object.keys(media.breakpoints).indexOf(breakpoint) === -1) {\n throw new Error(`Breakpoint \"${breakpoint}\" not defined as a breakpoint`);\n }\n\n media.mocked = true;\n media._mockedBreakpoint = breakpoint;\n media.trigger('change');\n\n await settled();\n}\n"],"names":["setBreakpoint","breakpoint","owner","getContext","media","lookup","mocked","Object","keys","breakpoints","indexOf","Error","_mockedBreakpoint","trigger","settled"],"mappings":";;AAKO,eAAeA,aAAaA,CAACC,UAAkB,EAAE;EACtD,MAAM;AAAEC,IAAAA;GAAO,GAAGC,UAAU,EAAiB;AAC7C,EAAA,MAAMC,KAAK,GAAGF,KAAK,CAACG,MAAM,CAAC,eAAe,CAAU;EAEpD,IAAIJ,UAAU,KAAK,MAAM,EAAE;IACzBG,KAAK,CAACE,MAAM,GAAG,KAAK;AACpB,IAAA;AACF,EAAA;AAEA,EAAA,IAAIC,MAAM,CAACC,IAAI,CAACJ,KAAK,CAACK,WAAW,CAAC,CAACC,OAAO,CAACT,UAAU,CAAC,KAAK,EAAE,EAAE;AAC7D,IAAA,MAAM,IAAIU,KAAK,CAAC,CAAA,YAAA,EAAeV,UAAU,+BAA+B,CAAC;AAC3E,EAAA;EAEAG,KAAK,CAACE,MAAM,GAAG,IAAI;EACnBF,KAAK,CAACQ,iBAAiB,GAAGX,UAAU;AACpCG,EAAAA,KAAK,CAACS,OAAO,CAAC,QAAQ,CAAC;EAEvB,MAAMC,OAAO,EAAE;AACjB;;;;"}
package/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@nrg-ui/ember-media",
3
+ "version": "0.1.1",
4
+ "description": "An Ember addon for safe media query handling",
5
+ "keywords": [
6
+ "ember-addon"
7
+ ],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/knoxville-utilities-board/ember-media.git"
11
+ },
12
+ "license": "MIT",
13
+ "author": "",
14
+ "imports": {
15
+ "#src/*": "./src/*"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./declarations/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./test-support": {
23
+ "types": "./declarations/test-support/index.d.ts",
24
+ "default": "./dist/test-support/index.js"
25
+ },
26
+ "./addon-main.js": "./addon-main.cjs",
27
+ "./*.css": "./dist/*.css",
28
+ "./*": {
29
+ "types": "./declarations/*.d.ts",
30
+ "default": "./dist/*.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "addon-main.cjs",
35
+ "declarations",
36
+ "dist",
37
+ "src"
38
+ ],
39
+ "dependencies": {
40
+ "@embroider/addon-shim": "^1.10.2",
41
+ "decorator-transforms": "^2.3.1",
42
+ "ember-lifeline": "^7.0.0",
43
+ "tracked-built-ins": "^4.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@babel/core": "^7.28.5",
47
+ "@babel/eslint-parser": "^7.28.5",
48
+ "@babel/plugin-transform-typescript": "^7.28.5",
49
+ "@babel/runtime": "^7.28.4",
50
+ "@ember/app-tsconfig": "^2.0.0",
51
+ "@ember/library-tsconfig": "^2.0.0",
52
+ "@ember/test-helpers": "^5.4.1",
53
+ "@ember/test-waiters": "^4.1.1",
54
+ "@embroider/addon-dev": "^8.2.0",
55
+ "@embroider/compat": "^4.1.12",
56
+ "@embroider/core": "^4.4.2",
57
+ "@embroider/macros": "^1.19.6",
58
+ "@embroider/vite": "^1.5.0",
59
+ "@eslint/js": "^9.39.2",
60
+ "@glimmer/component": "^2.0.0",
61
+ "@glint/ember-tsc": "^1.0.8",
62
+ "@glint/template": "^1.7.3",
63
+ "@glint/tsserver-plugin": "^2.0.8",
64
+ "@nrg-ui/standards": "^0.6.2",
65
+ "@rollup/plugin-babel": "^6.1.0",
66
+ "@types/qunit": "^2.19.13",
67
+ "babel-plugin-ember-template-compilation": "^3.0.1",
68
+ "concurrently": "^9.2.1",
69
+ "ember-page-title": "^9.0.3",
70
+ "ember-qunit": "^9.0.4",
71
+ "ember-source": "^6.9.0",
72
+ "ember-strict-application-resolver": "^0.1.1",
73
+ "ember-template-lint": "^7.9.3",
74
+ "eslint": "^9.39.2",
75
+ "eslint-plugin-decorator-position": "^6.0.0",
76
+ "eslint-plugin-ember": "^12.7.5",
77
+ "eslint-plugin-import": "^2.32.0",
78
+ "eslint-plugin-n": "^17.23.1",
79
+ "eslint-plugin-qunit": "^8.2.5",
80
+ "prettier": "^3.7.4",
81
+ "prettier-plugin-ember-template-tag": "^2.1.2",
82
+ "qunit": "^2.25.0",
83
+ "qunit-dom": "^3.5.0",
84
+ "release-plan": "^0.17.0",
85
+ "rollup": "^4.54.0",
86
+ "testem": "^3.17.0",
87
+ "typescript": "~5.9.3",
88
+ "typescript-eslint": "^8.51.0",
89
+ "vite": "^7.3.0"
90
+ },
91
+ "publishConfig": {
92
+ "access": "public",
93
+ "registry": "https://registry.npmjs.org"
94
+ },
95
+ "ember": {
96
+ "edition": "octane"
97
+ },
98
+ "ember-addon": {
99
+ "version": 2,
100
+ "type": "addon",
101
+ "main": "addon-main.cjs",
102
+ "app-js": {
103
+ "./services/media.js": "./dist/_app_/services/media.js"
104
+ }
105
+ },
106
+ "scripts": {
107
+ "build": "rollup --config",
108
+ "format": "prettier . --cache --write",
109
+ "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
110
+ "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm run format",
111
+ "lint:format": "prettier . --cache --check",
112
+ "lint:hbs": "ember-template-lint . --no-error-on-unmatched-pattern",
113
+ "lint:hbs:fix": "ember-template-lint . --fix --no-error-on-unmatched-pattern",
114
+ "lint:js": "eslint . --cache",
115
+ "lint:js:fix": "eslint . --fix",
116
+ "lint:types": "ember-tsc --noEmit",
117
+ "start": "vite dev",
118
+ "test": "vite build --mode=development --out-dir dist-tests && testem --file testem.cjs ci --port 0"
119
+ }
120
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type MediaService from './services/media.ts';
2
+
3
+ export type { MediaService };
4
+ export { defaultBreakpoints } from './services/media.ts';
@@ -0,0 +1,166 @@
1
+ import { assert } from '@ember/debug';
2
+ import Service from '@ember/service';
3
+ import {
4
+ getOwnConfig,
5
+ isDevelopingApp,
6
+ isTesting,
7
+ macroCondition,
8
+ } from '@embroider/macros';
9
+ import { tracked } from '@glimmer/tracking';
10
+ import { runTask } from 'ember-lifeline';
11
+ import { TrackedSet } from 'tracked-built-ins';
12
+
13
+ import type Owner from '@ember/owner';
14
+
15
+ type Fn = () => unknown | Promise<unknown>;
16
+ type Callbacks = {
17
+ change: Set<Fn>;
18
+ };
19
+ type Matcher = {
20
+ matches: boolean;
21
+ media: string;
22
+ };
23
+
24
+ export const defaultBreakpoints = Object.freeze({
25
+ xsmall: '(min-width: 0px) and (max-width: 575px)',
26
+ small: '(min-width: 576px) and (max-width: 767px)',
27
+ medium: '(min-width: 768px) and (max-width: 991px)',
28
+ large: '(min-width: 992px) and (max-width: 1199px)',
29
+ xlarge: '(min-width: 1200px) and (max-width: 1399px)',
30
+ xxlarge: '(min-width: 1400px)',
31
+ });
32
+
33
+ export default class Media extends Service {
34
+ _mockedBreakpoint = 'desktop';
35
+
36
+ @tracked
37
+ _matches = new TrackedSet<string>();
38
+
39
+ @tracked
40
+ mocked = macroCondition(isTesting() || isDevelopingApp()) ? true : false;
41
+
42
+ callbacks: Callbacks = {
43
+ change: new Set(),
44
+ };
45
+
46
+ breakpoints = {
47
+ ...defaultBreakpoints,
48
+ ...getOwnConfig()?.breakpoints,
49
+ };
50
+
51
+ constructor(owner: Owner) {
52
+ super(owner);
53
+
54
+ for (const [name, query] of Object.entries(this.breakpoints)) {
55
+ this.match(name, query);
56
+ }
57
+ }
58
+
59
+ get matches(): Set<string> {
60
+ if (
61
+ (macroCondition(isTesting() || isDevelopingApp()) ? true : false) &&
62
+ this.mocked
63
+ ) {
64
+ return new TrackedSet([this._mockedBreakpoint]);
65
+ }
66
+
67
+ return this._matches;
68
+ }
69
+
70
+ private set matches(value: Iterable<string>) {
71
+ this._matches = new TrackedSet(value);
72
+ }
73
+
74
+ get isXSmall(): boolean {
75
+ return this.matches.has('xsmall');
76
+ }
77
+
78
+ get isSmall(): boolean {
79
+ return this.matches.has('small');
80
+ }
81
+
82
+ get isMedium(): boolean {
83
+ return this.matches.has('medium');
84
+ }
85
+
86
+ get isLarge(): boolean {
87
+ return this.matches.has('large');
88
+ }
89
+
90
+ get isXLarge(): boolean {
91
+ return this.matches.has('xlarge');
92
+ }
93
+
94
+ get isXXLarge(): boolean {
95
+ return this.matches.has('xxlarge');
96
+ }
97
+
98
+ #getCallbackList(name: keyof Callbacks) {
99
+ const callbackList = this.callbacks[name];
100
+
101
+ assert(`Callback '${name}' is not valid`, callbackList !== undefined);
102
+
103
+ return callbackList;
104
+ }
105
+
106
+ on(name: keyof Callbacks, callback: Fn) {
107
+ const callbackList = this.#getCallbackList(name);
108
+
109
+ callbackList.add(callback);
110
+ }
111
+
112
+ off(name: keyof Callbacks, callback: Fn) {
113
+ const callbackList = this.#getCallbackList(name);
114
+
115
+ callbackList.delete(callback);
116
+ }
117
+
118
+ trigger(name: keyof Callbacks) {
119
+ const callbackList = this.#getCallbackList(name);
120
+ for (const callback of callbackList) {
121
+ try {
122
+ callback();
123
+ } catch {
124
+ // Ignore
125
+ }
126
+ }
127
+ }
128
+
129
+ match(name: string, query: string) {
130
+ if (macroCondition(isTesting() || isDevelopingApp())) {
131
+ return;
132
+ }
133
+
134
+ const mediaQueryList = matchMedia(query);
135
+
136
+ const listener = (matcher: Matcher) => {
137
+ let changed = false;
138
+
139
+ if (matcher.matches) {
140
+ if (!this.matches.has(name)) {
141
+ this.matches.add(name);
142
+ changed = true;
143
+ }
144
+ } else {
145
+ changed = this.matches.has(name);
146
+ this.matches.delete(name);
147
+ }
148
+
149
+ if (changed) {
150
+ runTask(this, () => this.trigger('change'));
151
+ }
152
+ };
153
+
154
+ mediaQueryList.addEventListener('change', (event) => {
155
+ runTask(this, () => listener(event));
156
+ });
157
+
158
+ listener(mediaQueryList);
159
+ }
160
+ }
161
+
162
+ declare module '@ember/service' {
163
+ interface Registry {
164
+ media: Media;
165
+ }
166
+ }
@@ -0,0 +1,10 @@
1
+ // Easily allow apps, which are not yet using strict mode templates, to consume your Glint types, by importing this file.
2
+ // Add all your components, helpers and modifiers to the template registry here, so apps don't have to do this.
3
+ // See https://typed-ember.gitbook.io/glint/environments/ember/authoring-addons
4
+
5
+ // import type MyComponent from './components/my-component';
6
+
7
+ // Uncomment this once entries have been added! 👇
8
+ // export default interface Registry {
9
+ // MyComponent: typeof MyComponent
10
+ // }
@@ -0,0 +1,24 @@
1
+ import { getContext, settled } from '@ember/test-helpers';
2
+
3
+ import type Media from '../services/media.ts';
4
+ import type { TestContext } from '@ember/test-helpers';
5
+
6
+ export async function setBreakpoint(breakpoint: string) {
7
+ const { owner } = getContext() as TestContext;
8
+ const media = owner.lookup('service:media') as Media;
9
+
10
+ if (breakpoint === 'auto') {
11
+ media.mocked = false;
12
+ return;
13
+ }
14
+
15
+ if (Object.keys(media.breakpoints).indexOf(breakpoint) === -1) {
16
+ throw new Error(`Breakpoint "${breakpoint}" not defined as a breakpoint`);
17
+ }
18
+
19
+ media.mocked = true;
20
+ media._mockedBreakpoint = breakpoint;
21
+ media.trigger('change');
22
+
23
+ await settled();
24
+ }