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,107 @@
|
|
|
1
|
+
import { BaseResponsiveState } from './base-state';
|
|
2
|
+
import { buildMediaQuery } from './create-responsive';
|
|
3
|
+
import { hasResizeObserver } from './utils';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Condition evaluator (JS-side, no matchMedia)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
function evaluateSingle(cond, w, h) {
|
|
8
|
+
const val = typeof cond.value === 'number' ? cond.value : parseFloat(String(cond.value));
|
|
9
|
+
switch (cond.type) {
|
|
10
|
+
case 'max-width': return w <= val;
|
|
11
|
+
case 'min-width': return w >= val;
|
|
12
|
+
case 'max-height': return h <= val;
|
|
13
|
+
case 'min-height': return h >= val;
|
|
14
|
+
case 'orientation': return cond.value === 'landscape' ? w > h : w <= h;
|
|
15
|
+
case 'aspect-ratio': {
|
|
16
|
+
const [rw, rh] = String(cond.value).split('/').map(Number);
|
|
17
|
+
return rh ? Math.abs(w / h - rw / rh) < 0.01 : false;
|
|
18
|
+
}
|
|
19
|
+
default: return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function evaluateConditions(conditions, w, h) {
|
|
23
|
+
if (Array.isArray(conditions[0])) {
|
|
24
|
+
// OR mode — any group must fully match
|
|
25
|
+
return conditions.some(group => group.every(c => evaluateSingle(c, w, h)));
|
|
26
|
+
}
|
|
27
|
+
// AND mode — all conditions must match
|
|
28
|
+
return conditions.every(c => evaluateSingle(c, w, h));
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// ContainerState — ResizeObserver-based
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
export class ContainerState extends BaseResponsiveState {
|
|
34
|
+
constructor(element, config, options) {
|
|
35
|
+
super();
|
|
36
|
+
this.observer = null;
|
|
37
|
+
this.configSnapshot = {};
|
|
38
|
+
this.element = element;
|
|
39
|
+
if ((options === null || options === void 0 ? void 0 : options.debounce) !== undefined)
|
|
40
|
+
this.debounceMs = options.debounce;
|
|
41
|
+
if ((options === null || options === void 0 ? void 0 : options.order) !== undefined)
|
|
42
|
+
this.order = options.order;
|
|
43
|
+
this.applyConfig(config);
|
|
44
|
+
}
|
|
45
|
+
setupSources(config) {
|
|
46
|
+
this.configSnapshot = config;
|
|
47
|
+
// Generate CSS @container-compatible strings for each breakpoint
|
|
48
|
+
Object.entries(config).forEach(([key, conditions]) => {
|
|
49
|
+
this.mediaQueries[key] = buildMediaQuery(conditions);
|
|
50
|
+
});
|
|
51
|
+
if (!hasResizeObserver()) {
|
|
52
|
+
// SSR or unsupported — default to false
|
|
53
|
+
Object.keys(config).forEach(key => { this.state[key] = false; });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Synchronous initial evaluation using the element's current size
|
|
57
|
+
const rect = this.element.getBoundingClientRect();
|
|
58
|
+
Object.entries(config).forEach(([key, conditions]) => {
|
|
59
|
+
this.state[key] = evaluateConditions(conditions, rect.width, rect.height);
|
|
60
|
+
});
|
|
61
|
+
this.observer = new ResizeObserver((entries) => {
|
|
62
|
+
const { width, height } = entries[0].contentRect;
|
|
63
|
+
Object.entries(this.configSnapshot).forEach(([key, conditions]) => {
|
|
64
|
+
this.proxy[key] = evaluateConditions(conditions, width, height);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
this.observer.observe(this.element);
|
|
68
|
+
}
|
|
69
|
+
cleanupSources() {
|
|
70
|
+
if (this.observer) {
|
|
71
|
+
this.observer.disconnect();
|
|
72
|
+
this.observer = null;
|
|
73
|
+
}
|
|
74
|
+
this.configSnapshot = {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Factory
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/**
|
|
81
|
+
* Creates a `ContainerState` that tracks an element's dimensions via
|
|
82
|
+
* `ResizeObserver` and evaluates breakpoint conditions in JavaScript.
|
|
83
|
+
*
|
|
84
|
+
* The API is identical to `ReactiveResponsiveState` — all subscription
|
|
85
|
+
* methods (`subscribe`, `on`, `onEnter`, `onLeave`, `waitFor`, etc.) work
|
|
86
|
+
* the same way.
|
|
87
|
+
*
|
|
88
|
+
* The `getMediaQueries()` method returns CSS `@container` compatible strings
|
|
89
|
+
* that can be used in stylesheets alongside the JS reactive state.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const card = document.querySelector('.card')!;
|
|
93
|
+
*
|
|
94
|
+
* const cardState = createContainerState(card, {
|
|
95
|
+
* compact: [{ type: 'max-width', value: 300 }],
|
|
96
|
+
* wide: [{ type: 'min-width', value: 600 }],
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* cardState.on('compact', (v) => card.classList.toggle('card--compact', v));
|
|
100
|
+
* cardState.syncCSSVars({ prefix: '--card-' });
|
|
101
|
+
*
|
|
102
|
+
* // Cleanup when no longer needed:
|
|
103
|
+
* cardState.destroy();
|
|
104
|
+
*/
|
|
105
|
+
export function createContainerState(element, config, options) {
|
|
106
|
+
return new ContainerState(element, config, options);
|
|
107
|
+
}
|
|
@@ -1,20 +1,49 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
declare
|
|
6
|
-
|
|
7
|
-
private listeners;
|
|
8
|
-
proxy: ResponsiveState;
|
|
1
|
+
import { type MediaQueryConfig } from './responsive.enum';
|
|
2
|
+
import { BaseResponsiveState, type ResponsiveState, type SetConfigOptions } from './base-state';
|
|
3
|
+
export type { MediaQueryConfig, ResponsiveState, SetConfigOptions };
|
|
4
|
+
export { BaseResponsiveState };
|
|
5
|
+
export declare function buildMediaQuery(conditions: MediaQueryConfig): string;
|
|
6
|
+
export declare class ReactiveResponsiveState extends BaseResponsiveState {
|
|
9
7
|
private queries;
|
|
10
|
-
private
|
|
11
|
-
constructor();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
setConfig(config: Record<string, MediaQueryConfig>): void;
|
|
15
|
-
subscribe(listener: ResponsiveListener): () => boolean;
|
|
16
|
-
private notify;
|
|
8
|
+
private handlers;
|
|
9
|
+
constructor(config?: Record<string, MediaQueryConfig>, options?: SetConfigOptions);
|
|
10
|
+
protected setupSources(config: Record<string, MediaQueryConfig>): void;
|
|
11
|
+
protected cleanupSources(): void;
|
|
17
12
|
}
|
|
18
|
-
export declare function getResponsiveMediaQueries(): Record<string, string>;
|
|
19
13
|
export declare const responsiveState: ReactiveResponsiveState;
|
|
20
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Creates an isolated `ReactiveResponsiveState` instance — useful for tests,
|
|
16
|
+
* SSR (per-request instances), or multiple independent responsive contexts.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const layoutState = createResponsiveState(TailwindPreset, {
|
|
20
|
+
* order: ['xs', 'sm', 'md', 'lg', 'xl', '2xl'],
|
|
21
|
+
* });
|
|
22
|
+
* const themeState = createResponsiveState(AccessibilityPreset);
|
|
23
|
+
*/
|
|
24
|
+
export declare function createResponsiveState(config?: Record<string, MediaQueryConfig>, options?: SetConfigOptions): ReactiveResponsiveState;
|
|
25
|
+
/**
|
|
26
|
+
* Converts a `MediaQueryConfig` array to a CSS media query string.
|
|
27
|
+
* Useful for CSS-in-JS, `@container` rules, or debugging.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* toMediaQueryString([{ type: 'min-width', value: 768 }, { type: 'max-width', value: 1024 }])
|
|
31
|
+
* // → "(min-width: 768px) and (max-width: 1024px)"
|
|
32
|
+
*
|
|
33
|
+
* toMediaQueryString([[{ type: 'max-width', value: 600 }], [{ type: 'orientation', value: 'portrait' }]])
|
|
34
|
+
* // → "(max-width: 600px), (orientation: portrait)"
|
|
35
|
+
*/
|
|
36
|
+
export declare function toMediaQueryString(conditions: MediaQueryConfig): string;
|
|
37
|
+
export declare function getResponsiveState<T extends Record<string, boolean> = ResponsiveState>(): T;
|
|
38
|
+
export declare function getResponsiveMediaQueries(): Record<string, string>;
|
|
39
|
+
export declare function setResponsiveConfig(config: Record<string, MediaQueryConfig>, options?: SetConfigOptions): void;
|
|
40
|
+
/**
|
|
41
|
+
* Returns the first value in `map` whose key is `true` in `state`,
|
|
42
|
+
* or `fallback` if none match. Priority follows `map` insertion order.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* const cols = match(state, { mobile: 1, tablet: 2, desktop: 4 });
|
|
46
|
+
* const View = match(state, { mobile: MobileMenu, desktop: DesktopNav });
|
|
47
|
+
* const label = match(state, { sm: 'Compact', lg: 'Full' }, 'Default');
|
|
48
|
+
*/
|
|
49
|
+
export declare function match<T>(state: Record<string, boolean>, map: Record<string, T>, fallback?: T): T | undefined;
|
|
@@ -1,69 +1,132 @@
|
|
|
1
1
|
import { ResponsiveConfig } from './responsive.enum';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
},
|
|
16
|
-
get: (target, prop) => {
|
|
17
|
-
return target[prop];
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
this.applyConfig(ResponsiveConfig);
|
|
2
|
+
import { BaseResponsiveState } from './base-state';
|
|
3
|
+
import { hasMatchMedia } from './utils';
|
|
4
|
+
export { BaseResponsiveState };
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Media query string builder (also used by ContainerState for @container strings)
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const PIXEL_TYPES = new Set([
|
|
9
|
+
'width', 'min-width', 'max-width',
|
|
10
|
+
'height', 'min-height', 'max-height',
|
|
11
|
+
]);
|
|
12
|
+
function formatValue(cond) {
|
|
13
|
+
if (typeof cond.value === 'number' && PIXEL_TYPES.has(cond.type)) {
|
|
14
|
+
return `${cond.value}px`;
|
|
21
15
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
return String(cond.value);
|
|
17
|
+
}
|
|
18
|
+
function conditionToString(cond) {
|
|
19
|
+
// 'raw' inserts the value verbatim (e.g. 'print', 'screen')
|
|
20
|
+
return cond.type === 'raw' ? String(cond.value) : `(${cond.type}: ${formatValue(cond)})`;
|
|
21
|
+
}
|
|
22
|
+
export function buildMediaQuery(conditions) {
|
|
23
|
+
if (conditions.length === 0)
|
|
24
|
+
return '';
|
|
25
|
+
if (Array.isArray(conditions[0])) {
|
|
26
|
+
return conditions
|
|
27
|
+
.map(group => group.map(conditionToString).join(' and '))
|
|
28
|
+
.join(', ');
|
|
29
|
+
}
|
|
30
|
+
return conditions.map(conditionToString).join(' and ');
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// ReactiveResponsiveState — viewport-based (matchMedia)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
export class ReactiveResponsiveState extends BaseResponsiveState {
|
|
36
|
+
constructor(config, options) {
|
|
37
|
+
super();
|
|
26
38
|
this.queries = {};
|
|
27
|
-
this.
|
|
28
|
-
|
|
39
|
+
this.handlers = {};
|
|
40
|
+
if ((options === null || options === void 0 ? void 0 : options.debounce) !== undefined)
|
|
41
|
+
this.debounceMs = options.debounce;
|
|
42
|
+
if ((options === null || options === void 0 ? void 0 : options.order) !== undefined)
|
|
43
|
+
this.order = options.order;
|
|
44
|
+
this.applyConfig(config !== null && config !== void 0 ? config : ResponsiveConfig);
|
|
45
|
+
}
|
|
46
|
+
setupSources(config) {
|
|
29
47
|
Object.entries(config).forEach(([key, conditions]) => {
|
|
30
|
-
const mq = conditions
|
|
31
|
-
.map(cond => {
|
|
32
|
-
const val = typeof cond.value === 'number' && !String(cond.type).includes('aspect-ratio')
|
|
33
|
-
? cond.value + 'px'
|
|
34
|
-
: cond.value;
|
|
35
|
-
return `(${cond.type}: ${val})`;
|
|
36
|
-
})
|
|
37
|
-
.join(' and ');
|
|
48
|
+
const mq = buildMediaQuery(conditions);
|
|
38
49
|
this.mediaQueries[key] = mq;
|
|
50
|
+
if (!hasMatchMedia()) {
|
|
51
|
+
this.state[key] = false;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
39
54
|
const query = window.matchMedia(mq);
|
|
40
55
|
this.queries[key] = query;
|
|
41
|
-
this.proxy[key] =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
const handler = (e) => { this.proxy[key] = e.matches; };
|
|
57
|
+
this.handlers[key] = handler;
|
|
58
|
+
query.addEventListener('change', handler);
|
|
59
|
+
this.state[key] = query.matches;
|
|
45
60
|
});
|
|
46
|
-
this.notify();
|
|
47
|
-
}
|
|
48
|
-
getMediaQueries() {
|
|
49
|
-
return { ...this.mediaQueries };
|
|
50
|
-
}
|
|
51
|
-
setConfig(config) {
|
|
52
|
-
this.applyConfig(config);
|
|
53
61
|
}
|
|
54
|
-
|
|
55
|
-
this.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.
|
|
62
|
+
cleanupSources() {
|
|
63
|
+
Object.entries(this.handlers).forEach(([key, handler]) => {
|
|
64
|
+
var _a;
|
|
65
|
+
(_a = this.queries[key]) === null || _a === void 0 ? void 0 : _a.removeEventListener('change', handler);
|
|
66
|
+
});
|
|
67
|
+
this.queries = {};
|
|
68
|
+
this.handlers = {};
|
|
61
69
|
}
|
|
62
70
|
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Singleton
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
export const responsiveState = new ReactiveResponsiveState();
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Public factory
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
/**
|
|
79
|
+
* Creates an isolated `ReactiveResponsiveState` instance — useful for tests,
|
|
80
|
+
* SSR (per-request instances), or multiple independent responsive contexts.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* const layoutState = createResponsiveState(TailwindPreset, {
|
|
84
|
+
* order: ['xs', 'sm', 'md', 'lg', 'xl', '2xl'],
|
|
85
|
+
* });
|
|
86
|
+
* const themeState = createResponsiveState(AccessibilityPreset);
|
|
87
|
+
*/
|
|
88
|
+
export function createResponsiveState(config, options) {
|
|
89
|
+
return new ReactiveResponsiveState(config, options);
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Standalone helpers
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
/**
|
|
95
|
+
* Converts a `MediaQueryConfig` array to a CSS media query string.
|
|
96
|
+
* Useful for CSS-in-JS, `@container` rules, or debugging.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* toMediaQueryString([{ type: 'min-width', value: 768 }, { type: 'max-width', value: 1024 }])
|
|
100
|
+
* // → "(min-width: 768px) and (max-width: 1024px)"
|
|
101
|
+
*
|
|
102
|
+
* toMediaQueryString([[{ type: 'max-width', value: 600 }], [{ type: 'orientation', value: 'portrait' }]])
|
|
103
|
+
* // → "(max-width: 600px), (orientation: portrait)"
|
|
104
|
+
*/
|
|
105
|
+
export function toMediaQueryString(conditions) {
|
|
106
|
+
return buildMediaQuery(conditions);
|
|
107
|
+
}
|
|
108
|
+
export function getResponsiveState() {
|
|
109
|
+
return responsiveState.getState();
|
|
110
|
+
}
|
|
63
111
|
export function getResponsiveMediaQueries() {
|
|
64
112
|
return responsiveState.getMediaQueries();
|
|
65
113
|
}
|
|
66
|
-
export
|
|
67
|
-
|
|
68
|
-
|
|
114
|
+
export function setResponsiveConfig(config, options) {
|
|
115
|
+
responsiveState.setConfig(config, options);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Returns the first value in `map` whose key is `true` in `state`,
|
|
119
|
+
* or `fallback` if none match. Priority follows `map` insertion order.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* const cols = match(state, { mobile: 1, tablet: 2, desktop: 4 });
|
|
123
|
+
* const View = match(state, { mobile: MobileMenu, desktop: DesktopNav });
|
|
124
|
+
* const label = match(state, { sm: 'Compact', lg: 'Full' }, 'Default');
|
|
125
|
+
*/
|
|
126
|
+
export function match(state, map, fallback) {
|
|
127
|
+
for (const [key, value] of Object.entries(map)) {
|
|
128
|
+
if (state[key])
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
return fallback;
|
|
69
132
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export * from './responsive.enum';
|
|
2
|
+
export * from './base-state';
|
|
2
3
|
export * from './create-responsive';
|
|
3
|
-
export
|
|
4
|
+
export * from './container-state';
|
|
5
|
+
export * from './media-query';
|
|
6
|
+
export * from './presets';
|
|
7
|
+
export { ResponsivePlugin, useResponsive, useMediaQuery, useBreakpoints, useContainerState } from './vue-responsive';
|
|
8
|
+
export type { BreakpointHelpers } from './vue-responsive';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export * from './responsive.enum';
|
|
2
|
+
export * from './base-state';
|
|
2
3
|
export * from './create-responsive';
|
|
3
|
-
export
|
|
4
|
+
export * from './container-state';
|
|
5
|
+
export * from './media-query';
|
|
6
|
+
export * from './presets';
|
|
7
|
+
export { ResponsivePlugin, useResponsive, useMediaQuery, useBreakpoints, useContainerState } from './vue-responsive';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level reactive wrapper around a single raw CSS media query string.
|
|
3
|
+
* Framework-agnostic — Vue and React adapters live in their own files.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Subscribes to a raw CSS media query string. The callback is called immediately
|
|
7
|
+
* with the current match state and again whenever it changes.
|
|
8
|
+
* Returns an unsubscribe / cleanup function.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const off = subscribeMediaQuery('(prefers-color-scheme: dark)', (matches) => {
|
|
12
|
+
* document.body.classList.toggle('dark', matches);
|
|
13
|
+
* });
|
|
14
|
+
* // Later:
|
|
15
|
+
* off();
|
|
16
|
+
*/
|
|
17
|
+
export declare function subscribeMediaQuery(query: string, callback: (matches: boolean) => void): () => void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level reactive wrapper around a single raw CSS media query string.
|
|
3
|
+
* Framework-agnostic — Vue and React adapters live in their own files.
|
|
4
|
+
*/
|
|
5
|
+
function isSSR() {
|
|
6
|
+
return typeof window === 'undefined' || typeof window.matchMedia !== 'function';
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Subscribes to a raw CSS media query string. The callback is called immediately
|
|
10
|
+
* with the current match state and again whenever it changes.
|
|
11
|
+
* Returns an unsubscribe / cleanup function.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const off = subscribeMediaQuery('(prefers-color-scheme: dark)', (matches) => {
|
|
15
|
+
* document.body.classList.toggle('dark', matches);
|
|
16
|
+
* });
|
|
17
|
+
* // Later:
|
|
18
|
+
* off();
|
|
19
|
+
*/
|
|
20
|
+
export function subscribeMediaQuery(query, callback) {
|
|
21
|
+
if (isSSR()) {
|
|
22
|
+
callback(false);
|
|
23
|
+
return () => { };
|
|
24
|
+
}
|
|
25
|
+
const mql = window.matchMedia(query);
|
|
26
|
+
const handler = (e) => callback(e.matches);
|
|
27
|
+
mql.addEventListener('change', handler);
|
|
28
|
+
callback(mql.matches);
|
|
29
|
+
return () => mql.removeEventListener('change', handler);
|
|
30
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { MediaQueryConfig } from './responsive.enum';
|
|
2
|
+
/**
|
|
3
|
+
* Tailwind CSS v3/v4 breakpoints — mutually exclusive ranges.
|
|
4
|
+
*
|
|
5
|
+
* | Key | Range |
|
|
6
|
+
* |------|--------------------|
|
|
7
|
+
* | xs | ≤ 639px |
|
|
8
|
+
* | sm | 640 – 767px |
|
|
9
|
+
* | md | 768 – 1023px |
|
|
10
|
+
* | lg | 1024 – 1279px |
|
|
11
|
+
* | xl | 1280 – 1535px |
|
|
12
|
+
* | 2xl | ≥ 1536px |
|
|
13
|
+
*/
|
|
14
|
+
export declare const TailwindPreset: Record<string, MediaQueryConfig>;
|
|
15
|
+
/** Ordered key array matching `TailwindPreset` — pass as `order` option. */
|
|
16
|
+
export declare const TailwindOrder: readonly ["xs", "sm", "md", "lg", "xl", "2xl"];
|
|
17
|
+
/**
|
|
18
|
+
* Bootstrap 5 breakpoints — mutually exclusive ranges.
|
|
19
|
+
*
|
|
20
|
+
* | Key | Range |
|
|
21
|
+
* |------|--------------------|
|
|
22
|
+
* | xs | ≤ 575px |
|
|
23
|
+
* | sm | 576 – 767px |
|
|
24
|
+
* | md | 768 – 991px |
|
|
25
|
+
* | lg | 992 – 1199px |
|
|
26
|
+
* | xl | 1200 – 1399px |
|
|
27
|
+
* | xxl | ≥ 1400px |
|
|
28
|
+
*/
|
|
29
|
+
export declare const BootstrapPreset: Record<string, MediaQueryConfig>;
|
|
30
|
+
/** Ordered key array matching `BootstrapPreset` — pass as `order` option. */
|
|
31
|
+
export declare const BootstrapOrder: readonly ["xs", "sm", "md", "lg", "xl", "xxl"];
|
|
32
|
+
/**
|
|
33
|
+
* Accessibility & user-preference media queries.
|
|
34
|
+
* Keys are mutually independent — multiple can be `true` at the same time.
|
|
35
|
+
*
|
|
36
|
+
* | Key | Matches when … |
|
|
37
|
+
* |----------------|---------------------------------------------|
|
|
38
|
+
* | dark | `prefers-color-scheme: dark` |
|
|
39
|
+
* | light | `prefers-color-scheme: light` |
|
|
40
|
+
* | reducedMotion | `prefers-reduced-motion: reduce` |
|
|
41
|
+
* | highContrast | `prefers-contrast: more` |
|
|
42
|
+
* | lowContrast | `prefers-contrast: less` |
|
|
43
|
+
* | noHover | `hover: none` (touch / stylus devices) |
|
|
44
|
+
* | coarsePointer | `pointer: coarse` (finger-sized input) |
|
|
45
|
+
* | forcedColors | `forced-colors: active` (Windows HCM) |
|
|
46
|
+
* | print | `print` media type |
|
|
47
|
+
*/
|
|
48
|
+
export declare const AccessibilityPreset: Record<string, MediaQueryConfig>;
|
package/dist/presets.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind CSS v3/v4 breakpoints — mutually exclusive ranges.
|
|
3
|
+
*
|
|
4
|
+
* | Key | Range |
|
|
5
|
+
* |------|--------------------|
|
|
6
|
+
* | xs | ≤ 639px |
|
|
7
|
+
* | sm | 640 – 767px |
|
|
8
|
+
* | md | 768 – 1023px |
|
|
9
|
+
* | lg | 1024 – 1279px |
|
|
10
|
+
* | xl | 1280 – 1535px |
|
|
11
|
+
* | 2xl | ≥ 1536px |
|
|
12
|
+
*/
|
|
13
|
+
export const TailwindPreset = {
|
|
14
|
+
xs: [{ type: 'max-width', value: 639 }],
|
|
15
|
+
sm: [{ type: 'min-width', value: 640 }, { type: 'max-width', value: 767 }],
|
|
16
|
+
md: [{ type: 'min-width', value: 768 }, { type: 'max-width', value: 1023 }],
|
|
17
|
+
lg: [{ type: 'min-width', value: 1024 }, { type: 'max-width', value: 1279 }],
|
|
18
|
+
xl: [{ type: 'min-width', value: 1280 }, { type: 'max-width', value: 1535 }],
|
|
19
|
+
'2xl': [{ type: 'min-width', value: 1536 }],
|
|
20
|
+
};
|
|
21
|
+
/** Ordered key array matching `TailwindPreset` — pass as `order` option. */
|
|
22
|
+
export const TailwindOrder = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
|
|
23
|
+
/**
|
|
24
|
+
* Bootstrap 5 breakpoints — mutually exclusive ranges.
|
|
25
|
+
*
|
|
26
|
+
* | Key | Range |
|
|
27
|
+
* |------|--------------------|
|
|
28
|
+
* | xs | ≤ 575px |
|
|
29
|
+
* | sm | 576 – 767px |
|
|
30
|
+
* | md | 768 – 991px |
|
|
31
|
+
* | lg | 992 – 1199px |
|
|
32
|
+
* | xl | 1200 – 1399px |
|
|
33
|
+
* | xxl | ≥ 1400px |
|
|
34
|
+
*/
|
|
35
|
+
export const BootstrapPreset = {
|
|
36
|
+
xs: [{ type: 'max-width', value: 575 }],
|
|
37
|
+
sm: [{ type: 'min-width', value: 576 }, { type: 'max-width', value: 767 }],
|
|
38
|
+
md: [{ type: 'min-width', value: 768 }, { type: 'max-width', value: 991 }],
|
|
39
|
+
lg: [{ type: 'min-width', value: 992 }, { type: 'max-width', value: 1199 }],
|
|
40
|
+
xl: [{ type: 'min-width', value: 1200 }, { type: 'max-width', value: 1399 }],
|
|
41
|
+
xxl: [{ type: 'min-width', value: 1400 }],
|
|
42
|
+
};
|
|
43
|
+
/** Ordered key array matching `BootstrapPreset` — pass as `order` option. */
|
|
44
|
+
export const BootstrapOrder = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
|
|
45
|
+
/**
|
|
46
|
+
* Accessibility & user-preference media queries.
|
|
47
|
+
* Keys are mutually independent — multiple can be `true` at the same time.
|
|
48
|
+
*
|
|
49
|
+
* | Key | Matches when … |
|
|
50
|
+
* |----------------|---------------------------------------------|
|
|
51
|
+
* | dark | `prefers-color-scheme: dark` |
|
|
52
|
+
* | light | `prefers-color-scheme: light` |
|
|
53
|
+
* | reducedMotion | `prefers-reduced-motion: reduce` |
|
|
54
|
+
* | highContrast | `prefers-contrast: more` |
|
|
55
|
+
* | lowContrast | `prefers-contrast: less` |
|
|
56
|
+
* | noHover | `hover: none` (touch / stylus devices) |
|
|
57
|
+
* | coarsePointer | `pointer: coarse` (finger-sized input) |
|
|
58
|
+
* | forcedColors | `forced-colors: active` (Windows HCM) |
|
|
59
|
+
* | print | `print` media type |
|
|
60
|
+
*/
|
|
61
|
+
export const AccessibilityPreset = {
|
|
62
|
+
dark: [{ type: 'prefers-color-scheme', value: 'dark' }],
|
|
63
|
+
light: [{ type: 'prefers-color-scheme', value: 'light' }],
|
|
64
|
+
reducedMotion: [{ type: 'prefers-reduced-motion', value: 'reduce' }],
|
|
65
|
+
highContrast: [{ type: 'prefers-contrast', value: 'more' }],
|
|
66
|
+
lowContrast: [{ type: 'prefers-contrast', value: 'less' }],
|
|
67
|
+
noHover: [{ type: 'hover', value: 'none' }],
|
|
68
|
+
coarsePointer: [{ type: 'pointer', value: 'coarse' }],
|
|
69
|
+
forcedColors: [{ type: 'forced-colors', value: 'active' }],
|
|
70
|
+
print: [{ type: 'raw', value: 'print' }],
|
|
71
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
import type { MediaQueryConfig, ResponsiveState, SetConfigOptions } from './create-responsive';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the current responsive state. Re-renders only when state changes.
|
|
5
|
+
* Requires React 18+. Generic `T` narrows the type for custom configs.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* type MyState = { sm: boolean; lg: boolean };
|
|
9
|
+
* const { sm, lg } = useResponsive<MyState>();
|
|
10
|
+
*/
|
|
11
|
+
export declare function useResponsive<T extends Record<string, boolean> = ResponsiveState>(): T;
|
|
12
|
+
export interface BreakpointHelpers {
|
|
13
|
+
/** First active breakpoint key, or `null`. */
|
|
14
|
+
current: string | null;
|
|
15
|
+
/** `true` when the current breakpoint is after `key` in the order. */
|
|
16
|
+
isAbove: (key: string) => boolean;
|
|
17
|
+
/** `true` when the current breakpoint is before `key` in the order. */
|
|
18
|
+
isBelow: (key: string) => boolean;
|
|
19
|
+
/** `true` when the current breakpoint is between `from` and `to` (inclusive). */
|
|
20
|
+
between: (from: string, to: string) => boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns ordered breakpoint helpers. Re-renders when the responsive state
|
|
24
|
+
* changes, so all helpers always reflect the current breakpoint.
|
|
25
|
+
*
|
|
26
|
+
* Requires a breakpoint `order` set via `setResponsiveConfig` or
|
|
27
|
+
* `createResponsiveState`. Falls back to config key insertion order.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const { current, isAbove, isBelow, between } = useBreakpoints();
|
|
31
|
+
* return isAbove('sm') ? <DesktopNav /> : <MobileNav />;
|
|
32
|
+
*/
|
|
33
|
+
export declare function useBreakpoints(): BreakpointHelpers;
|
|
34
|
+
/**
|
|
35
|
+
* Returns a boolean that tracks a raw CSS media query string.
|
|
36
|
+
* Compatible with React 18+ SSR (returns `false` on the server).
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const isDark = useMediaQuery('(prefers-color-scheme: dark)');
|
|
40
|
+
* const canHover = useMediaQuery('(hover: hover)');
|
|
41
|
+
*/
|
|
42
|
+
export declare function useMediaQuery(query: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Tracks an element's dimensions with `ResizeObserver` and evaluates
|
|
45
|
+
* breakpoint conditions in JavaScript (Container Queries).
|
|
46
|
+
*
|
|
47
|
+
* Pass a `ref` created with `useRef<Element>(null)` — the hook sets up the
|
|
48
|
+
* observer after mount and cleans up on unmount.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* function Card() {
|
|
52
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
53
|
+
* const { compact, wide } = useContainerState(ref, {
|
|
54
|
+
* compact: [{ type: 'max-width', value: 300 }],
|
|
55
|
+
* wide: [{ type: 'min-width', value: 600 }],
|
|
56
|
+
* });
|
|
57
|
+
* return (
|
|
58
|
+
* <div ref={ref}>
|
|
59
|
+
* {compact ? <CompactLayout /> : wide ? <WideLayout /> : <DefaultLayout />}
|
|
60
|
+
* </div>
|
|
61
|
+
* );
|
|
62
|
+
* }
|
|
63
|
+
*/
|
|
64
|
+
export declare function useContainerState(ref: RefObject<Element | null>, config: Record<string, MediaQueryConfig>, options?: SetConfigOptions): Record<string, boolean>;
|