@carparkteam/parz-ui 0.0.0-stage → 1.0.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.
@@ -0,0 +1,393 @@
1
+ # Parz Menu
2
+
3
+ [English](parz-menu.en.md) · [Հայերեն](parz-menu.hy.md) · **Русский**
4
+
5
+ Нижнее меню CarPark, вынесенное в React-компонент. Подсветка активного пункта перетекает между вкладками как капля, круглые кнопки справа упруго отвечают на нажатие. Вид настраивается целиком: цвета, размеры, скругления, стекло, скорость и сила анимаций.
6
+
7
+ - до 5–6 пунктов с иконкой и подписью, бейдж-число или бейдж-точка на любом пункте;
8
+ - круглые кнопки справа: иконка или аватар (буквы или фото), тоже с бейджем;
9
+ - иконки: встроенные из CarPark, Material Symbols, Material Icons или любой свой React-элемент;
10
+ - три раскладки: иконка над подписью, в строку, только иконки;
11
+ - прилипает к низу или верху экрана либо стоит в потоке страницы;
12
+ - уважает системную настройку «уменьшить движение» и работает с клавиатуры.
13
+
14
+ ## Установка
15
+
16
+ ```bash
17
+ npm install @carparkteam/parz-ui
18
+ ```
19
+
20
+ ```tsx
21
+ import { ParzMenu } from '@carparkteam/parz-ui';
22
+ import '@carparkteam/parz-ui/style.css';
23
+ ```
24
+
25
+ ## Быстрый старт
26
+
27
+ ```tsx
28
+ import { useState } from 'react';
29
+ import { ParzMenu } from '@carparkteam/parz-ui';
30
+ import '@carparkteam/parz-ui/style.css';
31
+
32
+ export function AppNav() {
33
+ const [tab, setTab] = useState('garage');
34
+ const [panel, setPanel] = useState<string | null>(null);
35
+
36
+ return (
37
+ <ParzMenu
38
+ items={[
39
+ { id: 'garage', label: 'Гараж', icon: 'ms:directions_car' },
40
+ { id: 'parking', label: 'Парковка', icon: 'ms:local_parking', badge: 2 },
41
+ { id: 'map', label: 'Карта', icon: 'pin' }
42
+ ]}
43
+ value={panel ?? tab}
44
+ onChange={id => { setPanel(null); setTab(id); }}
45
+ actions={[
46
+ { id: 'notif', ariaLabel: 'Уведомления', icon: 'bell', badge: 'dot' },
47
+ { id: 'profile', ariaLabel: 'Профиль', avatar: { text: 'A' } }
48
+ ]}
49
+ activeAction={panel}
50
+ onAction={id => setPanel(p => (p === id ? null : id))}
51
+ />
52
+ );
53
+ }
54
+ ```
55
+
56
+ Когда открыт раздел из круглой кнопки, передайте в `value` его id: капля под пунктами спрячется, потому что такого пункта нет. Так сделано в CarPark.
57
+
58
+ ## Пропсы
59
+
60
+ | Проп | Тип | По умолчанию | Что делает |
61
+ |---|---|---|---|
62
+ | `items` | `TabItem[]` | — | Пункты меню |
63
+ | `value` | `string \| null` | — | Активный пункт (управляемый режим). Нет такого пункта или `null` — капля прячется |
64
+ | `defaultValue` | `string` | первый пункт | Активный пункт без управления снаружи |
65
+ | `onChange` | `(id) => void` | — | Нажали пункт |
66
+ | `actions` | `ActionItem[]` | `[]` | Круглые кнопки справа, не больше двух. Любые: колокольчик, профиль, «+», поиск… |
67
+ | `activeAction` | `string \| null` | `null` | Какая круглая кнопка подсвечена |
68
+ | `onAction` | `(id) => void` | — | Нажали круглую кнопку |
69
+ | `hideActions` | `boolean` | `false` | Спрятать круглые кнопки справа. Меню сожмётся по пунктам и встанет по центру |
70
+ | `position` | `'bottom' \| 'top' \| 'static'` | `'bottom'` | Прилипать к низу или верху экрана либо стоять в потоке |
71
+ | `layout` | `'stacked' \| 'inline' \| 'icons'` | `'stacked'` | Иконка над подписью, в строку или только иконки |
72
+ | `theme` | `Partial<ParzMenuTheme>` | — | Цвета и размеры, см. ниже |
73
+ | `glass` | `number \| GlassPreset` | — | Прозрачность стекла: 0…100 % или пресет, см. «Стекло» |
74
+ | `overflow` | `'more' \| 'none'` | `'more'` | Что делать, если пункты не помещаются: прятать в кнопку «Меню» или нет |
75
+ | `minItemWidth` | `number` | 64 / 104 / 52 | Уже какой ширины пункт уходит в «Меню», px. По умолчанию зависит от `layout` |
76
+ | `locale` | `'en' \| 'ru' \| 'hy'` | `'en'` | Язык встроенных подписей: «Меню», поиск, озвучка бейджей. Свои `moreLabel`, `ariaLabel`, `searchPlaceholder` важнее |
77
+ | `moreLabel` | `string` | по `locale` | Подпись кнопки «Меню» |
78
+ | `mobileSearch` | `boolean \| MobileSearchOptions` | — | Поиск на узком экране, как в iOS, см. «Поиск на телефоне» |
79
+ | `desktop` | `boolean \| DesktopOptions` | — | **Beta.** Шапка для больших экранов, см. «Шапка для компьютера» |
80
+ | `moreIcon` | `IconSpec` | `'more'` | Иконка кнопки «Меню». По умолчанию — три точки в круге: контур, а когда открыт спрятанный пункт — заливка |
81
+ | `motion` | `Partial<LiquidMotion>` | — | Анимации, см. ниже |
82
+ | `maxBadge` | `number` | `99` | Числа больше показываются как «99+» |
83
+ | `badgeLabel` | `(label, badge) => string` | — | Как диктор читает пункт с бейджем |
84
+ | `autoLoadFonts` | `boolean` | `true` | Сам подключать шрифты Material, если они есть в иконках |
85
+ | `ariaLabel` | `string` | по `locale` | Подпись всего меню для диктора |
86
+ | `className`, `style` | — | — | Обычные |
87
+
88
+ ### TabItem
89
+
90
+ | Поле | Тип | Что делает |
91
+ |---|---|---|
92
+ | `id` | `string` | Уникальный ключ |
93
+ | `label` | `string` | Подпись |
94
+ | `icon` | `IconSpec` | Иконка, см. «Иконки» |
95
+ | `activeIcon` | `IconSpec` | Своя иконка для активного состояния. Встроенным и Material Symbols не нужна |
96
+ | `badge` | `number \| string \| 'dot' \| boolean` | Бейдж. `0`, `false`, `null` — нет бейджа |
97
+ | `disabled` | `boolean` | Пункт неактивен |
98
+ | `ariaLabel` | `string` | Подпись для диктора вместо `label` |
99
+
100
+ ### ActionItem
101
+
102
+ | Поле | Тип | Что делает |
103
+ |---|---|---|
104
+ | `id` | `string` | Уникальный ключ |
105
+ | `ariaLabel` | `string` | Подпись для диктора. Обязательна: у кнопки нет текста |
106
+ | `icon`, `activeIcon` | `IconSpec` | Иконка |
107
+ | `avatar` | `{ text?: string; src?: string }` | Аватар вместо иконки: буквы или фото |
108
+ | `badge` | как у пункта | Бейдж |
109
+ | `variant` | `'default' \| 'accent'` | `accent` — залитая цветом кнопка для главного действия, например «+» |
110
+
111
+ ## Кнопки справа
112
+
113
+ В группе справа — до двух круглых кнопок, любых. Колокольчик и аватар в примерах — просто самый частый вариант. Если передать больше двух, покажутся первые две, а в консоли будет предупреждение.
114
+
115
+ Кнопка бывает трёх видов:
116
+ - **иконка** — как колокольчик: `{ id: 'notif', ariaLabel: 'Уведомления', icon: 'bell' }`;
117
+ - **акцентная** — залита цветом, для главного действия: `{ id: 'add', ariaLabel: 'Добавить', icon: 'plus', variant: 'accent' }`;
118
+ - **аватар** — буквы или фото: `{ id: 'profile', ariaLabel: 'Профиль', avatar: { src: '/me.jpg' } }`.
119
+
120
+ Кнопка может открывать свой раздел (тогда передайте её id в `activeAction` — она подсветится, а капля под пунктами спрячется) или просто выполнять действие, как «+»: тогда ничего подсвечивать не нужно.
121
+
122
+ ```tsx
123
+ <ParzMenu
124
+ items={items}
125
+ value={panel ?? tab}
126
+ onChange={id => { setPanel(null); setTab(id); }}
127
+ actions={[
128
+ { id: 'notif', ariaLabel: 'Уведомления', icon: 'bell', badge: 3 },
129
+ { id: 'add', ariaLabel: 'Добавить', icon: 'plus', variant: 'accent' }
130
+ ]}
131
+ activeAction={panel}
132
+ onAction={id => (id === 'add' ? openCreateSheet() : setPanel(p => (p === id ? null : id)))}
133
+ />
134
+ ```
135
+
136
+ ## Пункты и кнопки на лету
137
+
138
+ Пункты и кнопки справа можно добавлять и убирать в любой момент — например, скрипт на сайте включает пункт «Акции», когда началась распродажа. Ничего не дёргается:
139
+
140
+ - уходящий пункт или кнопка плавно схлопывается, соседи сдвигаются на его место;
141
+ - новый «вырастает» из нуля с упругим перелётом;
142
+ - кнопка «Меню» появляется и исчезает так же, когда пункты перестают или снова начинают помещаться;
143
+ - капля едет вместе со своим пунктом.
144
+
145
+ Длительность — та же `motion.shift`. Достаточно просто поменять массив `items` или `actions`:
146
+
147
+ ```tsx
148
+ const items = [garage, parking, ...(saleIsOn ? [sale] : [])];
149
+ ```
150
+
151
+ ## Меню без кнопок справа
152
+
153
+ Если не передать `actions` или поставить `hideActions`, группы справа нет. Переключение `hideActions` анимируется: группа сворачивается, капсула с пунктами плавно сужается и переезжает в центр, капля едет за своим пунктом. Скорость задаёт `motion.shift`. Тогда капсула с пунктами не тянется на всю ширину, а сжимается по содержимому и встаёт по центру. Освободившееся место можно отдать под пункты: на телефоне спокойно помещается 5–6.
154
+
155
+ ```tsx
156
+ <ParzMenu
157
+ items={[
158
+ { id: 'home', label: 'Главная', icon: 'ms:home' },
159
+ { id: 'search', label: 'Поиск', icon: 'ms:search' },
160
+ { id: 'parking', label: 'Парковка', icon: 'ms:local_parking', badge: 2 },
161
+ { id: 'saved', label: 'Избранное', icon: 'ms:favorite' },
162
+ { id: 'profile', label: 'Профиль', icon: 'ms:person', badge: 'dot' }
163
+ ]}
164
+ value={tab}
165
+ onChange={setTab}
166
+ />
167
+ ```
168
+
169
+ Пункты одинаковой ширины, её задаёт `theme.itemMinWidth` (84 px). Капсула шириной «пункты × эта ширина», но не шире экрана. Если пункты не влезают, они делят ширину поровну, а длинные подписи обрезаются многоточием — тогда лучше `layout="icons"`.
170
+
171
+ ## Если пункты не помещаются
172
+
173
+ Компонент сам следит за шириной. Когда пункт становится уже `minItemWidth` (64 px для раскладки «иконка над подписью»), последние пункты уходят в кнопку «Меню» (три точки в круге). По нажатию над ней всплывает список спрятанных пунктов: иконка, подпись и бейдж.
174
+
175
+ - Если открыт спрятанный пункт, капля стоит под «Меню», а в списке он подсвечен.
176
+ - Бейджи спрятанных пунктов собираются на «Меню»: числа складываются, а если чисел нет — ставится точка.
177
+ - Список закрывается выбором пункта, повторным нажатием на «Меню», кликом мимо или клавишей Esc.
178
+ - Ширина пересчитывается при изменении размера окна, поэтому на планшете и компьютере видно больше пунктов.
179
+
180
+ ```tsx
181
+ <ParzMenu items={tenItems} value={tab} onChange={setTab} moreLabel="Ещё" />
182
+ ```
183
+
184
+ Чтобы никогда ничего не прятать, поставьте `overflow="none"` — тогда пункты просто сжимаются.
185
+
186
+ ## Поиск на телефоне
187
+
188
+ `mobileSearch` добавляет на узком экране поиск в духе iOS. Лупа встаёт первой кнопкой в группу справа; из `actions` остаётся последняя, чтобы в группе по-прежнему было две кнопки.
189
+
190
+ По нажатию на лупу группа по-жидкому растекается на всю ширину меню, а капсула с пунктами схлопывается. Внутри:
191
+ - слева лупа превращается в «✕» поменьше — закрыть поиск и очистить поле, пункты возвращаются;
192
+ - посередине поле ввода, клавиатура сразу открыта;
193
+ - справа, отдельной круглой кнопкой в своём стекле, — «→» (искать); выкатывается из-за группы и загорается акцентным цветом, когда в поле есть текст.
194
+
195
+ ```tsx
196
+ <ParzMenu
197
+ items={items}
198
+ actions={[bell, profile]} // на телефоне в группе будут лупа и профиль
199
+ mobileSearch={{ searchPlaceholder: 'Марка или парковка', onSearch: q => navigate(`/search?q=${q}`) }}
200
+ />
201
+ ```
202
+
203
+ Поля те же, что у поиска в шапке: `searchPlaceholder`, `searchValue` / `onSearchChange`, `onSearch`. На широком экране, если включена шапка, работает её поиск.
204
+
205
+ ## Шапка для компьютера (beta)
206
+
207
+ На широком экране меню может становиться полноценной верхней шапкой: слева логотип, затем пункты в строку, по центру поиск, справа кнопки. Под шапкой — затенение с размытием, контент красиво уходит под неё при прокрутке. На узком экране меню остаётся обычным нижним.
208
+
209
+ ```tsx
210
+ <ParzMenu
211
+ items={items}
212
+ value={tab}
213
+ onChange={setTab}
214
+ actions={actions}
215
+ desktop={{
216
+ logo: 'CARPARK', // строка или любой элемент: <img>, svg, анимированный логотип
217
+ search: 'expand', // 'bar' | 'button' | 'expand' | 'none'
218
+ searchPlaceholder: 'Марка, модель или парковка…',
219
+ onSearch: q => navigate(`/search?q=${q}`)
220
+ }}
221
+ />
222
+ ```
223
+
224
+ | Поле | По умолчанию | Что делает |
225
+ |---|---|---|
226
+ | `breakpoint` | `1100` | С какой ширины включается шапка, px |
227
+ | `breakpointBy` | `'window'` | Мерить ширину окна или родителя меню (`'parent'` — для узких контейнеров и превью) |
228
+ | `logo` | — | Логотип слева: строка или React-элемент |
229
+ | `search` | `'bar'` | `bar` — отдельная строка поиска; `button` — круглая кнопка поиска в группе справа; `expand` — кнопка в группе справа, которая раскрывается в строку прямо внутри группы; `none` — без поиска |
230
+ | `searchPlaceholder` | по `locale` | Подсказка в строке |
231
+ | `searchValue` / `onSearchChange` | — | Текст поиска, если хотите управлять им сами |
232
+ | `onSearch` | — | Нажали Enter (или кнопку поиска в раскрытой строке) |
233
+ | `onSearchClick` | — | Нажали кнопку поиска в виде `button` — например, открыть свой поиск |
234
+ | `searchShortcut` | `'/'` | Клавиша, которая открывает поиск; `false` — выключить |
235
+ | `searchWidth` | `420` / `320` | Ширина строки поиска, px: отдельной строки — 420, раскрытой внутри группы — 320 |
236
+ | `searchAt` | `'start'` | Где кнопка поиска в группе справа (`button`, `expand` и поиск на телефоне): `start` — первой, `end` — последней |
237
+ | `actionsAlign` | `'end'` | Где стоит группа справа, если нет строки поиска: `end` — у правого края, `next` — сразу за пунктами |
238
+ | `layout` | `'inline'` | Раскладка пунктов в шапке |
239
+ | `maxWidth` | `1360` | Ширина содержимого шапки, px |
240
+ | `height` | `80` | Высота шапки, px |
241
+
242
+ Цвета шапки — в теме: `headerTop`, `headerMid` (затенение), `headerBlur` (размытие), `searchBg`, `searchBorder` (строка поиска). Если пункты не помещаются рядом с логотипом и поиском, работает то же «Меню», только список открывается вниз. Все переключения шапки плавные: смена вида поиска (строка схлопывается, кнопка «вырастает» в группе), перенос группы между краем и пунктами, появление и смена логотипа, переход между шапкой и нижним меню при смене ширины.
243
+
244
+ Кнопка поиска в видах `button` и `expand` стоит первой в группе справа и не считается в лимит двух кнопок. Раскрывающийся поиск растягивает всю группу; если пунктам становится тесно, лишние плавно уходят в «Меню». Закрывается клавишей Esc или кликом мимо, если в нём ничего не набрано.
245
+
246
+ ## Бейджи
247
+
248
+ ```tsx
249
+ { id: 'fines', label: 'Штрафы', icon: 'ms:receipt_long', badge: 3 } // число
250
+ { id: 'fines', label: 'Штрафы', icon: 'ms:receipt_long', badge: 150 } // покажет «99+»
251
+ { id: 'news', label: 'Новое', icon: 'sparkles', badge: 'dot' } // точка
252
+ { id: 'sale', label: 'Акции', icon: 'gift', badge: 'new' } // текст
253
+ ```
254
+
255
+ Бейдж появляется с упругим «прыжком» и прыгает заново, когда меняется число. Цвета: `badgeBg` и `badgeText` для числа, `accent` для точки.
256
+
257
+ ## Иконки
258
+
259
+ | Запись | Что это |
260
+ |---|---|
261
+ | `'pin'` | Встроенная иконка. У активного пункта контур сменяется заливкой |
262
+ | `'ms:home'` | Material Symbols Rounded. У активного пункта заливка плавно перетекает |
263
+ | `'ms:home:outlined'`, `'ms:home:sharp'` | Material Symbols другого стиля |
264
+ | `'mi:home'` | Классические Material Icons (заливка) |
265
+ | `'mi:home:outlined'` | Также `round`, `sharp`, `two-tone` |
266
+ | `<MyIcon />` | Любой React-элемент: svg, картинка, иконка из любой библиотеки |
267
+
268
+ Встроенные: `film`, `ticket`, `pin`, `bell`, `glasses3d`, `clapperboard`, `flame`, `gift`, `card`, `globe`, `sparkles`, `info`, `logout`, `bulb`, `rotate`, `star`, `check`, `chevL`, `chevR`, `popcorn`, `calendar`, `wallet`, `route`, `users`, `more`, `plus`, `search`, `close`, `arrowR`.
269
+
270
+ Шрифты Material компонент подключает сам с Google Fonts, когда видит `ms:` или `mi:`. Если шрифты уже подключены у вас или Google Fonts недоступен, выключите это: `autoLoadFonts={false}`. Названия иконок — на [fonts.google.com/icons](https://fonts.google.com/icons).
271
+
272
+ Для своих иконок разный вид активного состояния задаётся через `activeIcon`:
273
+
274
+ ```tsx
275
+ { id: 'home', label: 'Главная', icon: <HomeOutline />, activeIcon: <HomeFilled /> }
276
+ ```
277
+
278
+ ## Язык
279
+
280
+ `locale` переключает собственные подписи компонента: кнопку «Меню», подпись меню для диктора, подсказки и кнопки поиска, озвучку бейджа-точки.
281
+
282
+ | `locale` | «Меню» | Поиск | Бейдж-точка |
283
+ |---|---|---|---|
284
+ | `'en'` (по умолчанию) | More | Search | «Parking, new» |
285
+ | `'hy'` | Մենյու | Որոնում | «Կայանատեղի, նոր» |
286
+ | `'ru'` | Меню | Поиск | «Парковка, есть новое» |
287
+
288
+ ```tsx
289
+ <ParzMenu locale="ru" items={items} />
290
+ ```
291
+
292
+ Подписи пунктов и `ariaLabel` кнопок — ваши, переводите их у себя в приложении. Сами строки экспортируются как `LTB_STRINGS`, если нужны где-то ещё.
293
+
294
+ ## Тема
295
+
296
+ Все поля необязательны. Цвета — любые CSS-цвета, размеры — числа в пикселях.
297
+
298
+ | Поле | По умолчанию | Что задаёт |
299
+ |---|---|---|
300
+ | `accent` | `#6ee7a0` | Точка-бейдж, кольцо фокуса, обводка активного аватара |
301
+ | `text` / `textActive` | `#8b8b93` / `#fff` | Подписи и иконки |
302
+ | `surfaceTop` / `surfaceBottom` | тёмное стекло | Градиент капсул |
303
+ | `border` | `rgba(255,255,255,.06)` | Тонкая обводка капсул |
304
+ | `bevelTop` / `bevelBottom` | белый 14% / чёрный 40% | Объёмный блик капсул: линия сверху и тень снизу. На светлой теме нижнюю делайте почти прозрачной |
305
+ | `highlightTop` / `highlightBottom` | белый 13% / 5% | Градиент капли |
306
+ | `highlightShadow` | тонкий блик | Тень и блик капли (CSS box-shadow). На светлой теме — мягкая тень снизу |
307
+ | `actionBg` | белый 7% | Фон круглых кнопок |
308
+ | `actionActiveBg` / `actionActiveText` | `#fff` / `#000` | Нажатая круглая кнопка |
309
+ | `actionAccentBg` / `actionAccentText` | `#6ee7a0` / `#04130a` | Акцентная круглая кнопка (`variant: 'accent'`) |
310
+ | `badgeBg` / `badgeText` | `#ff4d5e` / `#fff` | Бейдж с числом |
311
+ | `badgeRing` | `#1d1d21` | Кольцо вокруг бейджа — ставьте цвет панели |
312
+ | `avatarFrom` / `avatarTo` / `avatarText` | зелёный градиент | Аватар |
313
+ | `fontFamily` | `inherit` | Шрифт подписей |
314
+ | `radius` | `999` | Скругление капсул и капли |
315
+ | `padding` | `6` | Отступ внутри капсул |
316
+ | `gap` | `10` | Расстояние между капсулами |
317
+ | `itemPaddingY` | `8` | Высота пунктов |
318
+ | `iconSize` | `22` | Размер иконок |
319
+ | `fontSize` | `11` | Размер подписей |
320
+ | `actionSize` | `50` | Диаметр круглых кнопок |
321
+ | `blur` | `20` | Размытие стекла |
322
+ | `maxWidth` | `540` | Ширина меню на широких экранах |
323
+ | `offset` | `12` | Отступ от края экрана |
324
+ | `itemMinWidth` | `84` | Ширина пункта в меню без кнопок справа |
325
+ | `shadow` | мягкая тень | Тень под капсулами |
326
+ | `headerTop` / `headerMid` | почти чёрный 96% / 82% | Затенение под шапкой на большом экране: сверху и в середине, ниже растворяется |
327
+ | `headerBlur` | `14` | Размытие под шапкой |
328
+ | `searchBg` / `searchBorder` | белый 6% / 8% | Строка поиска: фон и обводка |
329
+
330
+ То же самое можно задать в CSS — каждое поле это переменная `--ltb-*`:
331
+
332
+ ```css
333
+ .my-bar {
334
+ --ltb-accent: #ff7a00;
335
+ --ltb-surface-top: rgba(255, 255, 255, .8);
336
+ --ltb-surface-bottom: rgba(240, 240, 245, .9);
337
+ --ltb-text: #6b6b75;
338
+ --ltb-text-active: #111;
339
+ }
340
+ ```
341
+
342
+ ```tsx
343
+ <ParzMenu className="my-bar" items={items} />
344
+ ```
345
+
346
+ ## Стекло
347
+
348
+ Проп `glass` задаёт, насколько прозрачен фон капсул: число от 0 до 100 (процент прозрачности) или готовый пресет. Цвет остаётся из темы (`surfaceTop` / `surfaceBottom`), меняется только прозрачность.
349
+
350
+ | Пресет | Прозрачность | Размытие | Как выглядит |
351
+ |---|---|---|---|
352
+ | `'solid'` | 0 % | 0 | Сплошная плашка, контент сквозь неё не виден |
353
+ | `'frosted'` | 15 % | 20 px | Матовое стекло — как в CarPark |
354
+ | `'glass'` | 40 % | 16 px | Заметно просвечивает |
355
+ | `'clear'` | 70 % | 10 px | Почти прозрачное, держится на размытии и обводке |
356
+ | `'none'` | 100 % | 0 | Фона нет — только иконки, подписи и капля |
357
+
358
+ ```tsx
359
+ <ParzMenu glass="glass" items={items} /> // пресет
360
+ <ParzMenu glass={55} items={items} /> // 55 % прозрачности, размытие из темы
361
+ ```
362
+
363
+ Пресет задаёт и размытие. Нужно своё размытие — передайте `glass` числом, а размытие задайте в `theme.blur`. Без `glass` прозрачность берётся из цветов темы как есть. Для цветов в формате `hsl()`, `oklch()` и именах вроде `navy` прозрачность не меняется — задавайте их в `#rrggbb` или `rgb()`.
364
+
365
+ ## Анимации
366
+
367
+ | Поле | По умолчанию | Что задаёт |
368
+ |---|---|---|
369
+ | `lead` | `360` | Время ведущего края капли, мс |
370
+ | `lag` | `660` | Время отстающего края, мс. Чем больше разница с `lead`, тем сильнее капля тянется |
371
+ | `squash` | `0.26` | Насколько капля сплющивается в полёте. `0` — без этого |
372
+ | `overshoot` | `1.35` | Пружинистость: `1` — без перелёта, `1.6` — сильный |
373
+ | `jelly` | `true` | Упругий отклик круглых кнопок и «всплеск» их капсулы |
374
+ | `wave` | `0.035` | Упругая волна всей капсулы с пунктами: при переходе — в сторону хода капли, при возврате из кнопок справа — влево, при повторном нажатии на открытый пункт — от центра. `0` — выключено, `0.08` — сильно |
375
+ | `shift` | `550` | За сколько мс группа справа появляется или прячется, а пункты переезжают по центру |
376
+
377
+ ```tsx
378
+ <ParzMenu motion={{ lead: 280, lag: 820, squash: 0.35 }} items={items} />
379
+ ```
380
+
381
+ При системной настройке «уменьшить движение» капля просто быстро переезжает, без растяжения и упругости.
382
+
383
+ ## Доступность
384
+
385
+ - Меню — элемент `nav`, активный пункт помечен `aria-current="page"`, круглые кнопки — `aria-pressed`.
386
+ - Бейдж зачитывается вместе с пунктом: «Штрафы, 3», «Новое, есть новое». Формулировку меняет `badgeLabel`.
387
+ - Всё работает с клавиатуры: Tab и Enter, видно кольцо фокуса.
388
+
389
+ ## Чего здесь нет
390
+
391
+ - Своих сообщений, роутинга и состояния страниц: компонент только показывает меню и сообщает о нажатиях.
392
+ - Перетаскивания пунктов и свайпа между вкладками.
393
+ - Поддержки браузеров без `backdrop-filter`: там капсулы будут без размытия, но с фоном.
@@ -0,0 +1,76 @@
1
+ # Parz Slider & Parz Range
2
+
3
+ Two Parz UI sliders with one liquid core.
4
+
5
+ - **`ParzSlider`** — a single value: volume, brightness, size.
6
+ - **`ParzRange`** — an interval with **2 to 5 thumbs** on one track: price “from–to”, working hours with a break, several zones.
7
+
8
+ How they move:
9
+
10
+ - **drag** — the thumb grows a little and stretches with the speed of your movement, then rounds back with a soft spring;
11
+ - **tap the track** — the nearest thumb slides over with a light overshoot, the fill catches up;
12
+ - **value bubble** — pops up above the thumb while you drag or focus it;
13
+ - **keyboard** — Tab to a thumb, arrows ±`step`, PageUp/PageDown ±10%, Home/End to the edges;
14
+ - **reduced motion** — with `prefers-reduced-motion` only short plain moves remain.
15
+
16
+ In `ParzRange` thumbs never jump over each other, and `minDistance` keeps a gap between neighbours.
17
+
18
+ ```tsx
19
+ import { useState } from 'react';
20
+ import { ParzSlider, ParzRange } from '@carparkteam/parz-ui';
21
+ import '@carparkteam/parz-ui/style.css';
22
+
23
+ const [volume, setVolume] = useState(60);
24
+ <ParzSlider value={volume} onChange={setVolume} label="Volume" formatValue={v => `${v}%`} />
25
+
26
+ const [price, setPrice] = useState([2000, 7000]);
27
+ <ParzRange value={price} onChange={setPrice} min={0} max={10000} step={500}
28
+ label="Price" formatValue={v => `$${v}`} onChangeEnd={loadResults} />
29
+
30
+ // four thumbs: work 9–13, break, work 14–18
31
+ <ParzRange defaultValue={[9, 13, 14, 18]} min={0} max={24} minDistance={1} fill="pairs" formatValue={v => `${v}:00`} />
32
+ ```
33
+
34
+ ## Shared props
35
+
36
+ | Prop | Default | What it does |
37
+ |---|---|---|
38
+ | `min` / `max` / `step` | `0` / `100` / `1` | Bounds and step |
39
+ | `label` | — | Caption above the slider |
40
+ | `showOutput` | `true` if `label` | Current value on the right of the caption |
41
+ | `formatValue` | — | How to print a number, e.g. ``v => `${v} km` `` |
42
+ | `showValue` | `'auto'` | Bubble above the thumb: `'auto'` — while dragging or focused, `'always'`, `'never'` |
43
+ | `marks` | — | `true` — a dot at every step, or a list: `[0, 50, { value: 100, label: 'Max' }]` |
44
+ | `size` | `'md'` | `'sm'` (16 px), `'md'` (20 px), `'lg'` (26 px) or thumb diameter in px |
45
+ | `disabled` | `false` | Turned off and dimmed |
46
+ | `accent` | `#6ee7a0` | Fill color |
47
+ | `trackColor` | `#2a2a30` | Track color. On a light page use something like `#d9dae1` |
48
+ | `thumbColor` | `#fff` | Thumb color |
49
+ | `name` | — | Values are sent with a form as hidden inputs |
50
+ | `className` / `style` | — | On the root element |
51
+
52
+ ## ParzSlider
53
+
54
+ | Prop | Default | What it does |
55
+ |---|---|---|
56
+ | `value` / `defaultValue` | — / `min` | Value (controlled / uncontrolled) |
57
+ | `onChange` | — | `(value: number) => void` — on every move |
58
+ | `onChangeEnd` | — | `(value: number) => void` — when released or after a key press |
59
+ | `ariaLabel` | — | Name for screen readers when there is no `label` |
60
+
61
+ ## ParzRange
62
+
63
+ | Prop | Default | What it does |
64
+ |---|---|---|
65
+ | `value` / `defaultValue` | — / 25% and 75% | Values in ascending order, **from 2 to 5**. Extra ones are cut, missing ones are added at `max` |
66
+ | `onChange` | — | `(value: number[]) => void` |
67
+ | `onChangeEnd` | — | `(value: number[]) => void` — good place to load results |
68
+ | `minDistance` | `0` | Minimum gap between neighbouring thumbs |
69
+ | `fill` | `'span'` | `'span'` — fill from the first to the last thumb, `'pairs'` — only pairs 1–2, 3–4 (work — break — work) |
70
+ | `ariaLabels` | — | Screen-reader names for thumbs, e.g. `['From', 'To']` |
71
+
72
+ The limits are exported too: `RANGE_MIN_THUMBS` (2) and `RANGE_MAX_THUMBS` (5).
73
+
74
+ ## CSS variables
75
+
76
+ `--pz-sl-accent`, `--pz-sl-track`, `--pz-sl-knob`, `--pz-sl-tip-bg`, `--pz-sl-tip-fg` — on the slider or any parent.
@@ -0,0 +1,76 @@
1
+ # Parz Slider և Parz Range
2
+
3
+ Parz UI-ի երկու սահիչ՝ ընդհանուր հեղուկ միջուկով։
4
+
5
+ - **`ParzSlider`** — մեկ արժեք՝ ձայն, պայծառություն, չափ։
6
+ - **`ParzRange`** — միջակայք, մեկ ուղու վրա **2-ից 5 սահնակ**՝ գին «սկսած–մինչև», աշխատանքային ժամեր ընդմիջումով, մի քանի գոտի։
7
+
8
+ Ինչպես են շարժվում.
9
+
10
+ - **քաշել** — սահնակը մի փոքր մեծանում է և ձգվում շարժման արագությանը համապատասխան, հետո մեղմ զսպանակով կլորանում;
11
+ - **սեղմել ուղուն** — մոտակա սահնակը մոտենում է թեթև անցումով, լցումը հասնում է;
12
+ - **արժեքի պղպջակ** — հայտնվում է սահնակի վերևում, երբ այն քաշում են կամ ֆոկուսում է;
13
+ - **ստեղնաշար** — Tab՝ սահնակին, սլաքներ ±`step`, PageUp/PageDown ±10%, Home/End՝ եզրերին;
14
+ - **քիչ շարժում** — `prefers-reduced-motion`-ի դեպքում մնում են միայն կարճ պարզ տեղաշարժեր։
15
+
16
+ `ParzRange`-ում սահնակները միմյանց վրայով չեն ցատկում, իսկ `minDistance`-ը պահում է բացը հարևանների միջև։
17
+
18
+ ```tsx
19
+ import { useState } from 'react';
20
+ import { ParzSlider, ParzRange } from '@carparkteam/parz-ui';
21
+ import '@carparkteam/parz-ui/style.css';
22
+
23
+ const [volume, setVolume] = useState(60);
24
+ <ParzSlider value={volume} onChange={setVolume} label="Ձայն" formatValue={v => `${v}%`} />
25
+
26
+ const [price, setPrice] = useState([2000, 7000]);
27
+ <ParzRange value={price} onChange={setPrice} min={0} max={10000} step={500}
28
+ label="Գին" formatValue={v => `${v} ֏`} onChangeEnd={loadResults} />
29
+
30
+ // չորս սահնակ՝ աշխատանք 9–13, ընդմիջում, աշխատանք 14–18
31
+ <ParzRange defaultValue={[9, 13, 14, 18]} min={0} max={24} minDistance={1} fill="pairs" formatValue={v => `${v}:00`} />
32
+ ```
33
+
34
+ ## Ընդհանուր props
35
+
36
+ | Prop | Լռելյայն | Ինչ է անում |
37
+ |---|---|---|
38
+ | `min` / `max` / `step` | `0` / `100` / `1` | Սահմաններ և քայլ |
39
+ | `label` | — | Պիտակ սահիչի վերևում |
40
+ | `showOutput` | `true`, եթե կա `label` | Ընթացիկ արժեքը պիտակի աջ կողմում |
41
+ | `formatValue` | — | Ինչպես ցույց տալ թիվը, օրինակ ``v => `${v} կմ` `` |
42
+ | `showValue` | `'auto'` | Պղպջակ սահնակի վրա՝ `'auto'` — քաշելիս կամ ֆոկուսում, `'always'`, `'never'` |
43
+ | `marks` | — | `true` — կետ ամեն քայլին, կամ ցուցակ՝ `[0, 50, { value: 100, label: 'Max' }]` |
44
+ | `size` | `'md'` | `'sm'` (16 px), `'md'` (20 px), `'lg'` (26 px) կամ սահնակի տրամագիծը px-ով |
45
+ | `disabled` | `false` | Անջատված և խամրած |
46
+ | `accent` | `#6ee7a0` | Լցման գույնը |
47
+ | `trackColor` | `#2a2a30` | Ուղու գույնը։ Բաց էջում՝ օրինակ `#d9dae1` |
48
+ | `thumbColor` | `#fff` | Սահնակի գույնը |
49
+ | `name` | — | Արժեքները ձևի հետ ուղարկվում են թաքնված input-ներով |
50
+ | `className` / `style` | — | Արմատային տարրի վրա |
51
+
52
+ ## ParzSlider
53
+
54
+ | Prop | Լռելյայն | Ինչ է անում |
55
+ |---|---|---|
56
+ | `value` / `defaultValue` | — / `min` | Արժեք (կառավարվող / չկառավարվող) |
57
+ | `onChange` | — | `(value: number) => void` — ամեն շարժման ժամանակ |
58
+ | `onChangeEnd` | — | `(value: number) => void` — բաց թողնելիս կամ ստեղնը սեղմելուց հետո |
59
+ | `ariaLabel` | — | Անուն էկրանի ընթերցողների համար, եթե `label` չկա |
60
+
61
+ ## ParzRange
62
+
63
+ | Prop | Լռելյայն | Ինչ է անում |
64
+ |---|---|---|
65
+ | `value` / `defaultValue` | — / 25% և 75% | Արժեքներ աճման կարգով, **2-ից 5**։ Ավելորդները կտրվում են, պակասողները ավելացվում են `max`-ում |
66
+ | `onChange` | — | `(value: number[]) => void` |
67
+ | `onChangeEnd` | — | `(value: number[]) => void` — հարմար է արդյունքները բեռնելու համար |
68
+ | `minDistance` | `0` | Նվազագույն բացը հարևան սահնակների միջև |
69
+ | `fill` | `'span'` | `'span'` — լցում առաջինից մինչև վերջին սահնակը, `'pairs'` — միայն զույգերը 1–2, 3–4 (աշխատանք — ընդմիջում — աշխատանք) |
70
+ | `ariaLabels` | — | Սահնակների անուններ էկրանի ընթերցողների համար, օրինակ `['Սկսած', 'Մինչև']` |
71
+
72
+ Սահմանները նույնպես արտահանվում են՝ `RANGE_MIN_THUMBS` (2) և `RANGE_MAX_THUMBS` (5)։
73
+
74
+ ## CSS փոփոխականներ
75
+
76
+ `--pz-sl-accent`, `--pz-sl-track`, `--pz-sl-knob`, `--pz-sl-tip-bg`, `--pz-sl-tip-fg` — սահիչի կամ ցանկացած ծնողի վրա։
@@ -0,0 +1,76 @@
1
+ # Parz Slider и Parz Range
2
+
3
+ Два ползунка Parz UI с общим жидким ядром.
4
+
5
+ - **`ParzSlider`** — одно значение: громкость, яркость, размер.
6
+ - **`ParzRange`** — промежуток, на одной дорожке **от 2 до 5 бегунков**: цена «от и до», рабочие часы с перерывом, несколько зон.
7
+
8
+ Как двигаются:
9
+
10
+ - **перетаскивание** — бегунок чуть увеличивается и тянется по скорости движения, потом мягко пружинит обратно в круг;
11
+ - **нажатие на дорожку** — ближайший бегунок подъезжает с лёгким перелётом, заливка догоняет;
12
+ - **пузырёк со значением** — выпрыгивает над бегунком, пока его тянут или он в фокусе;
13
+ - **клавиатура** — Tab на бегунок, стрелки ±`step`, PageUp/PageDown ±10%, Home/End — к краям;
14
+ - **меньше движения** — при `prefers-reduced-motion` остаются только короткие простые сдвиги.
15
+
16
+ В `ParzRange` бегунки не перепрыгивают друг через друга, а `minDistance` держит зазор между соседями.
17
+
18
+ ```tsx
19
+ import { useState } from 'react';
20
+ import { ParzSlider, ParzRange } from '@carparkteam/parz-ui';
21
+ import '@carparkteam/parz-ui/style.css';
22
+
23
+ const [volume, setVolume] = useState(60);
24
+ <ParzSlider value={volume} onChange={setVolume} label="Громкость" formatValue={v => `${v}%`} />
25
+
26
+ const [price, setPrice] = useState([2000, 7000]);
27
+ <ParzRange value={price} onChange={setPrice} min={0} max={10000} step={500}
28
+ label="Цена" formatValue={v => `${v} ₽`} onChangeEnd={loadResults} />
29
+
30
+ // четыре бегунка: работа 9–13, перерыв, работа 14–18
31
+ <ParzRange defaultValue={[9, 13, 14, 18]} min={0} max={24} minDistance={1} fill="pairs" formatValue={v => `${v}:00`} />
32
+ ```
33
+
34
+ ## Общие пропсы
35
+
36
+ | Проп | По умолчанию | Что делает |
37
+ |---|---|---|
38
+ | `min` / `max` / `step` | `0` / `100` / `1` | Границы и шаг |
39
+ | `label` | — | Подпись над ползунком |
40
+ | `showOutput` | `true`, если есть `label` | Текущее значение справа от подписи |
41
+ | `formatValue` | — | Как печатать число, например ``v => `${v} км` `` |
42
+ | `showValue` | `'auto'` | Пузырёк над бегунком: `'auto'` — пока тянут или фокус, `'always'`, `'never'` |
43
+ | `marks` | — | `true` — точка на каждом шаге, или список: `[0, 50, { value: 100, label: 'Макс' }]` |
44
+ | `size` | `'md'` | `'sm'` (16 px), `'md'` (20 px), `'lg'` (26 px) или диаметр бегунка в px |
45
+ | `disabled` | `false` | Отключён и приглушён |
46
+ | `accent` | `#6ee7a0` | Цвет заливки |
47
+ | `trackColor` | `#2a2a30` | Цвет дорожки. На светлой странице — например, `#d9dae1` |
48
+ | `thumbColor` | `#fff` | Цвет бегунка |
49
+ | `name` | — | Значения уходят с формой скрытыми input |
50
+ | `className` / `style` | — | На корневом элементе |
51
+
52
+ ## ParzSlider
53
+
54
+ | Проп | По умолчанию | Что делает |
55
+ |---|---|---|
56
+ | `value` / `defaultValue` | — / `min` | Значение (управляемый / неуправляемый режим) |
57
+ | `onChange` | — | `(value: number) => void` — при каждом движении |
58
+ | `onChangeEnd` | — | `(value: number) => void` — когда отпустили или после нажатия клавиши |
59
+ | `ariaLabel` | — | Имя для экранных дикторов, если нет `label` |
60
+
61
+ ## ParzRange
62
+
63
+ | Проп | По умолчанию | Что делает |
64
+ |---|---|---|
65
+ | `value` / `defaultValue` | — / 25% и 75% | Значения по возрастанию, **от 2 до 5**. Лишние отбрасываются, недостающие добавляются на `max` |
66
+ | `onChange` | — | `(value: number[]) => void` |
67
+ | `onChangeEnd` | — | `(value: number[]) => void` — удобно загружать результаты здесь |
68
+ | `minDistance` | `0` | Минимальный зазор между соседними бегунками |
69
+ | `fill` | `'span'` | `'span'` — заливка от первого до последнего бегунка, `'pairs'` — только пары 1–2, 3–4 (работа — перерыв — работа) |
70
+ | `ariaLabels` | — | Имена бегунков для экранных дикторов, например `['От', 'До']` |
71
+
72
+ Ограничения тоже экспортируются: `RANGE_MIN_THUMBS` (2) и `RANGE_MAX_THUMBS` (5).
73
+
74
+ ## CSS-переменные
75
+
76
+ `--pz-sl-accent`, `--pz-sl-track`, `--pz-sl-knob`, `--pz-sl-tip-bg`, `--pz-sl-tip-fg` — на ползунке или любом родителе.