prime-ui-kit 0.9.1 → 0.9.2

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.
Files changed (107) hide show
  1. package/README.md +22 -8
  2. package/dist/chunks/{chunk-KCQFOORX.js → chunk-IQELQNMR.js} +1 -1
  3. package/dist/chunks/{chunk-DB2Y3XXA.js → chunk-LXGRKN34.js} +2323 -1500
  4. package/dist/chunks/{chunk-NHOWNIAR.js → chunk-ZEFCDEEQ.js} +790 -536
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/data-table/DataTable.d.ts +6 -1
  7. package/dist/components/data-table/types.d.ts +5 -0
  8. package/dist/components/dnd/Dnd.d.ts +1 -1
  9. package/dist/components/dnd/dragSession.d.ts +9 -0
  10. package/dist/components/dnd/useDropTarget.d.ts +5 -0
  11. package/dist/components/dnd/useFlipList.d.ts +3 -3
  12. package/dist/components/drawer/Drawer.d.ts +8 -5
  13. package/dist/components/dropdown/Dropdown.d.ts +17 -0
  14. package/dist/components/index.d.ts +5 -1
  15. package/dist/components/kanban/Kanban.d.ts +129 -0
  16. package/dist/components/modal/Modal.d.ts +5 -2
  17. package/dist/components/page-toolbar/PageToolbar.d.ts +53 -0
  18. package/dist/components/segmented-control/SegmentedControl.d.ts +5 -2
  19. package/dist/components/tabs/Tabs.d.ts +5 -2
  20. package/dist/hooks/useEdgeOverflow.d.ts +12 -0
  21. package/dist/hooks/useMediaQuery.d.ts +7 -0
  22. package/dist/hooks/useSwipeDismiss.d.ts +34 -0
  23. package/dist/index.css +2271 -1163
  24. package/dist/index.js +10 -2
  25. package/dist/internal/overlay/FloatingPanel.d.ts +3 -1
  26. package/dist/internal/overlay/useFloatingLayer.d.ts +11 -1
  27. package/dist/internal/touchTarget.d.ts +4 -0
  28. package/dist/layout/app-header/AppHeader.d.ts +110 -0
  29. package/dist/layout/app-shell/AppShell.d.ts +14 -9
  30. package/dist/layout/bottom-nav/BottomNav.d.ts +80 -0
  31. package/dist/layout/index.d.ts +5 -1
  32. package/dist/tokens/primitives.d.ts +4 -0
  33. package/dist/tokens/semantic.d.ts +56 -0
  34. package/dist/tokens/themes/dark.d.ts +4 -0
  35. package/package.json +3 -2
  36. package/src/components/accordion/COMPONENT.md +1 -1
  37. package/src/components/badge/COMPONENT.md +2 -2
  38. package/src/components/banner/COMPONENT.md +1 -1
  39. package/src/components/checkbox/examples/examples.module.css +4 -2
  40. package/src/components/crossfade/COMPONENT.md +1 -1
  41. package/src/components/data-table/COMPONENT.md +19 -6
  42. package/src/components/data-table/examples/columns-visibility.tsx +167 -0
  43. package/src/components/digit-input/COMPONENT.md +10 -10
  44. package/src/components/digit-input/examples/examples.module.css +8 -0
  45. package/src/components/digit-input/examples/in-form.tsx +2 -5
  46. package/src/components/digit-input/examples/sizes.tsx +1 -1
  47. package/src/components/divider/COMPONENT.md +1 -1
  48. package/src/components/dnd/COMPONENT.md +3 -3
  49. package/src/components/drawer/COMPONENT.md +17 -9
  50. package/src/components/drawer/examples/date-sheet.tsx +41 -0
  51. package/src/components/drawer/examples/examples.module.css +5 -0
  52. package/src/components/drawer/examples/placement.tsx +4 -2
  53. package/src/components/dropdown/COMPONENT.md +19 -5
  54. package/src/components/dropdown/examples/checkbox-items.tsx +48 -0
  55. package/src/components/empty-page/COMPONENT.md +1 -1
  56. package/src/components/kanban/COMPONENT.md +181 -0
  57. package/src/components/kanban/examples/controlled.tsx +78 -0
  58. package/src/components/kanban/examples/examples.module.css +31 -0
  59. package/src/components/kanban/examples/limits.tsx +58 -0
  60. package/src/components/kanban/examples/narrow.tsx +76 -0
  61. package/src/components/kanban/examples/overview.tsx +130 -0
  62. package/src/components/kanban/examples/states.tsx +75 -0
  63. package/src/components/kanban/examples/structure.tsx +76 -0
  64. package/src/components/login-form/COMPONENT.md +1 -1
  65. package/src/components/modal/COMPONENT.md +9 -6
  66. package/src/components/notification/COMPONENT.md +2 -2
  67. package/src/components/page-content/COMPONENT.md +1 -1
  68. package/src/components/page-toolbar/COMPONENT.md +142 -0
  69. package/src/components/page-toolbar/examples/examples.module.css +16 -0
  70. package/src/components/page-toolbar/examples/narrow.tsx +103 -0
  71. package/src/components/page-toolbar/examples/overview.tsx +113 -0
  72. package/src/components/page-toolbar/examples/save-changes.tsx +40 -0
  73. package/src/components/page-toolbar/examples/without-search.tsx +60 -0
  74. package/src/components/popover/COMPONENT.md +4 -3
  75. package/src/components/progress-bar/COMPONENT.md +1 -1
  76. package/src/components/progress-circle/COMPONENT.md +1 -1
  77. package/src/components/scroll-container/COMPONENT.md +1 -1
  78. package/src/components/segmented-control/COMPONENT.md +8 -8
  79. package/src/components/segmented-control/examples/full-width.tsx +1 -1
  80. package/src/components/select/COMPONENT.md +2 -1
  81. package/src/components/skeleton/COMPONENT.md +1 -1
  82. package/src/components/smart-filter/COMPONENT.md +2 -0
  83. package/src/components/spinner/COMPONENT.md +1 -1
  84. package/src/components/tabs/COMPONENT.md +11 -8
  85. package/src/components/tabs/examples/closable.tsx +1 -1
  86. package/src/layout/app-header/COMPONENT.md +178 -0
  87. package/src/layout/app-header/examples/breadcrumbs.tsx +36 -0
  88. package/src/layout/app-header/examples/examples.module.css +47 -0
  89. package/src/layout/app-header/examples/in-app-shell.tsx +70 -0
  90. package/src/layout/app-header/examples/narrow.tsx +29 -0
  91. package/src/layout/app-header/examples/overview.tsx +36 -0
  92. package/src/layout/app-shell/COMPONENT.md +27 -21
  93. package/src/layout/app-shell/examples/examples.module.css +15 -0
  94. package/src/layout/app-shell/examples/footer.tsx +59 -0
  95. package/src/layout/app-shell/examples/overview.tsx +19 -8
  96. package/src/layout/bottom-nav/COMPONENT.md +153 -0
  97. package/src/layout/bottom-nav/examples/examples.module.css +39 -0
  98. package/src/layout/bottom-nav/examples/floating.tsx +89 -0
  99. package/src/layout/bottom-nav/examples/icon-only.tsx +41 -0
  100. package/src/layout/bottom-nav/examples/in-app-shell.tsx +64 -0
  101. package/src/layout/bottom-nav/examples/overview.tsx +44 -0
  102. package/src/layout/bottom-nav/examples/router.tsx +31 -0
  103. package/src/layout/bottom-nav/examples/structure.tsx +47 -0
  104. package/src/layout/sidebar/COMPONENT.md +11 -10
  105. package/src/styles/theme-dark.css +3 -0
  106. package/src/styles/theme-light.css +24 -0
  107. package/src/styles/tokens.css +2 -0
@@ -0,0 +1,153 @@
1
+ # BottomNav
2
+
3
+ **Category:** navigation
4
+ **Kind:** layout
5
+
6
+ > Phone navigation: a bar of 3–5 main sections at the bottom of the screen.
7
+
8
+ ## When to use
9
+ - The main sections of an app on a phone, one tap away (Главная, Заказы, Клиенты, Ещё).
10
+ - Next to Sidebar in one app: Sidebar navigates on a wide screen, BottomNav on a phone (inside `AppShell.Footer` it leaves by itself once the panel is 640px wide).
11
+
12
+ ## When not to use
13
+ - More than five destinations or nested sections → use [Sidebar](../sidebar/COMPONENT.md) (off-canvas on a phone), put the rest under «Ещё».
14
+ - Switching views inside one page → use [Tabs](../../components/tabs/COMPONENT.md) or [SegmentedControl](../../components/segmented-control/COMPONENT.md).
15
+ - Actions (create, filter) → use [Button](../../components/button/COMPONENT.md); the bar only navigates.
16
+
17
+ ## Import
18
+ ```tsx
19
+ import { BottomNav } from "prime-ui-kit";
20
+ ```
21
+
22
+ ## Anatomy
23
+ ```
24
+ BottomNav.Root <nav> bar, equal columns, safe-area inset below
25
+ └─ BottomNav.Item <button>, <a href> or the asChild element
26
+ ├─ BottomNav.ItemIcon the glyph above the label (aria-hidden)
27
+ ├─ BottomNav.ItemCount Badge on the icon corner
28
+ └─ label one short word
29
+ ```
30
+
31
+ ## API
32
+
33
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
34
+
35
+ ### BottomNav.Root
36
+ `ref` → `HTMLElement`. The `<nav>` bar: equal columns, surface fill with a faint top divider, the bottom safe-area inset below the items. Inside `AppShell.Footer` it shows only while the panel is narrower than 640px.
37
+
38
+ | Prop | Type | Default | Description |
39
+ |---|---|---|---|
40
+ | `iconOnly` | `boolean` | `false` | Icons without visible labels; the labels stay in the DOM as visually hidden text and name the items. |
41
+ | `floating` | `boolean` | `false` | A Liquid Glass capsule over the content (the iOS 26 tab bar): a clear `--prime-color-bg-glass` tint over a light backdrop blur, a specular top edge, a glass selection capsule under the current item, inset `--prime-bottom-nav-floating-inset` from the edges and above the home indicator. Positions itself at the bottom of its positioned container (`AppShell.Footer`, which then takes no height), so the page scrolls under it; with `iconOnly` the capsule hugs its square items, centred. Opaque under `prefers-reduced-transparency`. |
42
+ | `labels` | `Partial<BottomNavLabels>` | — | Built-in strings, see Labels. |
43
+ | `…rest` | `HTMLAttributes<HTMLElement>` | — | `children` (3–5 `BottomNav.Item`), `aria-label` (replaces `labels.nav`), `className` and the other attributes. |
44
+
45
+ ### BottomNav.Item
46
+ `ref` → the rendered element. `<button type="button">`, `<a>` with `href`, or the single child with `asChild`: the icon (20) above a short label (10).
47
+
48
+ | Prop | Type | Default | Description |
49
+ |---|---|---|---|
50
+ | `current` | `boolean` | `false` | Current section: `aria-current="page"`; the icon and the label turn primary (the others are muted). |
51
+ | `disabled` | `boolean` | `false` | Not interactive: `disabled` / `aria-disabled`, `data-disabled`; a link loses its `href`. |
52
+ | `href` | `string` | — | Renders an `<a>` (with `target`, `rel`). |
53
+ | `asChild` | `boolean` | `false` | Renders the single child (e.g. a router `NavLink`) as the item; its children are the label and parts. |
54
+ | `children` | `ReactNode` | — | `BottomNav.ItemIcon`, an optional `BottomNav.ItemCount` and a short label (one word). |
55
+ | `…rest` | `ButtonHTMLAttributes<HTMLButtonElement>` | — | `onClick`, `aria-*`, `className` and the other attributes. |
56
+
57
+ ### BottomNav.ItemIcon
58
+ `ref` → `HTMLSpanElement`. The item glyph (`aria-hidden`, 20) above the label.
59
+
60
+ | Prop | Type | Default | Description |
61
+ |---|---|---|---|
62
+ | `children` | `ReactNode` | — (required) | The icon, e.g. `<Icon name="nav.home" />`. |
63
+ | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other span attributes. |
64
+
65
+ ### BottomNav.ItemCount
66
+ `ref` → `HTMLSpanElement` (the Badge). An `xs` Badge on the icon's top-end corner; read after the label («Заказы 12»).
67
+
68
+ | Prop | Type | Default | Description |
69
+ |---|---|---|---|
70
+ | `children` | `ReactNode` | — (required) | The number (or a short status such as «99+»). |
71
+ | `color` | `PaletteColor` | `"red"` | Badge hue: a count on a section asks for attention. |
72
+ | `variant` | `"solid" \| "soft" \| "outline"` | `"solid"` | Badge treatment. |
73
+ | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children" \| "color">` | — | `className` and the other span attributes. |
74
+
75
+ ## Variants
76
+
77
+ Two item states only — current (primary color) and not (muted); no hover, no fill.
78
+
79
+ ### Flags (Root)
80
+ | Flag | Looks like | Use when | Default |
81
+ |---|---|---|---|
82
+ | `floating` | a glass capsule over the content: translucent fill, backdrop blur, bright rim, inset from the edges | the app's look wants depth over the page, the iOS tab-bar feel | |
83
+ | `iconOnly` | icons alone; labels visually hidden, still the items' names | 3–4 sections with unmistakable icons | |
84
+
85
+ Without flags the bar is flat at the screen edge on the surface, with a faint top divider.
86
+
87
+ ### Flags (Item)
88
+ | Flag | Part | Effect |
89
+ |---|---|---|
90
+ | `current` | Item | `aria-current="page"`, icon and label in the primary color |
91
+ | `disabled` | Item | not interactive, disabled text color |
92
+ | `asChild` | Item | the child element (router link) is the item |
93
+
94
+ ## States
95
+ | State | Driven by | DOM |
96
+ |---|---|---|
97
+ | current | `current` or a router link setting `aria-current` | `aria-current="page"` on the item |
98
+ | focus | keyboard | focus ring inside the item edge |
99
+ | disabled | `disabled` | `disabled` / `aria-disabled`, `data-disabled` |
100
+ | hidden | inside `AppShell.Footer` 640px wide or more | `display: none` (container `prime-shell-footer`) |
101
+ | floating / icon only | `floating` / `iconOnly` | `data-floating="true"` / `data-icon-only="true"` on the `<nav>` |
102
+ | less transparency | `prefers-reduced-transparency: reduce` | a floating capsule turns opaque (`--prime-color-bg-raised`), no blur |
103
+
104
+ Motion: the color of the icon and the label changes over `fast`; in the floating capsule the selection capsule grows in from 92% and fades in (`fast`, enter easing), instant under reduced motion. No hover or press feedback: an item is either current or not.
105
+
106
+ ## Layout & spacing
107
+ - Bar `--prime-bottom-nav-height` (52) plus `env(safe-area-inset-bottom)`; side padding `--prime-bottom-nav-padding-x`. With `iconOnly` the bar is `--prime-bottom-nav-icon-only-height` (48).
108
+ - Items share the width equally; the icon (`--prime-bottom-nav-icon-size`, 20 — a light glyph, so the bar stays quiet next to the page) sits in a `--prime-bottom-nav-indicator-width` × `--prime-bottom-nav-indicator-height` box (28 × 20) that anchors the count; the label is `--prime-bottom-nav-label-size` (10, medium, line 12 — the iOS tab-bar size) 4 below it and truncates.
109
+ - Floating (the iOS 26 tab bar): items `--prime-bottom-nav-floating-item-size` (44) high inset `--prime-bottom-nav-floating-padding` (4) inside a full-radius capsule (52 in all); with `iconOnly` the items are 44 squares and the capsule hugs them. `--prime-bottom-nav-floating-inset` (12) from the sides and above `env(safe-area-inset-bottom)`. Liquid Glass: a clear `--prime-color-bg-glass` tint over a light `blur(--prime-bottom-nav-blur)` (8) with `saturate(180%)`, so the page reads through; a specular top edge in `--prime-color-bg-glass-edge`, a hairline of ink around it and the overlay shadow. The current item sits on a glass selection capsule (ink at 8% with its own top highlight). Inside `AppShell.Footer` main gets extra bottom padding so the last line clears the capsule.
110
+ - Surface fill with a faint top divider; inside `AppShell.Footer` the bar sticks to the bottom of the panel.
111
+
112
+ ## Accessibility
113
+
114
+ ### Keyboard
115
+ | Key | Action |
116
+ |---|---|
117
+ | `Tab` · `Shift+Tab` | Moves focus through the sections; a disabled one is skipped. |
118
+ | `Enter` | Opens the section (a link or a button). |
119
+
120
+ ### ARIA
121
+ - A `<nav>` landmark named by `labels.nav` («Основные разделы») or its own `aria-label`.
122
+ - The current section has `aria-current="page"`; visually it is the primary-color item among muted ones (a contrast step, not a hue).
123
+ - The icon is hidden from assistive tech; the item is named by its label, the count is read after it («Заказы 12»).
124
+ - A disabled item is a `disabled` button or an `aria-disabled` link without `href`.
125
+
126
+ ### Labels
127
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
128
+
129
+ | Key | Default | Used for |
130
+ |---|---|---|
131
+ | `nav` | `"Основные разделы"` | `aria-label` of the `<nav>` landmark. |
132
+
133
+ ## Examples
134
+ | Example | Shows |
135
+ |---|---|
136
+ | [overview.tsx](examples/overview.tsx) | Four sections of the app, the current one in the primary color and a tap moves it — `current`, `BottomNav.ItemIcon`. |
137
+ | [structure.tsx](examples/structure.tsx) | Counts on the icons and a section that is not available yet — `BottomNav.ItemCount`, `disabled`. |
138
+ | [icon-only.tsx](examples/icon-only.tsx) | Icons alone: the labels are hidden but still name the sections for screen readers — `iconOnly`. |
139
+ | [floating.tsx](examples/floating.tsx) | A glass capsule over the page: the client list scrolls under its blur, with labels or icons alone — `floating`, `iconOnly`. |
140
+ | [router.tsx](examples/router.tsx) | A router link as the item: the router sets `aria-current` and the item shows as current; render it inside a router — `asChild`. |
141
+ | [in-app-shell.tsx](examples/in-app-shell.tsx) | The app frame on a phone: the AppHeader, the page and the bar in the footer, which leaves once the panel is 640px wide — `AppShell.Footer`. |
142
+
143
+ ## Mistakes
144
+ - Six or more items → keep 3–5; the rest goes to a «Ещё» section or Sidebar.
145
+ - Long labels («Управление заказами») → one word; the label truncates.
146
+ - `iconOnly` with icons that need a caption to be understood → keep the labels.
147
+ - A floating bar outside a positioned container → it sits at the bottom of the nearest positioned ancestor; put it in `AppShell.Footer`.
148
+ - An action item («Создать») in the bar → the bar navigates; put the action in the page header.
149
+ - Hiding the bar with your own media query inside `AppShell.Footer` → the footer already hides it from 640px of panel width.
150
+
151
+ ## Related
152
+ - **Built from:** Badge, VisuallyHidden (internal)
153
+ - **See also:** [AppShell](../app-shell/COMPONENT.md), [Sidebar](../sidebar/COMPONENT.md), [Tabs](../../components/tabs/COMPONENT.md)
@@ -0,0 +1,39 @@
1
+ /* Layout of the BottomNav scenarios: a phone-wide screen frame. Semantic tokens only. */
2
+
3
+ /* A phone screen: the content above, the bar pinned to the bottom edge. */
4
+ .phone {
5
+ box-sizing: border-box;
6
+ display: flex;
7
+ flex-direction: column;
8
+ width: min(100%, var(--prime-drawer-width-s));
9
+ height: calc(var(--prime-space-24) * 5);
10
+ margin-inline: auto;
11
+ overflow: hidden;
12
+ border-radius: var(--prime-card-radius);
13
+ background: var(--prime-color-bg-surface);
14
+ box-shadow:
15
+ 0 0 0 var(--prime-border-width) var(--prime-color-border-subtle),
16
+ var(--prime-shadow-raised);
17
+ }
18
+
19
+ /* The page above the bar. */
20
+ .screen {
21
+ display: flex;
22
+ flex: 1 1 auto;
23
+ flex-direction: column;
24
+ gap: var(--prime-space-2);
25
+ min-height: 0;
26
+ padding: var(--prime-space-6) var(--prime-layout-gutter-s);
27
+ }
28
+
29
+ /* in-app-shell.tsx: the shell fills the phone frame instead of the viewport. */
30
+ .shell {
31
+ height: 100%;
32
+ }
33
+
34
+ /* floating.tsx: a client row — avatar and two lines of text. */
35
+ .client {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: var(--prime-space-3);
39
+ }
@@ -0,0 +1,89 @@
1
+ /** A glass capsule over the page: the client list scrolls under its blur, with labels or icons alone — `floating`, `iconOnly`. */
2
+ import {
3
+ AppHeader,
4
+ AppShell,
5
+ Avatar,
6
+ BottomNav,
7
+ Icon,
8
+ PageContent,
9
+ Switch,
10
+ Typography,
11
+ } from "prime-ui-kit";
12
+ import * as React from "react";
13
+
14
+ import styles from "./examples.module.css";
15
+
16
+ const SECTIONS = [
17
+ { id: "home", label: "Главная", icon: "nav.home" },
18
+ { id: "clients", label: "Клиенты", icon: "object.users" },
19
+ { id: "inbox", label: "Входящие", icon: "object.inbox" },
20
+ { id: "settings", label: "Настройки", icon: "action.settings" },
21
+ ] as const;
22
+
23
+ const CLIENTS = [
24
+ { name: "ООО «Север»", initials: "СЕ", color: "blue", deals: "4 сделки" },
25
+ { name: "ИП Котов", initials: "КО", color: "green", deals: "1 сделка" },
26
+ { name: "ООО «Вектор»", initials: "ВЕ", color: "orange", deals: "7 сделок" },
27
+ { name: "АО «Меридиан»", initials: "МЕ", color: "purple", deals: "2 сделки" },
28
+ { name: "ООО «Полюс»", initials: "ПО", color: "red", deals: "5 сделок" },
29
+ { name: "ИП Белова", initials: "БЕ", color: "teal", deals: "3 сделки" },
30
+ { name: "ООО «Гранит»", initials: "ГР", color: "yellow", deals: "6 сделок" },
31
+ { name: "АО «Ладога»", initials: "ЛА", color: "sky", deals: "2 сделки" },
32
+ ] as const;
33
+
34
+ export default function BottomNavFloatingExample() {
35
+ const [section, setSection] = React.useState<string>("clients");
36
+ const [iconOnly, setIconOnly] = React.useState(false);
37
+
38
+ return (
39
+ <div className={styles.phone}>
40
+ <AppShell.Root fillViewport className={styles.shell}>
41
+ <AppHeader.Root>
42
+ <AppHeader.Start>
43
+ <AppHeader.Title>Клиенты</AppHeader.Title>
44
+ </AppHeader.Start>
45
+ <AppHeader.Actions>
46
+ <Switch.Root checked={iconOnly} onCheckedChange={setIconOnly}>
47
+ <Switch.Label>Только иконки</Switch.Label>
48
+ </Switch.Root>
49
+ </AppHeader.Actions>
50
+ </AppHeader.Root>
51
+ <AppShell.Main>
52
+ <PageContent.Section>
53
+ <PageContent.Body>
54
+ {CLIENTS.map((client) => (
55
+ <div key={client.name} className={styles.client}>
56
+ <Avatar.Root color={client.color} aria-hidden="true">
57
+ <Avatar.Fallback>{client.initials}</Avatar.Fallback>
58
+ </Avatar.Root>
59
+ <div>
60
+ <Typography variant="body-m">{client.name}</Typography>
61
+ <Typography variant="body-s" tone="muted">
62
+ {client.deals}
63
+ </Typography>
64
+ </div>
65
+ </div>
66
+ ))}
67
+ </PageContent.Body>
68
+ </PageContent.Section>
69
+ </AppShell.Main>
70
+ <AppShell.Footer>
71
+ <BottomNav.Root floating iconOnly={iconOnly}>
72
+ {SECTIONS.map((item) => (
73
+ <BottomNav.Item
74
+ key={item.id}
75
+ current={item.id === section}
76
+ onClick={() => setSection(item.id)}
77
+ >
78
+ <BottomNav.ItemIcon>
79
+ <Icon name={item.icon} />
80
+ </BottomNav.ItemIcon>
81
+ {item.label}
82
+ </BottomNav.Item>
83
+ ))}
84
+ </BottomNav.Root>
85
+ </AppShell.Footer>
86
+ </AppShell.Root>
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,41 @@
1
+ /** Icons alone: the labels are hidden but still name the sections for screen readers — `iconOnly`. */
2
+ import { BottomNav, Icon, Typography } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const SECTIONS = [
8
+ { id: "home", label: "Главная", icon: "nav.home" },
9
+ { id: "orders", label: "Заказы", icon: "object.package" },
10
+ { id: "inbox", label: "Входящие", icon: "object.inbox" },
11
+ { id: "profile", label: "Профиль", icon: "object.user" },
12
+ ] as const;
13
+
14
+ export default function BottomNavIconOnlyExample() {
15
+ const [section, setSection] = React.useState<string>("home");
16
+ const current = SECTIONS.find((item) => item.id === section) ?? SECTIONS[0];
17
+
18
+ return (
19
+ <div className={styles.phone}>
20
+ <div className={styles.screen}>
21
+ <Typography as="h2" variant="title-m">
22
+ {current.label}
23
+ </Typography>
24
+ </div>
25
+ <BottomNav.Root iconOnly>
26
+ {SECTIONS.map((item) => (
27
+ <BottomNav.Item
28
+ key={item.id}
29
+ current={item.id === section}
30
+ onClick={() => setSection(item.id)}
31
+ >
32
+ <BottomNav.ItemIcon>
33
+ <Icon name={item.icon} />
34
+ </BottomNav.ItemIcon>
35
+ {item.label}
36
+ </BottomNav.Item>
37
+ ))}
38
+ </BottomNav.Root>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,64 @@
1
+ /** The app frame on a phone: the AppHeader, the page and the bar in the footer, which leaves once the panel is 640px wide — `AppShell.Footer`. */
2
+ import {
3
+ AppHeader,
4
+ AppShell,
5
+ BottomNav,
6
+ Button,
7
+ Icon,
8
+ PageContent,
9
+ Typography,
10
+ } from "prime-ui-kit";
11
+
12
+ import styles from "./examples.module.css";
13
+
14
+ const SECTIONS = [
15
+ { label: "Главная", icon: "nav.home" },
16
+ { label: "Заказы", icon: "object.package" },
17
+ { label: "Клиенты", icon: "object.users" },
18
+ { label: "Ещё", icon: "action.more" },
19
+ ] as const;
20
+
21
+ const ORDERS = ["№ 4821 · ООО «Север»", "№ 4820 · ИП Котов", "№ 4819 · ООО «Вектор»"];
22
+
23
+ export default function BottomNavInAppShellExample() {
24
+ return (
25
+ <div className={styles.phone}>
26
+ <AppShell.Root fillViewport className={styles.shell}>
27
+ <AppHeader.Root>
28
+ <AppHeader.Start>
29
+ <AppHeader.Title>Заказы</AppHeader.Title>
30
+ </AppHeader.Start>
31
+ </AppHeader.Root>
32
+ <AppShell.Main>
33
+ <PageContent.Section>
34
+ <PageContent.Body>
35
+ {ORDERS.map((order) => (
36
+ <Typography key={order} variant="body-m">
37
+ {order}
38
+ </Typography>
39
+ ))}
40
+ <Button.Root variant="soft" tone="neutral">
41
+ <Button.Icon>
42
+ <Icon name="action.add" />
43
+ </Button.Icon>
44
+ Новый заказ
45
+ </Button.Root>
46
+ </PageContent.Body>
47
+ </PageContent.Section>
48
+ </AppShell.Main>
49
+ <AppShell.Footer>
50
+ <BottomNav.Root>
51
+ {SECTIONS.map((item, index) => (
52
+ <BottomNav.Item key={item.label} current={index === 1}>
53
+ <BottomNav.ItemIcon>
54
+ <Icon name={item.icon} />
55
+ </BottomNav.ItemIcon>
56
+ {item.label}
57
+ </BottomNav.Item>
58
+ ))}
59
+ </BottomNav.Root>
60
+ </AppShell.Footer>
61
+ </AppShell.Root>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,44 @@
1
+ /** Four sections of the app, the current one in the primary color and a tap moves it — `current`, `BottomNav.ItemIcon`. */
2
+ import { BottomNav, Icon, Typography } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const SECTIONS = [
8
+ { id: "home", label: "Главная", icon: "nav.home", summary: "Выручка за неделю — 1,2 млн ₽" },
9
+ { id: "orders", label: "Заказы", icon: "object.package", summary: "37 заказов ждут отгрузки" },
10
+ { id: "clients", label: "Клиенты", icon: "object.users", summary: "214 активных клиентов" },
11
+ { id: "settings", label: "Настройки", icon: "action.settings", summary: "Профиль и уведомления" },
12
+ ] as const;
13
+
14
+ export default function BottomNavOverviewExample() {
15
+ const [section, setSection] = React.useState<string>("orders");
16
+ const current = SECTIONS.find((item) => item.id === section) ?? SECTIONS[0];
17
+
18
+ return (
19
+ <div className={styles.phone}>
20
+ <div className={styles.screen}>
21
+ <Typography as="h2" variant="title-m">
22
+ {current.label}
23
+ </Typography>
24
+ <Typography variant="body-m" tone="secondary">
25
+ {current.summary}
26
+ </Typography>
27
+ </div>
28
+ <BottomNav.Root>
29
+ {SECTIONS.map((item) => (
30
+ <BottomNav.Item
31
+ key={item.id}
32
+ current={item.id === section}
33
+ onClick={() => setSection(item.id)}
34
+ >
35
+ <BottomNav.ItemIcon>
36
+ <Icon name={item.icon} />
37
+ </BottomNav.ItemIcon>
38
+ {item.label}
39
+ </BottomNav.Item>
40
+ ))}
41
+ </BottomNav.Root>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,31 @@
1
+ /** A router link as the item: the router sets `aria-current` and the item shows as current; render it inside a router — `asChild`. */
2
+ import { BottomNav, Icon } from "prime-ui-kit";
3
+ import { NavLink } from "react-router-dom";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const LINKS = [
8
+ { to: "/app-shell", label: "Каркас", icon: "nav.layoutGrid" },
9
+ { to: "/bottom-nav", label: "Навигация", icon: "nav.menu" },
10
+ { to: "/sidebar", label: "Сайдбар", icon: "nav.sidebarExpand" },
11
+ ] as const;
12
+
13
+ export default function BottomNavRouterExample() {
14
+ return (
15
+ <div className={styles.phone}>
16
+ <div className={styles.screen} />
17
+ <BottomNav.Root>
18
+ {LINKS.map((link) => (
19
+ <BottomNav.Item key={link.to} asChild>
20
+ <NavLink to={link.to}>
21
+ <BottomNav.ItemIcon>
22
+ <Icon name={link.icon} />
23
+ </BottomNav.ItemIcon>
24
+ {link.label}
25
+ </NavLink>
26
+ </BottomNav.Item>
27
+ ))}
28
+ </BottomNav.Root>
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,47 @@
1
+ /** Counts on the icons and a section that is not available yet — `BottomNav.ItemCount`, `disabled`. */
2
+ import { BottomNav, Icon, Typography } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ export default function BottomNavStructureExample() {
7
+ return (
8
+ <div className={styles.phone}>
9
+ <div className={styles.screen}>
10
+ <Typography as="h2" variant="title-m">
11
+ Главная
12
+ </Typography>
13
+ <Typography variant="body-m" tone="secondary">
14
+ 12 новых заказов и 3 сообщения от клиентов.
15
+ </Typography>
16
+ </div>
17
+ <BottomNav.Root>
18
+ <BottomNav.Item current>
19
+ <BottomNav.ItemIcon>
20
+ <Icon name="nav.home" />
21
+ </BottomNav.ItemIcon>
22
+ Главная
23
+ </BottomNav.Item>
24
+ <BottomNav.Item>
25
+ <BottomNav.ItemIcon>
26
+ <Icon name="object.package" />
27
+ </BottomNav.ItemIcon>
28
+ <BottomNav.ItemCount>12</BottomNav.ItemCount>
29
+ Заказы
30
+ </BottomNav.Item>
31
+ <BottomNav.Item>
32
+ <BottomNav.ItemIcon>
33
+ <Icon name="object.message" />
34
+ </BottomNav.ItemIcon>
35
+ <BottomNav.ItemCount color="blue">3</BottomNav.ItemCount>
36
+ Сообщения
37
+ </BottomNav.Item>
38
+ <BottomNav.Item disabled>
39
+ <BottomNav.ItemIcon>
40
+ <Icon name="object.chart" />
41
+ </BottomNav.ItemIcon>
42
+ Отчёты
43
+ </BottomNav.Item>
44
+ </BottomNav.Root>
45
+ </div>
46
+ );
47
+ }
@@ -1,6 +1,6 @@
1
1
  # Sidebar
2
2
 
3
- **Category:** layout
3
+ **Category:** navigation
4
4
  **Kind:** layout
5
5
 
6
6
  > App side navigation in three modes — expanded, compact, hidden — and an off-canvas panel on narrow screens.
@@ -22,7 +22,8 @@ same place. Structure it by what people do, not by how the backend is split.
22
22
  tooltip), so start sibling labels differently («Продажи» and «Проекты» both read «Про»).
23
23
  - **Fold only what is secondary.** Make a group `collapsible` when the list is long (about 12+
24
24
  items) or the group is used rarely; keep the main group open and fixed. `defaultOpen={false}`
25
- only for rarely used groups — a group with the current page opens by itself.
25
+ only for rarely used groups — a group with the current page opens by itself (on the compact rail
26
+ it stays folded; its `…` row marks the page).
26
27
  - **One level of nesting.** `Sidebar.Sub` is one section with 2–6 views of the same objects
27
28
  (Сделки → Новые · В работе · Закрытые). The parent only discloses; an overview page is the first
28
29
  child («Все сделки»). Never a sub-list inside a sub-list — split the section or use page tabs.
@@ -128,7 +129,7 @@ useSidebar() state hook for custom parts inside Root
128
129
  | `…rest` | `HTMLAttributes<HTMLElement>` | — | `children` (Groups, Items, Subs), `className` and the other attributes. |
129
130
 
130
131
  ### Sidebar.Group
131
- `ref` → `HTMLDivElement`. `<div role="group">` named by its label. With `collapsible` the heading is a disclosure button (`aria-expanded`, `aria-controls`) with a chevron at its end; the items fold away (inert). On the compact rail the heading keeps its height and layout: its text slides toward the edge, scales down and is cut by the rail with a fade, so the first letters still name the section. An open group keeps its items on the rail; a folded one stays folded and becomes one `…` row (named by the label, `aria-haspopup="dialog"`) whose items open in a flyout, like a sub-list. A folded group with a count that needs attention inside shows its dot (after the heading, or on the `…` row).
132
+ `ref` → `HTMLDivElement`. `<div role="group">` named by its label. With `collapsible` the heading is a disclosure button (`aria-expanded`, `aria-controls`) with a chevron at its end; the items fold away (inert). On the compact rail the heading keeps its height and layout: its text slides toward the edge, scales down and is cut by the rail with a fade, so the first letters still name the section, and it still folds and unfolds the group. An open group keeps its items on the rail; a folded one stays folded and becomes one `…` row (named by the label, `aria-haspopup="dialog"`) whose items open in a flyout, like a sub-list. A folded group with a count that needs attention inside shows its dot (after the heading, or on the `…` row).
132
133
 
133
134
  | Prop | Type | Default | Description |
134
135
  |---|---|---|---|
@@ -136,7 +137,7 @@ useSidebar() state hook for custom parts inside Root
136
137
  | `collapsible` | `boolean` | `false` | The heading shows and hides the items. Needs `label`. |
137
138
  | `open` | `boolean` | — | Items shown (controlled). |
138
139
  | `onOpenChange` | `(open: boolean) => void` | — | Called with the new open state (click, keyboard, a current page moving inside). |
139
- | `defaultOpen` | `boolean` | `true` | Initial state (uncontrolled). A closed group opens by itself when the current page moves into it. |
140
+ | `defaultOpen` | `boolean` | `true` | Initial state (uncontrolled). Off the compact rail a closed group opens by itself when the current page moves into it; on the rail it stays folded and its `…` row marks the page. |
140
141
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "role">` | — | `children` (Items, Subs), `className` and the other div attributes. |
141
142
 
142
143
  ### Sidebar.Item
@@ -178,7 +179,7 @@ useSidebar() state hook for custom parts inside Root
178
179
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other span attributes. |
179
180
 
180
181
  ### Sidebar.ItemAction
181
- `ref` → `HTMLButtonElement`. A row action (create, add): a ghost icon `Button` one tier down with a tooltip, next to the item element — never inside it. It shows on hover and focus of the row while the trail (count, hint) steps aside; hidden on the compact rail.
182
+ `ref` → `HTMLButtonElement`. A row action (create, add): a ghost icon `Button` one tier down with a tooltip, next to the item element — never inside it. It shows on hover and focus of the row, always on touch screens, while the trail (count, hint) steps aside; hidden on the compact rail.
182
183
 
183
184
  | Prop | Type | Default | Description |
184
185
  |---|---|---|---|
@@ -289,7 +290,7 @@ The rail width does not depend on `size`.
289
290
  | `current` (Item) | surface fill + raised shadow, primary text and icon | the current page | off |
290
291
  | `disabled` (Item) | `text-disabled`, no hover, `cursor: not-allowed` | an unavailable section | off |
291
292
 
292
- Put `<Sidebar.Toggle variant="header" />` next to `Sidebar.Brand` in the header, or `Sidebar.Toggle` in the footer for a rail without a brand; on phones (`offCanvas="auto"`) or always (`offCanvas="always"`) pair it with a menu button in `AppShell.Header` (`open` / `onOpenChange`). One current item at a time; give every top-level item an icon when the rail can go compact (child items need none).
293
+ Put `<Sidebar.Toggle variant="header" />` next to `Sidebar.Brand` in the header, or `Sidebar.Toggle` in the footer for a rail without a brand; on phones (`offCanvas="auto"`) or always (`offCanvas="always"`) pair it with `AppHeader.MenuButton` (`open` / `onOpenChange`). One current item at a time; give every top-level item an icon when the rail can go compact (child items need none).
293
294
 
294
295
  ## States
295
296
  | State | Driven by | DOM |
@@ -297,7 +298,7 @@ Put `<Sidebar.Toggle variant="header" />` next to `Sidebar.Brand` in the header,
297
298
  | mode | `mode` / `defaultMode` | Root `data-mode`, `data-panel-mode` (the last visible mode while hidden); `<nav inert>` while hidden |
298
299
  | off-canvas | `offCanvas="always"`, or `"auto"` + viewport < 768px | Root `data-off-canvas`, `data-state="open" \| "closed"`; `<nav inert>` while closed |
299
300
  | current | `current` or a router's `aria-current` | Item `aria-current="page"`, `data-state="active"` |
300
- | active path | a child of a Sub (or an item of a Group) is current | SubTrigger `data-active-path`; a closed Sub / Group opens |
301
+ | active path | a child of a Sub (or an item of a Group) is current | SubTrigger (or the `…` row of a folded Group) `data-active-path`; a closed Sub opens, a closed Group opens off the compact rail |
301
302
  | group open | Group `open` / `defaultOpen` (`collapsible`) | heading `aria-expanded`; region `data-state`, `inert` while closed |
302
303
  | sub open | Sub `open` / `defaultOpen` | SubTrigger `aria-expanded`; SubContent `data-state`, `inert` while closed |
303
304
  | flyout | hover / click / keyboard on a SubTrigger, or on the `…` row of a folded group, in compact mode | the row `aria-haspopup="dialog"`, `aria-expanded`; a Popover `role="dialog"` |
@@ -312,9 +313,9 @@ Leaving the narrow viewport closes the off-canvas panel; navigating from an `hre
312
313
  - Item horizontal padding is derived from the compact width so icons and the brand logo sit on one axis in every mode (the account avatar joins that axis on the rail).
313
314
  - Header is one row of the item height + 8. The header toggle (xs, 28) sits at its end, 8 from the rail edge; in compact mode it moves half past the edge, scales to 3/4, turns round and gets a canvas ring. The root clips with `clip-path`, reaching past the edge by the toggle's half-width and ring beside the header only; everywhere else the panel ends exactly at the spacer edge.
314
315
  - Collapsible group: heading 28 high with the chevron (14) at its end, aligned with the item trail; items start 4 below. A folded group's attention dot sits before the chevron.
315
- - On the rail a heading keeps its 28 and its layout: the text slides 8 toward the edge, scales to 0.85 and is cut at the rail box with a 12 fade into the canvas. A folded group adds one `…` row under its heading.
316
+ - On the rail a heading keeps its 28 and its layout: the text slides 8 toward the edge, scales to 0.85 and is cut at the rail box with a 12 fade into the canvas. A folded group adds one `…` row under its heading. The items and the row share one place: folding or unfolding on the rail glides the group's height one way (list ↔ row) while the two cross-fade, so the rows below never move back.
316
317
  - Sub-list: a 1px `border-default` guide line on the parent icon's centre; every child branches off it with an 8 elbow and a 6 bend; child labels line up with the parent label.
317
- - Trail (count, key hint, trailing icon) sits at the row end; a row action (one tier down) appears there on hover / focus and the trail moves aside.
318
+ - Trail (count, key hint, trailing icon) sits at the row end; a row action (one tier down) appears there on hover / focus (always on touch screens) and the trail moves aside.
318
319
  - Footer: items 4 apart; the account is set 8 apart below them. It is its own card-like row: the
319
320
  avatar of the tier (xs 20, s 24, m·l 32, xl 40) sits in an even frame — the inset before it, the
320
321
  gap to the text and the air above and below are one value, the tier gap (xs 4, s·m·l 8, xl 12),
@@ -344,7 +345,7 @@ Leaving the narrow viewport closes the off-canvas panel; navigating from an `hre
344
345
  - The current item has `aria-current="page"`; in compact mode the label shows as a tooltip, so icon-only items keep a visible name.
345
346
  - A collapsible group heading and a SubTrigger are buttons with `aria-expanded` and `aria-controls`; folded content is `inert`.
346
347
  - On the compact rail a SubTrigger and the `…` row of a folded group have `aria-haspopup="dialog"`; the flyout is a non-modal dialog named by the parent (the `…` row by the group label).
347
- - On the rail a group heading is out of the tab order and hidden from screen readers (the group keeps its name); hovering it shows the whole name in a tooltip.
348
+ - On the rail a collapsible group heading stays the same disclosure button (folding it there brings the `…` row); hovering it shows the whole name in a tooltip.
348
349
  - An attention dot is decorative: the count it stands for is read inside.
349
350
  - A row action is its own button next to the item (never nested), named by `label` with a tooltip.
350
351
  - The account avatar is hidden from screen readers; the name and the muted line name the button.
@@ -7,6 +7,9 @@
7
7
  --prime-color-accent-soft-hover: color-mix(in srgb, var(--prime-ref-color-cobalt-500) 28%, transparent);
8
8
  --prime-color-accent-text: var(--prime-ref-color-cobalt-400);
9
9
  --prime-color-bg-canvas: var(--prime-ref-color-gray-950);
10
+ --prime-color-bg-edge-shadow: rgba(0, 0, 0, 0.5);
11
+ --prime-color-bg-glass: color-mix(in srgb, var(--prime-ref-color-gray-875) 56%, transparent);
12
+ --prime-color-bg-glass-edge: rgba(255, 255, 255, 0.16);
10
13
  --prime-color-bg-inverse: var(--prime-ref-color-gray-100);
11
14
  --prime-color-bg-raised: var(--prime-ref-color-gray-875);
12
15
  --prime-color-bg-scrim: rgba(0, 0, 0, 0.6);