responsive-media 1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ npm install responsive-media
2
+
3
+ # responsive-media
4
+
5
+ A utility for reactive state based on CSS media queries. Includes integration with Vue 3 (Composition API).
6
+
7
+ ## Installation
8
+
9
+ ```
10
+ npm install responsive-media
11
+ ```
12
+
13
+ ## Usage without Vue
14
+
15
+ ### Getting the state
16
+
17
+ ```ts
18
+ import { responsiveState } from 'responsive-media';
19
+
20
+ // Get the current state:
21
+ const { mobile, tablet, desktop } = responsiveState.proxy;
22
+
23
+ console.log('isMobile:', mobile);
24
+ console.log('isTablet:', tablet);
25
+ console.log('isDesktop:', desktop);
26
+ ```
27
+
28
+ ### Subscribing to changes
29
+
30
+ ```ts
31
+ // Subscribe to state changes:
32
+ const unsubscribe = responsiveState.subscribe((state) => {
33
+ console.log('State changed:', state);
34
+ // state.mobile, state.tablet, state.desktop
35
+ });
36
+
37
+ // To unsubscribe:
38
+ unsubscribe();
39
+ ```
40
+
41
+ ## Usage with Vue 3 (Composition API)
42
+
43
+ ### Plugin registration
44
+
45
+ ```ts
46
+ import { createApp } from 'vue';
47
+ import { ResponsivePlugin, ResponsiveConfig } from 'responsive-media';
48
+ import App from './App.vue';
49
+
50
+ const app = createApp(App);
51
+
52
+ // Use default breakpoints:
53
+ app.use(ResponsivePlugin);
54
+
55
+ // Or provide your own breakpoints (now you can combine conditions):
56
+ app.use(ResponsivePlugin, {
57
+ ...ResponsiveConfig, // keep default breakpoints
58
+ myCustom: [
59
+ { type: 'min-width', value: 1200 },
60
+ { type: 'aspect-ratio', value: '16/9' },
61
+ ], // add your own with multiple conditions
62
+ mobile: [
63
+ { type: 'max-width', value: 500 },
64
+ { type: 'orientation', value: 'portrait' },
65
+ ], // override default with multiple conditions
66
+ });
67
+
68
+ app.mount('#app');
69
+ ```
70
+
71
+ ### Usage in a component
72
+
73
+ ```ts
74
+ <script setup lang="ts">
75
+ import { useResponsive } from 'responsive-media';
76
+
77
+ const responsive = useResponsive();
78
+
79
+ // responsive.mobile, responsive.tablet, responsive.desktop, etc.
80
+ </script>
81
+ ```
82
+
83
+ ## Customizing breakpoints
84
+
85
+ You can override or add your own breakpoints using the setResponsiveConfig function. Now each breakpoint can be an array of conditions (they will be combined with and):
86
+
87
+ ```ts
88
+ import { setResponsiveConfig, ResponsiveConfig } from 'responsive-media';
89
+
90
+ setResponsiveConfig({
91
+ ...ResponsiveConfig, // keep default
92
+ myCustom: [
93
+ { type: 'min-width', value: 1200 },
94
+ { type: 'aspect-ratio', value: '16/9' },
95
+ ], // add your own breakpoint with multiple conditions
96
+ mobile: [
97
+ { type: 'max-width', value: 500 },
98
+ { type: 'orientation', value: 'portrait' },
99
+ ], // override default with multiple conditions
100
+ });
101
+ ```
102
+
103
+ ## Exported entities
104
+ - responsiveState
105
+ - ResponsiveConfig
106
+ - setResponsiveConfig
107
+ - ResponsivePlugin (Vue)
108
+ - useResponsive (Vue)
109
+
110
+ ## Breakpoint config format
111
+
112
+ Each breakpoint is now described by an array of conditions (MediaQueryConfig = MediaQueryCondition[]), where each condition is an object with type and value. All conditions within a breakpoint are combined with and (e.g., `(max-width: 600px) and (aspect-ratio: 16/9)`).
113
+
114
+ ## Author
115
+
116
+ Danil Lisin Vladimirovich aka Macrulez
117
+
118
+ GitHub: [macrulezru](https://github.com/macrulezru)
119
+
120
+ Website: [macrulez.ru](https://macrulez.ru/)
121
+
122
+ ## License
123
+ MIT
@@ -0,0 +1,17 @@
1
+ import { type Breakpoint, type MediaQueryConfig } from './responsive.enum';
2
+ export type { MediaQueryConfig };
3
+ type ResponsiveState = Record<Breakpoint, boolean>;
4
+ type ResponsiveListener = (state: ResponsiveState) => void;
5
+ declare class ReactiveResponsiveState {
6
+ private state;
7
+ private listeners;
8
+ proxy: ResponsiveState;
9
+ private queries;
10
+ constructor();
11
+ private applyConfig;
12
+ setConfig(config: Record<string, MediaQueryConfig>): void;
13
+ subscribe(listener: ResponsiveListener): () => boolean;
14
+ private notify;
15
+ }
16
+ export declare const responsiveState: ReactiveResponsiveState;
17
+ export declare function setResponsiveConfig(config: Record<string, MediaQueryConfig>): void;
@@ -0,0 +1,61 @@
1
+ import { ResponsiveConfig } from './responsive.enum';
2
+ class ReactiveResponsiveState {
3
+ constructor() {
4
+ this.listeners = new Set();
5
+ this.queries = {};
6
+ this.state = {};
7
+ this.proxy = new Proxy(this.state, {
8
+ set: (target, prop, value) => {
9
+ if (target[prop] !== value) {
10
+ target[prop] = value;
11
+ this.notify();
12
+ }
13
+ return true;
14
+ },
15
+ get: (target, prop) => {
16
+ return target[prop];
17
+ },
18
+ });
19
+ this.applyConfig(ResponsiveConfig);
20
+ }
21
+ applyConfig(config) {
22
+ Object.entries(this.queries).forEach(([key, query]) => {
23
+ query.onchange = null;
24
+ });
25
+ this.queries = {};
26
+ Object.keys(this.state).forEach(key => delete this.state[key]);
27
+ Object.entries(config).forEach(([key, conditions]) => {
28
+ // Формируем строку media-запроса из массива условий
29
+ const mq = conditions
30
+ .map(cond => {
31
+ const val = typeof cond.value === 'number' && !String(cond.type).includes('aspect-ratio')
32
+ ? cond.value + 'px'
33
+ : cond.value;
34
+ return `(${cond.type}: ${val})`;
35
+ })
36
+ .join(' and ');
37
+ const query = window.matchMedia(mq);
38
+ this.queries[key] = query;
39
+ this.proxy[key] = query.matches;
40
+ query.addEventListener('change', (e) => {
41
+ this.proxy[key] = e.matches;
42
+ });
43
+ });
44
+ this.notify();
45
+ }
46
+ setConfig(config) {
47
+ this.applyConfig(config);
48
+ }
49
+ subscribe(listener) {
50
+ this.listeners.add(listener);
51
+ listener(this.proxy);
52
+ return () => this.listeners.delete(listener);
53
+ }
54
+ notify() {
55
+ this.listeners.forEach(listener => listener(this.proxy));
56
+ }
57
+ }
58
+ export const responsiveState = new ReactiveResponsiveState();
59
+ export function setResponsiveConfig(config) {
60
+ responsiveState.setConfig(config);
61
+ }
@@ -0,0 +1,2 @@
1
+ export * from './responsive.enum';
2
+ export * from './create-responsive';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './responsive.enum';
2
+ export * from './create-responsive';
@@ -0,0 +1,7 @@
1
+ export type Breakpoint = 'mobile' | 'tablet' | 'desktop';
2
+ export interface MediaQueryCondition {
3
+ type: 'width' | 'min-width' | 'max-width' | 'height' | 'min-height' | 'max-height' | 'aspect-ratio' | 'min-aspect-ratio' | 'max-aspect-ratio' | 'orientation' | 'resolution' | 'min-resolution' | 'max-resolution' | 'color' | 'min-color' | 'max-color' | 'color-index' | 'min-color-index' | 'max-color-index' | 'monochrome' | 'min-monochrome' | 'max-monochrome' | 'scan' | 'grid';
4
+ value: number | string;
5
+ }
6
+ export type MediaQueryConfig = MediaQueryCondition[];
7
+ export declare const ResponsiveConfig: Record<Breakpoint, MediaQueryConfig>;
@@ -0,0 +1,11 @@
1
+ export const ResponsiveConfig = {
2
+ mobile: [
3
+ { type: 'max-width', value: 600 },
4
+ ],
5
+ tablet: [
6
+ { type: 'max-width', value: 960 },
7
+ ],
8
+ desktop: [
9
+ { type: 'min-width', value: 961 },
10
+ ],
11
+ };
@@ -0,0 +1,8 @@
1
+ import type { App } from '@vue/runtime-core';
2
+ import type { MediaQueryConfig } from './create-responsive';
3
+ export declare function useResponsive(): any;
4
+ type ResponsiveConfigType = Record<string, MediaQueryConfig>;
5
+ export declare const ResponsivePlugin: {
6
+ install(app: App, config?: ResponsiveConfigType): void;
7
+ };
8
+ export {};
@@ -0,0 +1,38 @@
1
+ import { inject, readonly, reactive } from '@vue/runtime-core';
2
+ import { responsiveState, setResponsiveConfig } from './create-responsive';
3
+ const RESPONSIVE_KEY = Symbol('responsiveState');
4
+ let vueReactiveState = null;
5
+ export function useResponsive() {
6
+ // Получаем реактивный объект из provide/inject или создаём локально
7
+ const injected = inject(RESPONSIVE_KEY);
8
+ if (injected)
9
+ return injected;
10
+ if (!vueReactiveState) {
11
+ vueReactiveState = readonly(reactive({ ...responsiveState.proxy }));
12
+ // Синхронизируем с изменениями responsiveState
13
+ responsiveState.subscribe((state) => {
14
+ Object.keys(state).forEach(key => {
15
+ // @ts-ignore
16
+ vueReactiveState[key] = state[key];
17
+ });
18
+ });
19
+ }
20
+ return vueReactiveState;
21
+ }
22
+ export const ResponsivePlugin = {
23
+ install(app, config) {
24
+ if (config) {
25
+ setResponsiveConfig(config);
26
+ }
27
+ if (!vueReactiveState) {
28
+ vueReactiveState = readonly(reactive({ ...responsiveState.proxy }));
29
+ responsiveState.subscribe((state) => {
30
+ Object.keys(state).forEach(key => {
31
+ // @ts-ignore
32
+ vueReactiveState[key] = state[key];
33
+ });
34
+ });
35
+ }
36
+ app.provide(RESPONSIVE_KEY, vueReactiveState);
37
+ },
38
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "responsive-media",
3
+ "version": "1.0.0",
4
+ "description": "Утилита для реактивного состояния на основе CSS media queries без зависимостей.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/yourusername/responsive-media.git"
24
+ },
25
+ "keywords": [
26
+ "responsive",
27
+ "media-query",
28
+ "typescript",
29
+ "reactive",
30
+ "utility"
31
+ ],
32
+ "author": "",
33
+ "license": "MIT",
34
+ "devDependencies": {
35
+ "@types/vue": "^1.0.31",
36
+ "@typescript-eslint/eslint-plugin": "^8.53.0",
37
+ "@typescript-eslint/parser": "^8.53.0",
38
+ "eslint": "^9.39.2",
39
+ "eslint-plugin-vue": "^10.7.0",
40
+ "typescript": "^5.9.3"
41
+ },
42
+ "peerDependencies": {
43
+ "vue": "^3.5.27"
44
+ }
45
+ }