@carparkteam/parz-ui 0.0.0-stage → 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,58 @@
1
+ # Parz Checkbox
2
+
3
+ `ParzCheckbox` is a Parz UI checkbox with liquid motion. When checked, color spreads from the center like a drop and slightly overshoots, the box squashes and springs back, and the tick draws itself in. When unchecked, everything gently pulls back in. Nothing jumps.
4
+
5
+ - **tap or click** — on the box or on its label;
6
+ - **press** — the box softly squeezes;
7
+ - **keyboard** — Tab to focus (a visible ring appears), Space or Enter to toggle;
8
+ - **partly selected** — `indeterminate` shows a dash, e.g. for “Select all”;
9
+ - **error** — `invalid` paints the border and fill red;
10
+ - **reduced motion** — with `prefers-reduced-motion` only short fades remain.
11
+
12
+ Under the hood it is a real `<input type="checkbox">`, so forms, `name` / `value`, screen readers and focus work as usual.
13
+
14
+ ```tsx
15
+ import { useState } from 'react';
16
+ import { ParzCheckbox } from '@carparkteam/parz-ui';
17
+ import '@carparkteam/parz-ui/style.css';
18
+
19
+ const [on, setOn] = useState(false);
20
+
21
+ <ParzCheckbox checked={on} onChange={setOn} label="Remember me" />
22
+
23
+ // "Select all" over a list
24
+ const all = items.every(i => i.on), some = items.some(i => i.on);
25
+ <ParzCheckbox checked={all} indeterminate={some && !all} onChange={selectAll} label="Select all" />
26
+
27
+ // must agree
28
+ <ParzCheckbox checked={agree} onChange={setAgree} invalid={submitted && !agree}
29
+ label="I accept the terms" description="Required to continue" />
30
+ ```
31
+
32
+ | Prop | Default | What it does |
33
+ |---|---|---|
34
+ | `checked` | — | State (controlled mode) |
35
+ | `defaultChecked` | `false` | Initial state (uncontrolled mode) |
36
+ | `onChange` | — | `(checked: boolean) => void` — on click or keyboard |
37
+ | `indeterminate` | `false` | Partly selected — a dash instead of the tick |
38
+ | `label` | — | Text or node next to the box |
39
+ | `description` | — | Small helper text under the label |
40
+ | `labelPosition` | `'end'` | `'end'` — label on the right, `'start'` — on the left |
41
+ | `ariaLabel` | — | Name for screen readers when there is no visible `label` |
42
+ | `disabled` | `false` | Turned off, dimmed, not focusable |
43
+ | `invalid` | `false` | Error state: red border and fill, `aria-invalid` |
44
+ | `size` | `'md'` | `'sm'` (16 px), `'md'` (20 px), `'lg'` (26 px) or box side in px |
45
+ | `radius` | about a third of size | Corner radius, px |
46
+ | `accent` | `#6ee7a0` | Fill color when checked |
47
+ | `borderColor` | `#4a4a55` | Border when unchecked. On a light page use something like `#b9bac6` |
48
+ | `checkColor` | `#04130a` | Tick color |
49
+ | `name` / `value` / `id` | — | Passed to the inner checkbox |
50
+ | `className` / `style` | — | On the root `<label>` |
51
+
52
+ ## CSS variables
53
+
54
+ Colors can also be set from CSS on the checkbox or any parent: `--pz-cb-accent`, `--pz-cb-border`, `--pz-cb-check`, `--pz-cb-invalid`.
55
+
56
+ ```css
57
+ .form { --pz-cb-accent: #7aa7ff; --pz-cb-check: #fff; }
58
+ ```
@@ -0,0 +1,58 @@
1
+ # Parz Checkbox
2
+
3
+ `ParzCheckbox`-ը Parz UI-ի նշիչն է՝ հեղուկ անիմացիայով։ Նշելիս գույնը կենտրոնից տարածվում է կաթիլի պես և թեթև անցնում սահմանը, քառակուսին տափակում է ու հետ զսպանակում, իսկ նշանը գծվում է։ Հանելիս ամեն ինչ մեղմ հետ է քաշվում։ Ոչինչ կտրուկ չէ։
4
+
5
+ - **սեղմում կամ կտտոց** — քառակուսու կամ պիտակի վրա;
6
+ - **պահում** — քառակուսին մեղմ սեղմվում է;
7
+ - **ստեղնաշար** — Tab-ը տալիս է ֆոկուս (երևում է օղակ), Space կամ Enter՝ փոխարկում;
8
+ - **մասամբ ընտրված** — `indeterminate`-ը ցույց է տալիս գծիկ, օրինակ «Ընտրել բոլորը»-ի համար;
9
+ - **սխալ** — `invalid`-ը եզրագիծը և լցումը ներկում է կարմիր;
10
+ - **քիչ շարժում** — `prefers-reduced-motion`-ի դեպքում մնում են միայն կարճ գունային անցումներ։
11
+
12
+ Ներսում իսկական `<input type="checkbox">` է, ուստի ձևերը, `name` / `value`-ն, էկրանի ընթերցողները և ֆոկուսը աշխատում են սովորականի պես։
13
+
14
+ ```tsx
15
+ import { useState } from 'react';
16
+ import { ParzCheckbox } from '@carparkteam/parz-ui';
17
+ import '@carparkteam/parz-ui/style.css';
18
+
19
+ const [on, setOn] = useState(false);
20
+
21
+ <ParzCheckbox checked={on} onChange={setOn} label="Հիշել ինձ" />
22
+
23
+ // «Ընտրել բոլորը» ցուցակի վրա
24
+ const all = items.every(i => i.on), some = items.some(i => i.on);
25
+ <ParzCheckbox checked={all} indeterminate={some && !all} onChange={selectAll} label="Ընտրել բոլորը" />
26
+
27
+ // պարտադիր համաձայնություն
28
+ <ParzCheckbox checked={agree} onChange={setAgree} invalid={submitted && !agree}
29
+ label="Ընդունում եմ պայմանները" description="Առանց դրա շարունակել հնարավոր չէ" />
30
+ ```
31
+
32
+ | Prop | Լռելյայն | Ինչ է անում |
33
+ |---|---|---|
34
+ | `checked` | — | Վիճակ (կառավարվող ռեժիմ) |
35
+ | `defaultChecked` | `false` | Սկզբնական վիճակ (չկառավարվող ռեժիմ) |
36
+ | `onChange` | — | `(checked: boolean) => void` — կտտոցով կամ ստեղնաշարով |
37
+ | `indeterminate` | `false` | Մասամբ ընտրված՝ նշանի փոխարեն գծիկ |
38
+ | `label` | — | Տեքստ կամ տարր նշիչի կողքին |
39
+ | `description` | — | Փոքր բացատրող տեքստ պիտակի տակ |
40
+ | `labelPosition` | `'end'` | `'end'` — պիտակը աջից, `'start'` — ձախից |
41
+ | `ariaLabel` | — | Անուն էկրանի ընթերցողների համար, եթե տեսանելի `label` չկա |
42
+ | `disabled` | `false` | Անջատված, խամրած, ֆոկուս չի ստանում |
43
+ | `invalid` | `false` | Սխալ՝ կարմիր եզրագիծ և լցում, `aria-invalid` |
44
+ | `size` | `'md'` | `'sm'` (16 px), `'md'` (20 px), `'lg'` (26 px) կամ քառակուսու կողմը px-ով |
45
+ | `radius` | չափի մոտ մեկ երրորդը | Անկյունների կլորացում, px |
46
+ | `accent` | `#6ee7a0` | Լցման գույնը, երբ նշված է |
47
+ | `borderColor` | `#4a4a55` | Եզրագիծը, երբ նշված չէ։ Բաց էջում՝ օրինակ `#b9bac6` |
48
+ | `checkColor` | `#04130a` | Նշանի գույնը |
49
+ | `name` / `value` / `id` | — | Փոխանցվում են ներքին checkbox-ին |
50
+ | `className` / `style` | — | Արմատային `<label>`-ի վրա |
51
+
52
+ ## CSS փոփոխականներ
53
+
54
+ Գույները կարելի է տալ նաև CSS-ով՝ նշիչի կամ ցանկացած ծնողի վրա՝ `--pz-cb-accent`, `--pz-cb-border`, `--pz-cb-check`, `--pz-cb-invalid`։
55
+
56
+ ```css
57
+ .form { --pz-cb-accent: #7aa7ff; --pz-cb-check: #fff; }
58
+ ```
@@ -0,0 +1,58 @@
1
+ # Parz Checkbox
2
+
3
+ `ParzCheckbox` — чекбокс Parz UI с жидкой анимацией. При отметке цвет растекается из центра каплей и чуть перелетает, квадратик сплющивается и отпружинивает, а галочка дорисовывается штрихом. При снятии всё мягко втягивается обратно. Ничего резкого.
4
+
5
+ - **нажатие или клик** — по квадратику или по подписи;
6
+ - **удержание** — квадратик мягко поджимается;
7
+ - **клавиатура** — Tab ставит фокус (появляется заметное кольцо), Пробел или Enter переключают;
8
+ - **частично выбрано** — `indeterminate` показывает чёрточку, например у «Выбрать все»;
9
+ - **ошибка** — `invalid` красит рамку и заливку в красный;
10
+ - **меньше движения** — при `prefers-reduced-motion` остаются только короткие переходы цвета.
11
+
12
+ Внутри — настоящий `<input type="checkbox">`, поэтому формы, `name` / `value`, экранные дикторы и фокус работают как обычно.
13
+
14
+ ```tsx
15
+ import { useState } from 'react';
16
+ import { ParzCheckbox } from '@carparkteam/parz-ui';
17
+ import '@carparkteam/parz-ui/style.css';
18
+
19
+ const [on, setOn] = useState(false);
20
+
21
+ <ParzCheckbox checked={on} onChange={setOn} label="Запомнить меня" />
22
+
23
+ // «Выбрать все» над списком
24
+ const all = items.every(i => i.on), some = items.some(i => i.on);
25
+ <ParzCheckbox checked={all} indeterminate={some && !all} onChange={selectAll} label="Выбрать все" />
26
+
27
+ // обязательное согласие
28
+ <ParzCheckbox checked={agree} onChange={setAgree} invalid={submitted && !agree}
29
+ label="Принимаю условия" description="Без этого продолжить нельзя" />
30
+ ```
31
+
32
+ | Проп | По умолчанию | Что делает |
33
+ |---|---|---|
34
+ | `checked` | — | Состояние (управляемый режим) |
35
+ | `defaultChecked` | `false` | Начальное состояние (неуправляемый режим) |
36
+ | `onChange` | — | `(checked: boolean) => void` — при клике или с клавиатуры |
37
+ | `indeterminate` | `false` | Частично выбрано — чёрточка вместо галочки |
38
+ | `label` | — | Текст или элемент рядом с флажком |
39
+ | `description` | — | Мелкий поясняющий текст под подписью |
40
+ | `labelPosition` | `'end'` | `'end'` — подпись справа, `'start'` — слева |
41
+ | `ariaLabel` | — | Имя для экранных дикторов, если нет видимой `label` |
42
+ | `disabled` | `false` | Отключён, приглушён, не получает фокус |
43
+ | `invalid` | `false` | Ошибка: красная рамка и заливка, `aria-invalid` |
44
+ | `size` | `'md'` | `'sm'` (16 px), `'md'` (20 px), `'lg'` (26 px) или сторона квадрата в px |
45
+ | `radius` | около трети размера | Скругление углов, px |
46
+ | `accent` | `#6ee7a0` | Цвет заливки, когда отмечен |
47
+ | `borderColor` | `#4a4a55` | Рамка, когда не отмечен. На светлой странице — например, `#b9bac6` |
48
+ | `checkColor` | `#04130a` | Цвет галочки |
49
+ | `name` / `value` / `id` | — | Передаются во внутренний чекбокс |
50
+ | `className` / `style` | — | На корневой `<label>` |
51
+
52
+ ## CSS-переменные
53
+
54
+ Цвета можно задать и из CSS — на самом флажке или любом родителе: `--pz-cb-accent`, `--pz-cb-border`, `--pz-cb-check`, `--pz-cb-invalid`.
55
+
56
+ ```css
57
+ .form { --pz-cb-accent: #7aa7ff; --pz-cb-check: #fff; }
58
+ ```
@@ -0,0 +1,394 @@
1
+ # Parz Menu
2
+
3
+ **English** · [Հայերեն](parz-menu.hy.md) · [Русский](parz-menu.ru.md)
4
+
5
+ The CarPark bottom navigation, packaged as a React component. The active-item highlight flows between tabs like a drop of liquid, and the round buttons on the right respond to taps with a springy bounce. Everything about the look is configurable: colors, sizes, corner radii, glass, animation speed and strength.
6
+
7
+ - 5–6 items with an icon and a label, a number or dot badge on any item;
8
+ - round buttons on the right: an icon or an avatar (letters or a photo), also with a badge;
9
+ - icons: built-in, Material Symbols, Material Icons or any React element of your own;
10
+ - three layouts: icon above label, inline, icons only;
11
+ - sticks to the bottom or top of the screen, or sits in the page flow;
12
+ - built-in labels in English, Armenian and Russian (`locale`);
13
+ - respects the system “reduce motion” setting and works with the keyboard.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @carparkteam/parz-ui
19
+ ```
20
+
21
+ ```tsx
22
+ import { ParzMenu } from '@carparkteam/parz-ui';
23
+ import '@carparkteam/parz-ui/style.css';
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ```tsx
29
+ import { useState } from 'react';
30
+ import { ParzMenu } from '@carparkteam/parz-ui';
31
+ import '@carparkteam/parz-ui/style.css';
32
+
33
+ export function AppNav() {
34
+ const [tab, setTab] = useState('garage');
35
+ const [panel, setPanel] = useState<string | null>(null);
36
+
37
+ return (
38
+ <ParzMenu
39
+ items={[
40
+ { id: 'garage', label: 'Garage', icon: 'ms:directions_car' },
41
+ { id: 'parking', label: 'Parking', icon: 'ms:local_parking', badge: 2 },
42
+ { id: 'map', label: 'Map', icon: 'pin' }
43
+ ]}
44
+ value={panel ?? tab}
45
+ onChange={id => { setPanel(null); setTab(id); }}
46
+ actions={[
47
+ { id: 'notif', ariaLabel: 'Notifications', icon: 'bell', badge: 'dot' },
48
+ { id: 'profile', ariaLabel: 'Profile', avatar: { text: 'A' } }
49
+ ]}
50
+ activeAction={panel}
51
+ onAction={id => setPanel(p => (p === id ? null : id))}
52
+ />
53
+ );
54
+ }
55
+ ```
56
+
57
+ When a section opened from a round button is showing, pass its id to `value`: the drop under the items hides, because there is no such item. That is how CarPark does it.
58
+
59
+ ## Props
60
+
61
+ | Prop | Type | Default | What it does |
62
+ |---|---|---|---|
63
+ | `items` | `TabItem[]` | — | Menu items |
64
+ | `value` | `string \| null` | — | Active item (controlled mode). No such item or `null` — the drop hides |
65
+ | `defaultValue` | `string` | first item | Active item without outside control |
66
+ | `onChange` | `(id) => void` | — | An item was tapped |
67
+ | `actions` | `ActionItem[]` | `[]` | Round buttons on the right, two at most. Anything: a bell, a profile, “+”, search… |
68
+ | `activeAction` | `string \| null` | `null` | Which round button is highlighted |
69
+ | `onAction` | `(id) => void` | — | A round button was tapped |
70
+ | `hideActions` | `boolean` | `false` | Hide the round buttons. The menu shrinks to its items and centers |
71
+ | `position` | `'bottom' \| 'top' \| 'static'` | `'bottom'` | Stick to the bottom or top of the screen, or sit in the flow |
72
+ | `layout` | `'stacked' \| 'inline' \| 'icons'` | `'stacked'` | Icon above label, inline, or icons only |
73
+ | `theme` | `Partial<ParzMenuTheme>` | — | Colors and sizes, see below |
74
+ | `glass` | `number \| GlassPreset` | — | Glass transparency: 0…100 % or a preset, see “Glass” |
75
+ | `overflow` | `'more' \| 'none'` | `'more'` | What to do when items don’t fit: hide them in a “More” button or not |
76
+ | `minItemWidth` | `number` | 64 / 104 / 52 | Below this width an item moves into “More”, px. Default depends on `layout` |
77
+ | `locale` | `'en' \| 'ru' \| 'hy'` | `'en'` | Language of the built-in labels: “More”, search, badge announcements. Your own `moreLabel`, `ariaLabel`, `searchPlaceholder` take priority |
78
+ | `moreLabel` | `string` | from `locale` | Label of the “More” button |
79
+ | `mobileSearch` | `boolean \| MobileSearchOptions` | — | iOS-style search on narrow screens, see “Search on phones” |
80
+ | `desktop` | `boolean \| DesktopOptions` | — | **Beta.** Header for large screens, see “Desktop header” |
81
+ | `moreIcon` | `IconSpec` | `'more'` | Icon of the “More” button. By default three dots in a circle: outlined, filled while a hidden item is open |
82
+ | `motion` | `Partial<LiquidMotion>` | — | Animations, see below |
83
+ | `maxBadge` | `number` | `99` | Larger numbers show as “99+” |
84
+ | `badgeLabel` | `(label, badge) => string` | — | How a screen reader reads an item with a badge |
85
+ | `autoLoadFonts` | `boolean` | `true` | Load Material fonts automatically when icons use them |
86
+ | `ariaLabel` | `string` | from `locale` | Screen reader label of the whole menu |
87
+ | `className`, `style` | — | — | The usual |
88
+
89
+ ### TabItem
90
+
91
+ | Field | Type | What it does |
92
+ |---|---|---|
93
+ | `id` | `string` | Unique key |
94
+ | `label` | `string` | Label |
95
+ | `icon` | `IconSpec` | Icon, see “Icons” |
96
+ | `activeIcon` | `IconSpec` | Custom icon for the active state. Not needed for built-in and Material Symbols icons |
97
+ | `badge` | `number \| string \| 'dot' \| boolean` | Badge. `0`, `false`, `null` — no badge |
98
+ | `disabled` | `boolean` | The item is disabled |
99
+ | `ariaLabel` | `string` | Screen reader label instead of `label` |
100
+
101
+ ### ActionItem
102
+
103
+ | Field | Type | What it does |
104
+ |---|---|---|
105
+ | `id` | `string` | Unique key |
106
+ | `ariaLabel` | `string` | Screen reader label. Required: the button has no text |
107
+ | `icon`, `activeIcon` | `IconSpec` | Icon |
108
+ | `avatar` | `{ text?: string; src?: string }` | Avatar instead of an icon: letters or a photo |
109
+ | `badge` | same as an item | Badge |
110
+ | `variant` | `'default' \| 'accent'` | `accent` — a color-filled button for the main action, such as “+” |
111
+
112
+ ## Right buttons
113
+
114
+ The group on the right holds up to two round buttons of any kind. The bell and avatar in the examples are just the most common choice. Pass more than two and the first two are shown, with a warning in the console.
115
+
116
+ A button comes in three kinds:
117
+ - **icon** — like the bell: `{ id: 'notif', ariaLabel: 'Notifications', icon: 'bell' }`;
118
+ - **accent** — filled with color, for the main action: `{ id: 'add', ariaLabel: 'Add', icon: 'plus', variant: 'accent' }`;
119
+ - **avatar** — letters or a photo: `{ id: 'profile', ariaLabel: 'Profile', avatar: { src: '/me.jpg' } }`.
120
+
121
+ A button can open its own section (then pass its id to `activeAction` — it lights up and the drop under the items hides) or just perform an action, like “+”: then nothing needs highlighting.
122
+
123
+ ```tsx
124
+ <ParzMenu
125
+ items={items}
126
+ value={panel ?? tab}
127
+ onChange={id => { setPanel(null); setTab(id); }}
128
+ actions={[
129
+ { id: 'notif', ariaLabel: 'Notifications', icon: 'bell', badge: 3 },
130
+ { id: 'add', ariaLabel: 'Add', icon: 'plus', variant: 'accent' }
131
+ ]}
132
+ activeAction={panel}
133
+ onAction={id => (id === 'add' ? openCreateSheet() : setPanel(p => (p === id ? null : id)))}
134
+ />
135
+ ```
136
+
137
+ ## Adding items on the fly
138
+
139
+ Items and right buttons can be added and removed at any moment — say, a script turns on an “Offers” item when a sale starts. Nothing jumps:
140
+
141
+ - a leaving item or button collapses smoothly, its neighbors slide into its place;
142
+ - a new one “grows” from zero with a springy overshoot;
143
+ - the “More” button appears and disappears the same way when items stop or start fitting;
144
+ - the drop travels together with its item.
145
+
146
+ The duration is the same `motion.shift`. Just change the `items` or `actions` array:
147
+
148
+ ```tsx
149
+ const items = [garage, parking, ...(saleIsOn ? [sale] : [])];
150
+ ```
151
+
152
+ ## Menu without right buttons
153
+
154
+ If you don’t pass `actions` or set `hideActions`, there is no right group. Toggling `hideActions` is animated: the group folds away, the item capsule smoothly narrows and moves to the center, the drop follows its item. `motion.shift` sets the speed. The item capsule then doesn’t stretch to full width but shrinks to its content and centers. The freed space can go to items: 5–6 fit comfortably on a phone.
155
+
156
+ ```tsx
157
+ <ParzMenu
158
+ items={[
159
+ { id: 'home', label: 'Home', icon: 'ms:home' },
160
+ { id: 'search', label: 'Search', icon: 'ms:search' },
161
+ { id: 'parking', label: 'Parking', icon: 'ms:local_parking', badge: 2 },
162
+ { id: 'saved', label: 'Saved', icon: 'ms:favorite' },
163
+ { id: 'profile', label: 'Profile', icon: 'ms:person', badge: 'dot' }
164
+ ]}
165
+ value={tab}
166
+ onChange={setTab}
167
+ />
168
+ ```
169
+
170
+ Items are equally wide; `theme.itemMinWidth` (84 px) sets the width. The capsule is “items × that width” wide, but no wider than the screen. If items don’t fit, they share the width equally and long labels get an ellipsis — `layout="icons"` works better then.
171
+
172
+ ## When items don’t fit
173
+
174
+ The component watches its own width. When an item gets narrower than `minItemWidth` (64 px for the “icon above label” layout), the last items move into a “More” button (three dots in a circle). Tapping it pops up a list of the hidden items: icon, label and badge.
175
+
176
+ - If a hidden item is open, the drop sits under “More” and the item is highlighted in the list.
177
+ - Badges of hidden items gather on “More”: numbers add up, and if there are no numbers a dot appears.
178
+ - The list closes on choosing an item, tapping “More” again, clicking outside, or the Esc key.
179
+ - The width is recalculated on resize, so tablets and desktops show more items.
180
+
181
+ ```tsx
182
+ <ParzMenu items={tenItems} value={tab} onChange={setTab} moreLabel="Other" />
183
+ ```
184
+
185
+ To never hide anything, set `overflow="none"` — items just shrink then.
186
+
187
+ ## Search on phones
188
+
189
+ `mobileSearch` adds iOS-style search on narrow screens. The magnifier becomes the first button in the right group; only the last of the `actions` stays, so the group still has two buttons.
190
+
191
+ Tapping the magnifier makes the group flow, liquid-like, to the full width of the menu while the item capsule collapses. Inside:
192
+ - on the left the magnifier turns into a smaller “✕” — closes search and clears the field, the items come back;
193
+ - in the middle is the input, with the keyboard already open;
194
+ - on the right, as a separate round button in its own glass, is “→” (search); it rolls out from behind the group and lights up in the accent color once the field has text.
195
+
196
+ ```tsx
197
+ <ParzMenu
198
+ items={items}
199
+ actions={[bell, profile]} // on a phone the group shows the magnifier and the profile
200
+ mobileSearch={{ searchPlaceholder: 'Make or parking', onSearch: q => navigate(`/search?q=${q}`) }}
201
+ />
202
+ ```
203
+
204
+ The fields are the same as for the header search: `searchPlaceholder`, `searchValue` / `onSearchChange`, `onSearch`. On a wide screen with the header enabled, the header search is used.
205
+
206
+ ## Desktop header (beta)
207
+
208
+ On a wide screen the menu can turn into a full top header: logo on the left, then inline items, search in the middle, buttons on the right. Below the header is a blurred shade, so content slides nicely under it while scrolling. On a narrow screen the menu stays a regular bottom bar.
209
+
210
+ ```tsx
211
+ <ParzMenu
212
+ items={items}
213
+ value={tab}
214
+ onChange={setTab}
215
+ actions={actions}
216
+ desktop={{
217
+ logo: 'CARPARK', // a string or any element: <img>, svg, an animated logo
218
+ search: 'expand', // 'bar' | 'button' | 'expand' | 'none'
219
+ searchPlaceholder: 'Make, model or parking…',
220
+ onSearch: q => navigate(`/search?q=${q}`)
221
+ }}
222
+ />
223
+ ```
224
+
225
+ | Field | Default | What it does |
226
+ |---|---|---|
227
+ | `breakpoint` | `1100` | Width from which the header turns on, px |
228
+ | `breakpointBy` | `'window'` | Measure the window or the menu’s parent (`'parent'` — for narrow containers and previews) |
229
+ | `logo` | — | Logo on the left: a string or a React element |
230
+ | `search` | `'bar'` | `bar` — a separate search bar; `button` — a round search button in the right group; `expand` — a button in the right group that expands into a bar right inside the group; `none` — no search |
231
+ | `searchPlaceholder` | from `locale` | Placeholder in the bar |
232
+ | `searchValue` / `onSearchChange` | — | Search text, if you want to control it yourself |
233
+ | `onSearch` | — | Enter was pressed (or the search button in the expanded bar) |
234
+ | `onSearchClick` | — | The search button of the `button` kind was pressed — for example, to open your own search |
235
+ | `searchShortcut` | `'/'` | Key that opens search; `false` — off |
236
+ | `searchWidth` | `420` / `320` | Search bar width, px: 420 for the separate bar, 320 when expanded inside the group |
237
+ | `searchAt` | `'start'` | Where the search button sits in the right group (`button`, `expand`, and the phone search): `start` — first, `end` — last |
238
+ | `actionsAlign` | `'end'` | Where the right group sits when there is no search bar: `end` — at the right edge, `next` — right after the items |
239
+ | `layout` | `'inline'` | Item layout in the header |
240
+ | `maxWidth` | `1360` | Header content width, px |
241
+ | `height` | `80` | Header height, px |
242
+
243
+ Header colors are in the theme: `headerTop`, `headerMid` (shade), `headerBlur` (blur), `searchBg`, `searchBorder` (search bar). If items don’t fit next to the logo and search, the same “More” works, only its list opens downward. All header transitions are smooth: switching the search kind (the bar collapses, the button “grows” in the group), moving the group between the edge and the items, the logo appearing or changing, switching between header and bottom bar on resize.
244
+
245
+ The search button in the `button` and `expand` kinds is the first in the right group and doesn’t count toward the two-button limit. The expanding search stretches the whole group; if items get cramped, the extra ones glide into “More”. It closes on Esc or a click outside when nothing is typed.
246
+
247
+ ## Badges
248
+
249
+ ```tsx
250
+ { id: 'fines', label: 'Fines', icon: 'ms:receipt_long', badge: 3 } // number
251
+ { id: 'fines', label: 'Fines', icon: 'ms:receipt_long', badge: 150 } // shows “99+”
252
+ { id: 'news', label: 'New', icon: 'sparkles', badge: 'dot' } // dot
253
+ { id: 'sale', label: 'Offers', icon: 'gift', badge: 'new' } // text
254
+ ```
255
+
256
+ A badge appears with a springy “hop” and hops again when the number changes. Colors: `badgeBg` and `badgeText` for numbers, `accent` for the dot.
257
+
258
+ ## Icons
259
+
260
+ | Value | What it is |
261
+ |---|---|
262
+ | `'pin'` | Built-in icon. On the active item the outline turns into a fill |
263
+ | `'ms:home'` | Material Symbols Rounded. On the active item the fill flows in smoothly |
264
+ | `'ms:home:outlined'`, `'ms:home:sharp'` | Material Symbols in another style |
265
+ | `'mi:home'` | Classic Material Icons (filled) |
266
+ | `'mi:home:outlined'` | Also `round`, `sharp`, `two-tone` |
267
+ | `<MyIcon />` | Any React element: svg, image, an icon from any library |
268
+
269
+ Built-in: `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`.
270
+
271
+ The component loads Material fonts from Google Fonts by itself when it sees `ms:` or `mi:`. If you already load the fonts or Google Fonts is unavailable, turn it off: `autoLoadFonts={false}`. Icon names are at [fonts.google.com/icons](https://fonts.google.com/icons).
272
+
273
+ For your own icons, set a different active look with `activeIcon`:
274
+
275
+ ```tsx
276
+ { id: 'home', label: 'Home', icon: <HomeOutline />, activeIcon: <HomeFilled /> }
277
+ ```
278
+
279
+ ## Language
280
+
281
+ `locale` switches the component’s own labels: the “More” button, the menu label for screen readers, search placeholders and buttons, and how a dot badge is announced.
282
+
283
+ | `locale` | “More” | Search | Dot badge |
284
+ |---|---|---|---|
285
+ | `'en'` (default) | More | Search | “Parking, new” |
286
+ | `'hy'` | Մենյու | Որոնում | “Կայանատեղի, նոր” |
287
+ | `'ru'` | Меню | Поиск | “Парковка, есть новое” |
288
+
289
+ ```tsx
290
+ <ParzMenu locale="hy" items={items} />
291
+ ```
292
+
293
+ Item labels and `ariaLabel` of the buttons are yours — translate them in your app. The strings themselves are exported as `LTB_STRINGS` if you need them elsewhere.
294
+
295
+ ## Theme
296
+
297
+ All fields are optional. Colors are any CSS colors, sizes are numbers in pixels.
298
+
299
+ | Field | Default | What it sets |
300
+ |---|---|---|
301
+ | `accent` | `#6ee7a0` | Dot badge, focus ring, active avatar outline |
302
+ | `text` / `textActive` | `#8b8b93` / `#fff` | Labels and icons |
303
+ | `surfaceTop` / `surfaceBottom` | dark glass | Capsule gradient |
304
+ | `border` | `rgba(255,255,255,.06)` | Thin capsule border |
305
+ | `bevelTop` / `bevelBottom` | white 14% / black 40% | Capsule bevel: a line on top and a shade below. On a light theme make the bottom one almost transparent |
306
+ | `highlightTop` / `highlightBottom` | white 13% / 5% | Drop gradient |
307
+ | `highlightShadow` | thin highlight | Drop shadow and highlight (CSS box-shadow). On a light theme — a soft shadow below |
308
+ | `actionBg` | white 7% | Round button background |
309
+ | `actionActiveBg` / `actionActiveText` | `#fff` / `#000` | Pressed round button |
310
+ | `actionAccentBg` / `actionAccentText` | `#6ee7a0` / `#04130a` | Accent round button (`variant: 'accent'`) |
311
+ | `badgeBg` / `badgeText` | `#ff4d5e` / `#fff` | Number badge |
312
+ | `badgeRing` | `#1d1d21` | Ring around the badge — use the bar color |
313
+ | `avatarFrom` / `avatarTo` / `avatarText` | green gradient | Avatar |
314
+ | `fontFamily` | `inherit` | Label font |
315
+ | `radius` | `999` | Capsule and drop corner radius |
316
+ | `padding` | `6` | Padding inside capsules |
317
+ | `gap` | `10` | Space between capsules |
318
+ | `itemPaddingY` | `8` | Item height |
319
+ | `iconSize` | `22` | Icon size |
320
+ | `fontSize` | `11` | Label size |
321
+ | `actionSize` | `50` | Round button diameter |
322
+ | `blur` | `20` | Glass blur |
323
+ | `maxWidth` | `540` | Menu width on wide screens |
324
+ | `offset` | `12` | Offset from the screen edge |
325
+ | `itemMinWidth` | `84` | Item width in a menu without right buttons |
326
+ | `shadow` | soft shadow | Shadow under the capsules |
327
+ | `headerTop` / `headerMid` | near-black 96% / 82% | Shade under the header on large screens: top and middle, fades out below |
328
+ | `headerBlur` | `14` | Blur under the header |
329
+ | `searchBg` / `searchBorder` | white 6% / 8% | Search bar background and border |
330
+
331
+ The same can be set in CSS — every field is a `--ltb-*` variable:
332
+
333
+ ```css
334
+ .my-bar {
335
+ --ltb-accent: #ff7a00;
336
+ --ltb-surface-top: rgba(255, 255, 255, .8);
337
+ --ltb-surface-bottom: rgba(240, 240, 245, .9);
338
+ --ltb-text: #6b6b75;
339
+ --ltb-text-active: #111;
340
+ }
341
+ ```
342
+
343
+ ```tsx
344
+ <ParzMenu className="my-bar" items={items} />
345
+ ```
346
+
347
+ ## Glass
348
+
349
+ The `glass` prop sets how transparent the capsule background is: a number from 0 to 100 (percent transparency) or a ready preset. The color stays from the theme (`surfaceTop` / `surfaceBottom`); only the transparency changes.
350
+
351
+ | Preset | Transparency | Blur | Looks like |
352
+ |---|---|---|---|
353
+ | `'solid'` | 0 % | 0 | A solid plate, content doesn’t show through |
354
+ | `'frosted'` | 15 % | 20 px | Frosted glass — like in CarPark |
355
+ | `'glass'` | 40 % | 16 px | Noticeably see-through |
356
+ | `'clear'` | 70 % | 10 px | Almost transparent, held together by blur and border |
357
+ | `'none'` | 100 % | 0 | No background — only icons, labels and the drop |
358
+
359
+ ```tsx
360
+ <ParzMenu glass="glass" items={items} /> // preset
361
+ <ParzMenu glass={55} items={items} /> // 55 % transparency, blur from the theme
362
+ ```
363
+
364
+ A preset sets the blur too. Need your own blur — pass `glass` as a number and set the blur in `theme.blur`. Without `glass` the transparency comes from the theme colors as is. For colors in `hsl()`, `oklch()` and names like `navy` the transparency doesn’t change — use `#rrggbb` or `rgb()`.
365
+
366
+ ## Animations
367
+
368
+ | Field | Default | What it sets |
369
+ |---|---|---|
370
+ | `lead` | `360` | Duration of the drop’s leading edge, ms |
371
+ | `lag` | `660` | Duration of the trailing edge, ms. The bigger the gap to `lead`, the more the drop stretches |
372
+ | `squash` | `0.26` | How much the drop flattens in flight. `0` — not at all |
373
+ | `overshoot` | `1.35` | Springiness: `1` — no overshoot, `1.6` — strong |
374
+ | `jelly` | `true` | Springy response of round buttons and a “splash” of their capsule |
375
+ | `wave` | `0.035` | A springy wave of the whole item capsule: on a switch — in the drop’s direction, on returning from the right buttons — to the left, on tapping the open item again — from the center. `0` — off, `0.08` — strong |
376
+ | `shift` | `550` | How many ms the right group takes to appear or hide while items move to the center |
377
+
378
+ ```tsx
379
+ <ParzMenu motion={{ lead: 280, lag: 820, squash: 0.35 }} items={items} />
380
+ ```
381
+
382
+ With the system “reduce motion” setting the drop just moves quickly, without stretching or springiness.
383
+
384
+ ## Accessibility
385
+
386
+ - The menu is a `nav` element, the active item has `aria-current="page"`, round buttons use `aria-pressed`.
387
+ - A badge is read together with its item: “Fines, 3”, “New, new”. Change the wording with `badgeLabel` or the language with `locale`.
388
+ - Everything works from the keyboard: Tab and Enter, with a visible focus ring.
389
+
390
+ ## What’s not here
391
+
392
+ - Your own messages, routing and page state: the component only shows the menu and reports taps.
393
+ - Dragging items and swiping between tabs.
394
+ - Support for browsers without `backdrop-filter`: capsules there will have a background but no blur.