responsive-media 1.0.7 → 1.2.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/README.md +868 -141
- package/dist/base-state.d.ts +182 -0
- package/dist/base-state.js +406 -0
- package/dist/container-state.d.ts +36 -0
- package/dist/container-state.js +107 -0
- package/dist/create-responsive.d.ts +46 -17
- package/dist/create-responsive.js +117 -54
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/media-query.d.ts +17 -0
- package/dist/media-query.js +30 -0
- package/dist/presets.d.ts +48 -0
- package/dist/presets.js +71 -0
- package/dist/react-responsive.d.ts +64 -0
- package/dist/react-responsive.js +95 -0
- package/dist/responsive.enum.d.ts +58 -2
- package/dist/responsive.enum.js +9 -9
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +3 -0
- package/dist/vue-responsive.d.ts +80 -6
- package/dist/vue-responsive.js +159 -24
- package/package.json +82 -49
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useSyncExternalStore, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { responsiveState } from './create-responsive';
|
|
3
|
+
import { subscribeMediaQuery } from './media-query';
|
|
4
|
+
import { createContainerState } from './container-state';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// useResponsive
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* Returns the current responsive state. Re-renders only when state changes.
|
|
10
|
+
* Requires React 18+. Generic `T` narrows the type for custom configs.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* type MyState = { sm: boolean; lg: boolean };
|
|
14
|
+
* const { sm, lg } = useResponsive<MyState>();
|
|
15
|
+
*/
|
|
16
|
+
export function useResponsive() {
|
|
17
|
+
return useSyncExternalStore((onChange) => responsiveState.subscribe(() => onChange()), () => responsiveState.getState(), () => responsiveState.getState());
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns ordered breakpoint helpers. Re-renders when the responsive state
|
|
21
|
+
* changes, so all helpers always reflect the current breakpoint.
|
|
22
|
+
*
|
|
23
|
+
* Requires a breakpoint `order` set via `setResponsiveConfig` or
|
|
24
|
+
* `createResponsiveState`. Falls back to config key insertion order.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const { current, isAbove, isBelow, between } = useBreakpoints();
|
|
28
|
+
* return isAbove('sm') ? <DesktopNav /> : <MobileNav />;
|
|
29
|
+
*/
|
|
30
|
+
export function useBreakpoints() {
|
|
31
|
+
// Subscribe to state so the component re-renders on changes
|
|
32
|
+
useResponsive();
|
|
33
|
+
return {
|
|
34
|
+
current: responsiveState.current,
|
|
35
|
+
isAbove: (key) => responsiveState.isAbove(key),
|
|
36
|
+
isBelow: (key) => responsiveState.isBelow(key),
|
|
37
|
+
between: (from, to) => responsiveState.between(from, to),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// useMediaQuery
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/**
|
|
44
|
+
* Returns a boolean that tracks a raw CSS media query string.
|
|
45
|
+
* Compatible with React 18+ SSR (returns `false` on the server).
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const isDark = useMediaQuery('(prefers-color-scheme: dark)');
|
|
49
|
+
* const canHover = useMediaQuery('(hover: hover)');
|
|
50
|
+
*/
|
|
51
|
+
export function useMediaQuery(query) {
|
|
52
|
+
const queryRef = useRef(query);
|
|
53
|
+
queryRef.current = query;
|
|
54
|
+
const [matches, setMatches] = useState(false);
|
|
55
|
+
useEffect(() => subscribeMediaQuery(queryRef.current, setMatches), [query]);
|
|
56
|
+
return matches;
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// useContainerState
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
/**
|
|
62
|
+
* Tracks an element's dimensions with `ResizeObserver` and evaluates
|
|
63
|
+
* breakpoint conditions in JavaScript (Container Queries).
|
|
64
|
+
*
|
|
65
|
+
* Pass a `ref` created with `useRef<Element>(null)` — the hook sets up the
|
|
66
|
+
* observer after mount and cleans up on unmount.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* function Card() {
|
|
70
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
71
|
+
* const { compact, wide } = useContainerState(ref, {
|
|
72
|
+
* compact: [{ type: 'max-width', value: 300 }],
|
|
73
|
+
* wide: [{ type: 'min-width', value: 600 }],
|
|
74
|
+
* });
|
|
75
|
+
* return (
|
|
76
|
+
* <div ref={ref}>
|
|
77
|
+
* {compact ? <CompactLayout /> : wide ? <WideLayout /> : <DefaultLayout />}
|
|
78
|
+
* </div>
|
|
79
|
+
* );
|
|
80
|
+
* }
|
|
81
|
+
*/
|
|
82
|
+
export function useContainerState(ref, config, options) {
|
|
83
|
+
const [state, setState] = useState({});
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const el = ref.current;
|
|
86
|
+
if (!el)
|
|
87
|
+
return;
|
|
88
|
+
const cs = createContainerState(el, config, options);
|
|
89
|
+
const off = cs.subscribe((s) => setState({ ...s }));
|
|
90
|
+
return () => { off(); cs.destroy(); };
|
|
91
|
+
// config / options are treated as static after mount; memoize if needed
|
|
92
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
}, []);
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
@@ -1,7 +1,63 @@
|
|
|
1
1
|
export type Breakpoint = 'mobile' | 'tablet' | 'desktop';
|
|
2
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'
|
|
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' | 'prefers-color-scheme' | 'prefers-reduced-motion' | 'prefers-contrast' | 'hover' | 'any-hover' | 'pointer' | 'any-pointer' | 'forced-colors' | 'display-mode' | 'update'
|
|
4
|
+
/**
|
|
5
|
+
* Inserts the value verbatim into the media query string without wrapping
|
|
6
|
+
* it in parentheses. Use for media types (`print`, `screen`, `all`) or
|
|
7
|
+
* any raw token not covered by the typed conditions above.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Match only print media
|
|
11
|
+
* { type: 'raw', value: 'print' }
|
|
12
|
+
* // → window.matchMedia('print')
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Restrict a condition to screen media
|
|
16
|
+
* [{ type: 'raw', value: 'screen' }, { type: 'max-width', value: 600 }]
|
|
17
|
+
* // → window.matchMedia('screen and (max-width: 600px)')
|
|
18
|
+
*/
|
|
19
|
+
| 'raw';
|
|
4
20
|
value: number | string;
|
|
5
21
|
}
|
|
6
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A flat array of conditions joined with `and`.
|
|
24
|
+
* Example: [min-width: 600, max-width: 960] → (min-width: 600px) and (max-width: 960px)
|
|
25
|
+
*/
|
|
26
|
+
export type MediaQueryAndGroup = MediaQueryCondition[];
|
|
27
|
+
/**
|
|
28
|
+
* A config entry for a single breakpoint.
|
|
29
|
+
*
|
|
30
|
+
* - Flat array → all conditions joined with `and`
|
|
31
|
+
* - Nested array → inner arrays joined with `and`, outer arrays joined with `,` (or)
|
|
32
|
+
*
|
|
33
|
+
* @example AND only
|
|
34
|
+
* [{ type: 'min-width', value: 601 }, { type: 'max-width', value: 960 }]
|
|
35
|
+
*
|
|
36
|
+
* @example OR between groups
|
|
37
|
+
* [
|
|
38
|
+
* [{ type: 'max-width', value: 600 }],
|
|
39
|
+
* [{ type: 'orientation', value: 'portrait' }, { type: 'max-width', value: 1024 }],
|
|
40
|
+
* ]
|
|
41
|
+
*/
|
|
42
|
+
export type MediaQueryConfig = MediaQueryCondition[] | MediaQueryCondition[][];
|
|
43
|
+
/**
|
|
44
|
+
* Derives a boolean-state shape from a breakpoint config object.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const config = {
|
|
48
|
+
* sm: [{ type: 'max-width', value: 640 }],
|
|
49
|
+
* lg: [{ type: 'min-width', value: 1024 }],
|
|
50
|
+
* } satisfies Record<string, MediaQueryConfig>;
|
|
51
|
+
*
|
|
52
|
+
* type MyState = ConfigToState<typeof config>; // { sm: boolean; lg: boolean }
|
|
53
|
+
*/
|
|
54
|
+
export type ConfigToState<T extends Record<string, MediaQueryConfig>> = {
|
|
55
|
+
[K in keyof T]: boolean;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Default breakpoint config. Mutually exclusive:
|
|
59
|
+
* - mobile: ≤ 600px
|
|
60
|
+
* - tablet: 601px – 960px
|
|
61
|
+
* - desktop: ≥ 961px
|
|
62
|
+
*/
|
|
7
63
|
export declare const ResponsiveConfig: Record<Breakpoint, MediaQueryConfig>;
|
package/dist/responsive.enum.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default breakpoint config. Mutually exclusive:
|
|
3
|
+
* - mobile: ≤ 600px
|
|
4
|
+
* - tablet: 601px – 960px
|
|
5
|
+
* - desktop: ≥ 961px
|
|
6
|
+
*/
|
|
1
7
|
export const ResponsiveConfig = {
|
|
2
|
-
mobile: [
|
|
3
|
-
|
|
4
|
-
],
|
|
5
|
-
tablet: [
|
|
6
|
-
{ type: 'max-width', value: 960 },
|
|
7
|
-
],
|
|
8
|
-
desktop: [
|
|
9
|
-
{ type: 'min-width', value: 961 },
|
|
10
|
-
],
|
|
8
|
+
mobile: [{ type: 'max-width', value: 600 }],
|
|
9
|
+
tablet: [{ type: 'min-width', value: 601 }, { type: 'max-width', value: 960 }],
|
|
10
|
+
desktop: [{ type: 'min-width', value: 961 }],
|
|
11
11
|
};
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
package/dist/vue-responsive.d.ts
CHANGED
|
@@ -1,8 +1,82 @@
|
|
|
1
|
-
import type { App } from '@vue/runtime-core';
|
|
2
|
-
import type { MediaQueryConfig } from './create-responsive';
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { App, Ref, ComputedRef } from '@vue/runtime-core';
|
|
2
|
+
import type { MediaQueryConfig, ResponsiveState, SetConfigOptions } from './create-responsive';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the reactive responsive state.
|
|
5
|
+
* Generic `T` narrows the type for custom configs.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* type MyState = { sm: boolean; lg: boolean };
|
|
9
|
+
* const state = useResponsive<MyState>();
|
|
10
|
+
*/
|
|
11
|
+
export declare function useResponsive<T extends Record<string, boolean> = Record<string, boolean>>(): T;
|
|
12
|
+
export interface BreakpointHelpers {
|
|
13
|
+
/** First active breakpoint key, or `null`. Reactive computed. */
|
|
14
|
+
current: ComputedRef<string | null>;
|
|
15
|
+
/** `true` when the current breakpoint is after `key` in the order. Reactive in templates. */
|
|
16
|
+
isAbove: (key: string) => boolean;
|
|
17
|
+
/** `true` when the current breakpoint is before `key` in the order. Reactive in templates. */
|
|
18
|
+
isBelow: (key: string) => boolean;
|
|
19
|
+
/** `true` when the current breakpoint is between `from` and `to` (inclusive). Reactive in templates. */
|
|
20
|
+
between: (from: string, to: string) => boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns ordered breakpoint helpers that are **reactive in Vue templates
|
|
24
|
+
* and computed properties** — they read from the Vue reactive state and
|
|
25
|
+
* re-evaluate automatically on viewport changes.
|
|
26
|
+
*
|
|
27
|
+
* Requires a breakpoint `order` to be set via `setConfig` or `createResponsiveState`.
|
|
28
|
+
* Falls back to config key insertion order if none is provided.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const { current, isAbove, isBelow, between } = useBreakpoints();
|
|
32
|
+
*
|
|
33
|
+
* // In template:
|
|
34
|
+
* // <DesktopNav v-if="isAbove('sm')" />
|
|
35
|
+
* // <span>{{ current }}</span>
|
|
36
|
+
*/
|
|
37
|
+
export declare function useBreakpoints(): BreakpointHelpers;
|
|
38
|
+
/**
|
|
39
|
+
* Reactive composable for a single raw CSS media query string.
|
|
40
|
+
* Returns a `Ref<boolean>`. Cleans up automatically on `onUnmounted`.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const isDark = useMediaQuery('(prefers-color-scheme: dark)');
|
|
44
|
+
* const canHover = useMediaQuery('(hover: hover)');
|
|
45
|
+
*/
|
|
46
|
+
export declare function useMediaQuery(query: string): Ref<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Reactive composable that tracks an element's dimensions with `ResizeObserver`
|
|
49
|
+
* and evaluates breakpoint conditions in JavaScript (Container Queries).
|
|
50
|
+
*
|
|
51
|
+
* Accepts a `Ref<Element | null>` (e.g. from `useTemplateRef`) — automatically
|
|
52
|
+
* sets up and tears down the observer as the element mounts / unmounts.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* <script setup>
|
|
56
|
+
* const cardRef = useTemplateRef('card');
|
|
57
|
+
* const cardState = useContainerState(cardRef, {
|
|
58
|
+
* compact: [{ type: 'max-width', value: 300 }],
|
|
59
|
+
* wide: [{ type: 'min-width', value: 600 }],
|
|
60
|
+
* });
|
|
61
|
+
* </script>
|
|
62
|
+
*
|
|
63
|
+
* <template>
|
|
64
|
+
* <div ref="card">
|
|
65
|
+
* <CompactLayout v-if="cardState.compact" />
|
|
66
|
+
* <WideLayout v-else-if="cardState.wide" />
|
|
67
|
+
* </div>
|
|
68
|
+
* </template>
|
|
69
|
+
*/
|
|
70
|
+
export declare function useContainerState(elementRef: Ref<Element | null>, config: Record<string, MediaQueryConfig>, options?: SetConfigOptions): ResponsiveState;
|
|
71
|
+
/**
|
|
72
|
+
* Vue plugin that registers the responsive state via `provide`.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* app.use(ResponsivePlugin, {
|
|
76
|
+
* sm: [{ type: 'max-width', value: 640 }],
|
|
77
|
+
* lg: [{ type: 'min-width', value: 1024 }],
|
|
78
|
+
* });
|
|
79
|
+
*/
|
|
5
80
|
export declare const ResponsivePlugin: {
|
|
6
|
-
install(app: App, config?:
|
|
81
|
+
install(app: App, config?: Record<string, MediaQueryConfig>): void;
|
|
7
82
|
};
|
|
8
|
-
export {};
|
package/dist/vue-responsive.js
CHANGED
|
@@ -1,36 +1,171 @@
|
|
|
1
|
-
import { inject, reactive } from '@vue/runtime-core';
|
|
1
|
+
import { inject, reactive, ref, computed, watchEffect, onUnmounted, getCurrentInstance, } from '@vue/runtime-core';
|
|
2
2
|
import { responsiveState, setResponsiveConfig } from './create-responsive';
|
|
3
|
+
import { subscribeMediaQuery } from './media-query';
|
|
4
|
+
import { createContainerState } from './container-state';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Shared Vue reactive state (singleton per Vue app)
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
3
8
|
const RESPONSIVE_KEY = Symbol('responsiveState');
|
|
4
9
|
let vueReactiveState = null;
|
|
10
|
+
function ensureVueState() {
|
|
11
|
+
if (vueReactiveState)
|
|
12
|
+
return vueReactiveState;
|
|
13
|
+
vueReactiveState = reactive({ ...responsiveState.getState() });
|
|
14
|
+
responsiveState.subscribe((state) => {
|
|
15
|
+
Object.keys(vueReactiveState).forEach(key => {
|
|
16
|
+
if (!(key in state))
|
|
17
|
+
delete vueReactiveState[key];
|
|
18
|
+
});
|
|
19
|
+
Object.assign(vueReactiveState, state);
|
|
20
|
+
});
|
|
21
|
+
return vueReactiveState;
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// useResponsive
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Returns the reactive responsive state.
|
|
28
|
+
* Generic `T` narrows the type for custom configs.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* type MyState = { sm: boolean; lg: boolean };
|
|
32
|
+
* const state = useResponsive<MyState>();
|
|
33
|
+
*/
|
|
5
34
|
export function useResponsive() {
|
|
6
|
-
const injected = inject(RESPONSIVE_KEY);
|
|
7
|
-
if (injected)
|
|
35
|
+
const injected = inject(RESPONSIVE_KEY, null);
|
|
36
|
+
if (injected !== null)
|
|
8
37
|
return injected;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
38
|
+
return ensureVueState();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns ordered breakpoint helpers that are **reactive in Vue templates
|
|
42
|
+
* and computed properties** — they read from the Vue reactive state and
|
|
43
|
+
* re-evaluate automatically on viewport changes.
|
|
44
|
+
*
|
|
45
|
+
* Requires a breakpoint `order` to be set via `setConfig` or `createResponsiveState`.
|
|
46
|
+
* Falls back to config key insertion order if none is provided.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* const { current, isAbove, isBelow, between } = useBreakpoints();
|
|
50
|
+
*
|
|
51
|
+
* // In template:
|
|
52
|
+
* // <DesktopNav v-if="isAbove('sm')" />
|
|
53
|
+
* // <span>{{ current }}</span>
|
|
54
|
+
*/
|
|
55
|
+
export function useBreakpoints() {
|
|
56
|
+
const state = ensureVueState();
|
|
57
|
+
// Reads from Vue reactive state → Vue tracks these as dependencies
|
|
58
|
+
function getCurrent() {
|
|
59
|
+
var _a;
|
|
60
|
+
const order = responsiveState.getOrder();
|
|
61
|
+
const keys = order.length ? order : Object.keys(state);
|
|
62
|
+
return (_a = keys.find(k => state[k])) !== null && _a !== void 0 ? _a : null;
|
|
17
63
|
}
|
|
18
|
-
|
|
64
|
+
function getOrder() {
|
|
65
|
+
const order = responsiveState.getOrder();
|
|
66
|
+
return order.length ? order : Object.keys(state);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
current: computed(getCurrent),
|
|
70
|
+
isAbove(key) {
|
|
71
|
+
const ord = getOrder();
|
|
72
|
+
const cur = getCurrent();
|
|
73
|
+
return ord.indexOf(cur !== null && cur !== void 0 ? cur : '') > ord.indexOf(key);
|
|
74
|
+
},
|
|
75
|
+
isBelow(key) {
|
|
76
|
+
const ord = getOrder();
|
|
77
|
+
const cur = getCurrent();
|
|
78
|
+
const curIdx = ord.indexOf(cur !== null && cur !== void 0 ? cur : '');
|
|
79
|
+
const keyIdx = ord.indexOf(key);
|
|
80
|
+
return curIdx !== -1 && curIdx < keyIdx;
|
|
81
|
+
},
|
|
82
|
+
between(from, to) {
|
|
83
|
+
var _a;
|
|
84
|
+
const ord = getOrder();
|
|
85
|
+
const idx = ord.indexOf((_a = getCurrent()) !== null && _a !== void 0 ? _a : '');
|
|
86
|
+
return idx !== -1 && idx >= ord.indexOf(from) && idx <= ord.indexOf(to);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// useMediaQuery
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
/**
|
|
94
|
+
* Reactive composable for a single raw CSS media query string.
|
|
95
|
+
* Returns a `Ref<boolean>`. Cleans up automatically on `onUnmounted`.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* const isDark = useMediaQuery('(prefers-color-scheme: dark)');
|
|
99
|
+
* const canHover = useMediaQuery('(hover: hover)');
|
|
100
|
+
*/
|
|
101
|
+
export function useMediaQuery(query) {
|
|
102
|
+
const matches = ref(false);
|
|
103
|
+
const off = subscribeMediaQuery(query, (v) => { matches.value = v; });
|
|
104
|
+
if (getCurrentInstance())
|
|
105
|
+
onUnmounted(off);
|
|
106
|
+
return matches;
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// useContainerState
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
/**
|
|
112
|
+
* Reactive composable that tracks an element's dimensions with `ResizeObserver`
|
|
113
|
+
* and evaluates breakpoint conditions in JavaScript (Container Queries).
|
|
114
|
+
*
|
|
115
|
+
* Accepts a `Ref<Element | null>` (e.g. from `useTemplateRef`) — automatically
|
|
116
|
+
* sets up and tears down the observer as the element mounts / unmounts.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* <script setup>
|
|
120
|
+
* const cardRef = useTemplateRef('card');
|
|
121
|
+
* const cardState = useContainerState(cardRef, {
|
|
122
|
+
* compact: [{ type: 'max-width', value: 300 }],
|
|
123
|
+
* wide: [{ type: 'min-width', value: 600 }],
|
|
124
|
+
* });
|
|
125
|
+
* </script>
|
|
126
|
+
*
|
|
127
|
+
* <template>
|
|
128
|
+
* <div ref="card">
|
|
129
|
+
* <CompactLayout v-if="cardState.compact" />
|
|
130
|
+
* <WideLayout v-else-if="cardState.wide" />
|
|
131
|
+
* </div>
|
|
132
|
+
* </template>
|
|
133
|
+
*/
|
|
134
|
+
export function useContainerState(elementRef, config, options) {
|
|
135
|
+
const state = reactive({});
|
|
136
|
+
watchEffect((cleanup) => {
|
|
137
|
+
const el = elementRef.value;
|
|
138
|
+
if (!el)
|
|
139
|
+
return;
|
|
140
|
+
const cs = createContainerState(el, config, options);
|
|
141
|
+
const off = cs.subscribe((s) => {
|
|
142
|
+
Object.keys(state).forEach(k => { if (!(k in s))
|
|
143
|
+
delete state[k]; });
|
|
144
|
+
Object.assign(state, s);
|
|
145
|
+
});
|
|
146
|
+
cleanup(() => {
|
|
147
|
+
off();
|
|
148
|
+
cs.destroy();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
return state;
|
|
19
152
|
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// ResponsivePlugin
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
/**
|
|
157
|
+
* Vue plugin that registers the responsive state via `provide`.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* app.use(ResponsivePlugin, {
|
|
161
|
+
* sm: [{ type: 'max-width', value: 640 }],
|
|
162
|
+
* lg: [{ type: 'min-width', value: 1024 }],
|
|
163
|
+
* });
|
|
164
|
+
*/
|
|
20
165
|
export const ResponsivePlugin = {
|
|
21
166
|
install(app, config) {
|
|
22
|
-
if (config)
|
|
167
|
+
if (config)
|
|
23
168
|
setResponsiveConfig(config);
|
|
24
|
-
|
|
25
|
-
if (!vueReactiveState) {
|
|
26
|
-
vueReactiveState = reactive({ ...responsiveState.proxy });
|
|
27
|
-
responsiveState.subscribe((state) => {
|
|
28
|
-
Object.keys(state).forEach(key => {
|
|
29
|
-
// @ts-ignore
|
|
30
|
-
vueReactiveState[key] = state[key];
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
app.provide(RESPONSIVE_KEY, vueReactiveState);
|
|
169
|
+
app.provide(RESPONSIVE_KEY, ensureVueState());
|
|
35
170
|
},
|
|
36
171
|
};
|
package/package.json
CHANGED
|
@@ -1,49 +1,82 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "responsive-media",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "A utility for reactive state based on CSS media queries. Includes integration with Vue 3 (Composition API)
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "responsive-media",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "A utility for reactive state based on CSS media queries. Includes integration with Vue 3 (Composition API) and React 18+.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./react": {
|
|
14
|
+
"types": "./dist/react-responsive.d.ts",
|
|
15
|
+
"import": "./dist/react-responsive.js",
|
|
16
|
+
"require": "./dist/react-responsive.js"
|
|
17
|
+
},
|
|
18
|
+
"./presets": {
|
|
19
|
+
"types": "./dist/presets.d.ts",
|
|
20
|
+
"import": "./dist/presets.js",
|
|
21
|
+
"require": "./dist/presets.js"
|
|
22
|
+
},
|
|
23
|
+
"./container": {
|
|
24
|
+
"types": "./dist/container-state.d.ts",
|
|
25
|
+
"import": "./dist/container-state.js",
|
|
26
|
+
"require": "./dist/container-state.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"prepublishOnly": "npm run build",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test:coverage": "vitest run --coverage"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/macrulezru/responsive-media.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://macrulez.ru/#/en",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/macrulezru/responsive-media/issues"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"responsive",
|
|
49
|
+
"media-query",
|
|
50
|
+
"typescript",
|
|
51
|
+
"reactive",
|
|
52
|
+
"utility",
|
|
53
|
+
"vue",
|
|
54
|
+
"react"
|
|
55
|
+
],
|
|
56
|
+
"author": "",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/react": "^18.3.0",
|
|
60
|
+
"@types/vue": "^1.0.31",
|
|
61
|
+
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
62
|
+
"@typescript-eslint/parser": "^8.53.0",
|
|
63
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
64
|
+
"eslint": "^9.39.2",
|
|
65
|
+
"eslint-plugin-vue": "^10.7.0",
|
|
66
|
+
"jsdom": "^26.0.0",
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"vitest": "^3.0.0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"react": "^18.0.0",
|
|
72
|
+
"vue": "^3.5.27"
|
|
73
|
+
},
|
|
74
|
+
"peerDependenciesMeta": {
|
|
75
|
+
"vue": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"react": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|