create-nadvan-app 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.
@@ -0,0 +1,243 @@
1
+ // =============================================================================
2
+ // MIXINS — переиспользуемые блоки стилей
3
+ // Требует: _variables.scss
4
+ // =============================================================================
5
+
6
+ // ─── Утилиты ──────────────────────────────────────────────────────────────────
7
+
8
+ // Конвертация px → rem
9
+ @function rem($px) {
10
+ @return calc(#{$px} / 16 * 1rem);
11
+ }
12
+
13
+ // ─── Брейкпоинты ──────────────────────────────────────────────────────────────
14
+ // Использование: @include mobile { ... }
15
+
16
+ @mixin mobile {
17
+ @media (max-width: #{$bp-mobile-max}) { @content; }
18
+ }
19
+
20
+ @mixin tablet {
21
+ @media (min-width: #{$bp-tablet}) and (max-width: #{$bp-tablet-max}) { @content; }
22
+ }
23
+
24
+ @mixin tablet-up {
25
+ @media (min-width: #{$bp-tablet}) { @content; }
26
+ }
27
+
28
+ @mixin laptop {
29
+ @media (min-width: #{$bp-laptop}) and (max-width: #{$bp-laptop-max}) { @content; }
30
+ }
31
+
32
+ @mixin laptop-up {
33
+ @media (min-width: #{$bp-laptop}) { @content; }
34
+ }
35
+
36
+ @mixin desktop-m {
37
+ @media (min-width: #{$bp-desktop-m}) and (max-width: #{$bp-desktop-m-max}) { @content; }
38
+ }
39
+
40
+ @mixin desktop-m-up {
41
+ @media (min-width: #{$bp-desktop-m}) { @content; }
42
+ }
43
+
44
+ @mixin desktop-l {
45
+ @media (min-width: #{$bp-desktop-l}) and (max-width: #{$bp-desktop-l-max}) { @content; }
46
+ }
47
+
48
+ @mixin desktop-xl {
49
+ @media (min-width: #{$bp-desktop-xl}) { @content; }
50
+ }
51
+
52
+ @mixin landscape {
53
+ @media (orientation: landscape) { @content; }
54
+ }
55
+
56
+ @mixin portrait {
57
+ @media (orientation: portrait) { @content; }
58
+ }
59
+
60
+ // ─── Контейнер ────────────────────────────────────────────────────────────────
61
+ @mixin container {
62
+ width: 100%;
63
+ margin-inline: auto;
64
+ padding-inline: $grid-margin-mobile;
65
+
66
+ @include tablet-up {
67
+ padding-inline: $grid-margin-tablet;
68
+ }
69
+
70
+ @include laptop-up {
71
+ padding-inline: $grid-margin-laptop;
72
+ }
73
+
74
+ @include desktop-m-up {
75
+ max-width: $container-max-desktop-m;
76
+ padding-inline: $grid-margin-desktop-m;
77
+ }
78
+
79
+ @include desktop-l {
80
+ max-width: $container-max-desktop-l;
81
+ padding-inline: $grid-margin-desktop-l;
82
+ }
83
+ }
84
+
85
+ // ─── Типографика ──────────────────────────────────────────────────────────────
86
+ @mixin text-h1 {
87
+ font-family: $font-primary;
88
+ font-size: $font-size-h1-mobile;
89
+ font-weight: $font-weight-bold;
90
+ line-height: $line-height-heading;
91
+ letter-spacing: $letter-spacing-heading;
92
+
93
+ @include tablet-up { font-size: $font-size-h1-tablet; }
94
+ @include desktop-m-up { font-size: $font-size-h1-desktop; }
95
+ }
96
+
97
+ @mixin text-h2 {
98
+ font-family: $font-primary;
99
+ font-size: $font-size-h2-mobile;
100
+ font-weight: $font-weight-bold;
101
+ line-height: $line-height-heading;
102
+ letter-spacing: $letter-spacing-heading;
103
+
104
+ @include tablet-up { font-size: $font-size-h2-tablet; }
105
+ @include desktop-m-up { font-size: $font-size-h2-desktop; }
106
+ }
107
+
108
+ @mixin text-h3 {
109
+ font-family: $font-primary;
110
+ font-size: $font-size-h3-mobile;
111
+ font-weight: $font-weight-semibold;
112
+ line-height: $line-height-heading;
113
+
114
+ @include tablet-up { font-size: $font-size-h3-tablet; }
115
+ @include desktop-m-up { font-size: $font-size-h3-desktop; }
116
+ }
117
+
118
+ @mixin text-h4 {
119
+ font-family: $font-primary;
120
+ font-size: $font-size-h4-mobile;
121
+ font-weight: $font-weight-semibold;
122
+ line-height: $line-height-heading;
123
+
124
+ @include desktop-m-up { font-size: $font-size-h4-desktop; }
125
+ }
126
+
127
+ @mixin text-body {
128
+ font-family: $font-primary;
129
+ font-size: $font-size-body-mobile;
130
+ font-weight: $font-weight-regular;
131
+ line-height: $line-height-body;
132
+
133
+ @include desktop-m-up { font-size: $font-size-body-desktop; }
134
+ }
135
+
136
+ @mixin text-small {
137
+ font-family: $font-primary;
138
+ font-size: $font-size-small;
139
+ font-weight: $font-weight-regular;
140
+ line-height: $line-height-caption;
141
+ }
142
+
143
+ @mixin text-caption {
144
+ font-family: $font-primary;
145
+ font-size: $font-size-small;
146
+ line-height: $line-height-caption;
147
+ letter-spacing: $letter-spacing-wide;
148
+
149
+ @include desktop-m-up { font-size: $font-size-caption-desktop; }
150
+ }
151
+
152
+ // ─── Доступность ──────────────────────────────────────────────────────────────
153
+
154
+ // Визуально скрыт, но доступен для скринридеров
155
+ @mixin visually-hidden {
156
+ position: absolute;
157
+ width: 1px;
158
+ height: 1px;
159
+ padding: 0;
160
+ margin: -1px;
161
+ overflow: hidden;
162
+ clip: rect(0, 0, 0, 0);
163
+ white-space: nowrap;
164
+ border: 0;
165
+ }
166
+
167
+ // Стандартный фокус-ринг
168
+ @mixin focus-ring($color: var(--color-focus, #3b82f6), $offset: 2px) {
169
+ &:focus-visible {
170
+ outline: 2px solid $color;
171
+ outline-offset: $offset;
172
+ border-radius: $radius-sm;
173
+ }
174
+ }
175
+
176
+ // ─── Текстовые утилиты ────────────────────────────────────────────────────────
177
+ @mixin truncate {
178
+ overflow: hidden;
179
+ text-overflow: ellipsis;
180
+ white-space: nowrap;
181
+ }
182
+
183
+ @mixin line-clamp($lines: 2) {
184
+ display: -webkit-box;
185
+ overflow: hidden;
186
+ -webkit-box-orient: vertical;
187
+ -webkit-line-clamp: $lines;
188
+ }
189
+
190
+ // ─── Flex-утилиты ─────────────────────────────────────────────────────────────
191
+ @mixin flex-center {
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: center;
195
+ }
196
+
197
+ @mixin flex-between {
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: space-between;
201
+ }
202
+
203
+ @mixin flex-column {
204
+ display: flex;
205
+ flex-direction: column;
206
+ }
207
+
208
+ // ─── Интерактивные состояния ──────────────────────────────────────────────────
209
+ @mixin interactive {
210
+ cursor: pointer;
211
+ transition: opacity $transition-base;
212
+
213
+ &:hover { opacity: 0.85; }
214
+ &:active { opacity: 0.7; }
215
+ &:disabled, &[aria-disabled='true'] {
216
+ opacity: 0.4;
217
+ cursor: not-allowed;
218
+ pointer-events: none;
219
+ }
220
+
221
+ @include focus-ring;
222
+ }
223
+
224
+ // ─── Сброс стилей ────────────────────────────────────────────────────────────
225
+ @mixin reset-button {
226
+ padding: 0;
227
+ margin: 0;
228
+ background: none;
229
+ border: none;
230
+ cursor: pointer;
231
+ appearance: none;
232
+ }
233
+
234
+ @mixin reset-list {
235
+ padding: 0;
236
+ margin: 0;
237
+ list-style: none;
238
+ }
239
+
240
+ @mixin reset-link {
241
+ color: inherit;
242
+ text-decoration: none;
243
+ }
@@ -0,0 +1,53 @@
1
+ // =============================================================================
2
+ // ТИПОГРАФИКА — семантические теги и утилитарные классы
3
+ // Миксины подключаются автоматически через vite.config.ts (additionalData)
4
+ // =============================================================================
5
+
6
+ h1, .h1 { @include text-h1; }
7
+ h2, .h2 { @include text-h2; }
8
+ h3, .h3 { @include text-h3; }
9
+ h4, .h4 { @include text-h4; }
10
+
11
+ p,
12
+ .body {
13
+ @include text-body;
14
+
15
+ margin-block: 0;
16
+ }
17
+
18
+ small,
19
+ .small { @include text-small; }
20
+
21
+ .caption { @include text-caption; }
22
+
23
+ // Начертания
24
+ b, strong { font-weight: $font-weight-bold; }
25
+ em, i { font-style: italic; }
26
+ u { text-decoration: underline; }
27
+
28
+ // Ссылки
29
+ a {
30
+ color: var(--color-link, inherit);
31
+ text-decoration: underline;
32
+ transition: color $transition-base;
33
+
34
+ @include focus-ring;
35
+
36
+ &:hover { opacity: 0.8; }
37
+ }
38
+
39
+ // Утилиты выравнивания
40
+ .text-left { text-align: left; }
41
+ .text-center { text-align: center; }
42
+ .text-right { text-align: right; }
43
+
44
+ // Утилиты веса
45
+ .font-regular { font-weight: $font-weight-regular; }
46
+ .font-medium { font-weight: $font-weight-medium; }
47
+ .font-semibold { font-weight: $font-weight-semibold; }
48
+ .font-bold { font-weight: $font-weight-bold; }
49
+
50
+ // Утилиты обрезки
51
+ .truncate { @include truncate; }
52
+ .line-clamp-2 { @include line-clamp(2); }
53
+ .line-clamp-3 { @include line-clamp(3); }
@@ -0,0 +1,124 @@
1
+ // =============================================================================
2
+ // DESIGN TOKENS — единственный источник истины для всех стилей проекта
3
+ // Все значения берутся отсюда. Магические числа в компонентах запрещены.
4
+ // =============================================================================
5
+
6
+ // ─── Брейкпоинты ──────────────────────────────────────────────────────────────
7
+ $bp-mobile-max: 767px;
8
+ $bp-tablet: 768px;
9
+ $bp-tablet-max: 1023px;
10
+ $bp-laptop: 1024px;
11
+ $bp-laptop-max: 1279px;
12
+ $bp-desktop-m: 1280px;
13
+ $bp-desktop-m-max: 1599px;
14
+ $bp-desktop-l: 1600px;
15
+ $bp-desktop-l-max: 1919px;
16
+ $bp-desktop-xl: 1920px;
17
+
18
+ // ─── Сетка ────────────────────────────────────────────────────────────────────
19
+ $grid-columns: 12;
20
+
21
+ $grid-gutter-mobile: 16px;
22
+ $grid-gutter-tablet: 24px;
23
+ $grid-gutter-desktop: 32px;
24
+
25
+ $grid-margin-mobile: 16px;
26
+ $grid-margin-tablet: 32px;
27
+ $grid-margin-laptop: 40px;
28
+ $grid-margin-desktop-m: 64px;
29
+ $grid-margin-desktop-l: 80px;
30
+
31
+ $container-max-desktop-m: 1280px;
32
+ $container-max-desktop-l: 1440px;
33
+
34
+ // ─── Шкала отступов (base 8px) ────────────────────────────────────────────────
35
+ $space-1: 4px;
36
+ $space-2: 8px;
37
+ $space-3: 12px;
38
+ $space-4: 16px;
39
+ $space-5: 20px;
40
+ $space-6: 24px;
41
+ $space-8: 32px;
42
+ $space-10: 40px;
43
+ $space-12: 48px;
44
+ $space-16: 64px;
45
+ $space-20: 80px;
46
+ $space-24: 96px;
47
+ $space-32: 128px;
48
+
49
+ // ─── Типографика ──────────────────────────────────────────────────────────────
50
+ $font-size-base: 16px; // = 1rem
51
+
52
+ // Семейства (переопределяются в проекте)
53
+ $font-primary: 'Inter', sans-serif;
54
+ $font-secondary: 'Georgia', serif;
55
+
56
+ // Размеры по адаптивам (в rem)
57
+ $font-size-h1-mobile: 1.75rem; // 28px
58
+ $font-size-h1-tablet: 2.25rem; // 36px
59
+ $font-size-h1-desktop: 3rem; // 48px
60
+
61
+ $font-size-h2-mobile: 1.375rem; // 22px
62
+ $font-size-h2-tablet: 1.75rem; // 28px
63
+ $font-size-h2-desktop: 2.25rem; // 36px
64
+
65
+ $font-size-h3-mobile: 1.125rem; // 18px
66
+ $font-size-h3-tablet: 1.375rem; // 22px
67
+ $font-size-h3-desktop: 1.75rem; // 28px
68
+
69
+ $font-size-h4-desktop: 1.25rem; // 20px
70
+ $font-size-h4-mobile: 1rem; // 16px
71
+
72
+ $font-size-body-desktop: 1rem; // 16px
73
+ $font-size-body-mobile: 0.875rem; // 14px
74
+
75
+ $font-size-small: 0.75rem; // 12px
76
+ $font-size-caption-desktop: 0.875rem; // 14px
77
+
78
+ // Жирность
79
+ $font-weight-regular: 400;
80
+ $font-weight-medium: 500;
81
+ $font-weight-semibold: 600;
82
+ $font-weight-bold: 700;
83
+
84
+ // Межстрочный интервал
85
+ $line-height-heading: 1.2;
86
+ $line-height-body: 1.6;
87
+ $line-height-caption: 1.4;
88
+
89
+ // Межбуквенный интервал
90
+ $letter-spacing-normal: 0;
91
+ $letter-spacing-wide: 0.02em;
92
+ $letter-spacing-heading: -0.01em;
93
+
94
+ // ─── Радиусы скруглений ───────────────────────────────────────────────────────
95
+ $radius-xs: 2px;
96
+ $radius-sm: 4px;
97
+ $radius-md: 8px;
98
+ $radius-lg: 16px;
99
+ $radius-xl: 24px;
100
+ $radius-2xl: 32px;
101
+ $radius-full: 9999px;
102
+
103
+ // ─── Тени ─────────────────────────────────────────────────────────────────────
104
+ $shadow-sm: 0 1px 3px rgb(0 0 0 / 0.08), 0 1px 2px rgb(0 0 0 / 0.04);
105
+ $shadow-md: 0 4px 12px rgb(0 0 0 / 0.08), 0 2px 4px rgb(0 0 0 / 0.04);
106
+ $shadow-lg: 0 8px 24px rgb(0 0 0 / 0.1), 0 4px 8px rgb(0 0 0 / 0.04);
107
+ $shadow-xl: 0 20px 40px rgb(0 0 0 / 0.12);
108
+
109
+ // ─── Анимации ─────────────────────────────────────────────────────────────────
110
+ $transition-fast: 100ms ease;
111
+ $transition-base: 200ms ease;
112
+ $transition-slow: 400ms ease;
113
+ $transition-spring: 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
114
+
115
+ // ─── Z-index ──────────────────────────────────────────────────────────────────
116
+ $z-below: -1;
117
+ $z-base: 0;
118
+ $z-raised: 10;
119
+ $z-dropdown: 100;
120
+ $z-sticky: 200;
121
+ $z-overlay: 300;
122
+ $z-modal: 400;
123
+ $z-toast: 500;
124
+ $z-tooltip: 600;
@@ -0,0 +1,96 @@
1
+ // =============================================================================
2
+ // GLOBAL STYLES — базовый сброс + глобальные переменные
3
+ // Подключается один раз в src/app/index.tsx
4
+ // =============================================================================
5
+
6
+ @use 'variables' as *;
7
+ @use 'mixins' as *;
8
+ @use 'typography';
9
+
10
+ // ─── CSS Custom Properties (runtime tokens) ───────────────────────────────────
11
+ :root {
12
+ // Цвета — переопределяются в теме проекта
13
+ --color-focus: #3b82f6;
14
+ --color-link: inherit;
15
+
16
+ // Сетка
17
+ --grid-columns: #{$grid-columns};
18
+ --container-max: #{$container-max-desktop-l};
19
+
20
+ // Отступы (удобно для JS)
21
+ --space-1: #{$space-1};
22
+ --space-2: #{$space-2};
23
+ --space-4: #{$space-4};
24
+ --space-6: #{$space-6};
25
+ --space-8: #{$space-8};
26
+ --space-12: #{$space-12};
27
+ --space-16: #{$space-16};
28
+ }
29
+
30
+ // ─── Сброс ────────────────────────────────────────────────────────────────────
31
+ *,
32
+ *::before,
33
+ *::after {
34
+ box-sizing: border-box;
35
+ margin: 0;
36
+ padding: 0;
37
+ }
38
+
39
+ html {
40
+ font-size: $font-size-base;
41
+ scroll-behavior: smooth;
42
+ text-size-adjust: 100%;
43
+ -webkit-text-size-adjust: 100%;
44
+ }
45
+
46
+ body {
47
+ font-family: $font-primary;
48
+ font-size: $font-size-body-desktop;
49
+ font-weight: $font-weight-regular;
50
+ line-height: $line-height-body;
51
+ -webkit-font-smoothing: antialiased;
52
+ -moz-osx-font-smoothing: grayscale;
53
+ }
54
+
55
+ // ─── Медиа ────────────────────────────────────────────────────────────────────
56
+ img,
57
+ video,
58
+ svg {
59
+ display: block;
60
+ max-width: 100%;
61
+ height: auto;
62
+ }
63
+
64
+ // ─── Форма ────────────────────────────────────────────────────────────────────
65
+ button {
66
+ cursor: pointer;
67
+ font-family: inherit;
68
+ }
69
+
70
+ input,
71
+ textarea,
72
+ select {
73
+ font-family: inherit;
74
+ font-size: inherit;
75
+ }
76
+
77
+ // ─── Утилиты ──────────────────────────────────────────────────────────────────
78
+ .container {
79
+ @include container;
80
+ }
81
+
82
+ .visually-hidden {
83
+ @include visually-hidden;
84
+ }
85
+
86
+ // Сброс анимаций для пользователей с настройкой «уменьшить движение»
87
+ @media (prefers-reduced-motion: reduce) {
88
+ *,
89
+ *::before,
90
+ *::after {
91
+ animation-duration: 0.01ms !important; // stylelint-disable-line declaration-no-important
92
+ animation-iteration-count: 1 !important; // stylelint-disable-line declaration-no-important
93
+ transition-duration: 0.01ms !important; // stylelint-disable-line declaration-no-important
94
+ scroll-behavior: auto !important; // stylelint-disable-line declaration-no-important
95
+ }
96
+ }
@@ -0,0 +1,36 @@
1
+ // =============================================================================
2
+ // BREAKPOINTS — зеркало значений из _variables.scss для использования в JS/TS
3
+ // Используется в хуках (useMediaQuery), тестах и утилитах
4
+ // =============================================================================
5
+
6
+ export const BREAKPOINTS = {
7
+ mobileMax: 767,
8
+ tablet: 768,
9
+ tabletMax: 1023,
10
+ laptop: 1024,
11
+ laptopMax: 1279,
12
+ desktopM: 1280,
13
+ desktopMMax: 1599,
14
+ desktopL: 1600,
15
+ desktopLMax: 1919,
16
+ desktopXL: 1920,
17
+ } as const;
18
+
19
+ export type BreakpointKey = keyof typeof BREAKPOINTS;
20
+
21
+ // Готовые медиа-строки для matchMedia / CSS-in-JS
22
+ export const MEDIA = {
23
+ mobile: `(max-width: ${BREAKPOINTS.mobileMax}px)`,
24
+ tablet: `(min-width: ${BREAKPOINTS.tablet}px) and (max-width: ${BREAKPOINTS.tabletMax}px)`,
25
+ tabletUp: `(min-width: ${BREAKPOINTS.tablet}px)`,
26
+ laptop: `(min-width: ${BREAKPOINTS.laptop}px) and (max-width: ${BREAKPOINTS.laptopMax}px)`,
27
+ laptopUp: `(min-width: ${BREAKPOINTS.laptop}px)`,
28
+ desktopM: `(min-width: ${BREAKPOINTS.desktopM}px) and (max-width: ${BREAKPOINTS.desktopMMax}px)`,
29
+ desktopMUp: `(min-width: ${BREAKPOINTS.desktopM}px)`,
30
+ desktopL: `(min-width: ${BREAKPOINTS.desktopL}px) and (max-width: ${BREAKPOINTS.desktopLMax}px)`,
31
+ desktopXL: `(min-width: ${BREAKPOINTS.desktopXL}px)`,
32
+ landscape: `(orientation: landscape)`,
33
+ portrait: `(orientation: portrait)`,
34
+ } as const;
35
+
36
+ export type MediaKey = keyof typeof MEDIA;
@@ -0,0 +1,79 @@
1
+ // =============================================================================
2
+ // DESIGN TOKENS — зеркало _variables.scss для JS/TS
3
+ // =============================================================================
4
+
5
+ export const SPACING = {
6
+ 1: 4,
7
+ 2: 8,
8
+ 3: 12,
9
+ 4: 16,
10
+ 5: 20,
11
+ 6: 24,
12
+ 8: 32,
13
+ 10: 40,
14
+ 12: 48,
15
+ 16: 64,
16
+ 20: 80,
17
+ 24: 96,
18
+ 32: 128,
19
+ } as const satisfies Record<number, number>;
20
+
21
+ export type SpacingKey = keyof typeof SPACING;
22
+
23
+ // ─── Типографика ──────────────────────────────────────────────────────────────
24
+ export const FONT_SIZE = {
25
+ h1: { mobile: 28, tablet: 36, desktop: 48 },
26
+ h2: { mobile: 22, tablet: 28, desktop: 36 },
27
+ h3: { mobile: 18, tablet: 22, desktop: 28 },
28
+ h4: { mobile: 16, desktop: 20 },
29
+ body: { mobile: 14, desktop: 16 },
30
+ small: 12,
31
+ captionDesktop: 14,
32
+ } as const;
33
+
34
+ export const FONT_WEIGHT = {
35
+ regular: 400,
36
+ medium: 500,
37
+ semibold: 600,
38
+ bold: 700,
39
+ } as const;
40
+
41
+ export const LINE_HEIGHT = {
42
+ heading: 1.2,
43
+ body: 1.6,
44
+ caption: 1.4,
45
+ } as const;
46
+
47
+ // ─── Радиусы ──────────────────────────────────────────────────────────────────
48
+ export const RADIUS = {
49
+ xs: 2,
50
+ sm: 4,
51
+ md: 8,
52
+ lg: 16,
53
+ xl: 24,
54
+ '2xl': 32,
55
+ full: 9999,
56
+ } as const;
57
+
58
+ // ─── Z-index ──────────────────────────────────────────────────────────────────
59
+ export const Z_INDEX = {
60
+ below: -1,
61
+ base: 0,
62
+ raised: 10,
63
+ dropdown: 100,
64
+ sticky: 200,
65
+ overlay: 300,
66
+ modal: 400,
67
+ toast: 500,
68
+ tooltip: 600,
69
+ } as const;
70
+
71
+ export type ZIndexKey = keyof typeof Z_INDEX;
72
+
73
+ // ─── Анимации ─────────────────────────────────────────────────────────────────
74
+ export const TRANSITION = {
75
+ fast: '100ms ease',
76
+ base: '200ms ease',
77
+ slow: '400ms ease',
78
+ spring: '300ms cubic-bezier(0.34, 1.56, 0.64, 1)',
79
+ } as const;
@@ -0,0 +1 @@
1
+ export { useMediaQuery } from './useMediaQuery';
@@ -0,0 +1,30 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ import { MEDIA, type MediaKey } from '@shared/config/breakpoints';
4
+
5
+ /**
6
+ * Хук для реактивного отслеживания медиа-запросов.
7
+ *
8
+ * @example
9
+ * const isMobile = useMediaQuery('mobile');
10
+ * const isTabletUp = useMediaQuery('tabletUp');
11
+ */
12
+ export function useMediaQuery(key: MediaKey): boolean {
13
+ const query = MEDIA[key];
14
+
15
+ const [matches, setMatches] = useState<boolean>(
16
+ () => window.matchMedia(query).matches,
17
+ );
18
+
19
+ useEffect(() => {
20
+ const media = window.matchMedia(query);
21
+ const listener = (e: MediaQueryListEvent) => { setMatches(e.matches); };
22
+
23
+ media.addEventListener('change', listener);
24
+ setMatches(media.matches);
25
+
26
+ return () => { media.removeEventListener('change', listener); };
27
+ }, [query]);
28
+
29
+ return matches;
30
+ }