prime-ui-kit 0.21.0 → 0.21.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.
@@ -128,7 +128,7 @@ declare namespace PageActions {
128
128
  }
129
129
  export type PageToolsProps = DivProps;
130
130
  /**
131
- * The page's tools — sections, search and filter, view — as kit controls in the order written, laid
131
+ * The page's tools — sections, view, a shared period, a filter over several views — as kit controls in the order written, laid
132
132
  * out by the one tool row (`ToolRow`, as the DataTable `toolbar`). On the bar's first line each keeps
133
133
  * its natural width; on a line of their own a search takes the free room, switchers and pickers
134
134
  * keep their width at the end (a `SegmentedControl.Root` and a `Datepicker.Root` are `fullWidth`
@@ -10,6 +10,7 @@ checks.
10
10
  - [ ] A nested record's path ends in its title on one line inside `Page.Heading`; no title row under the path ([the top bar](composition.md#the-top-bar)).
11
11
  - [ ] Every button of the page is in one `Page.Actions`, the primary last, each with a `Button.Icon` ([the top bar](composition.md#the-top-bar)).
12
12
  - [ ] `SmartFilter.Root` wraps `Page.Root`; its toolbar is in `Page.Tools`, its chips in `Page.Chips` ([the top bar](composition.md#the-top-bar)).
13
+ - [ ] A section's sub-pages are route links (`SegmentedControl.Item asChild`) in its `Page.Tools`; the title stays the section and its Sidebar item stays current on every sub-route — no second Sidebar level, no tab strip in the body ([a section with sub-pages](composition.md#a-section-with-sub-pages)).
13
14
  - [ ] A control stands with what it changes: a table's search, filter, chips and chooser are in its `toolbar` / `toolbarChips` on every page; the bar keeps the title, sections, the view, the shared period and the buttons ([where the filters go](composition.md#where-the-filters-go)).
14
15
  - [ ] A table section has an `options` filter field for each categorical column and a `date-range` field per date column, its value in the URL ([where the filters go](composition.md#where-the-filters-go)).
15
16
  - [ ] Actions on selected rows are `renderSelectionActions` — one «Действия · N» menu added to the tools row, which stays ([controls of a table](composition.md#controls-of-a-table)).
@@ -143,8 +143,9 @@ outside the bar), with an optional `Page.TopBar collapse` that repeats it once i
143
143
  `onRowClick` + Drawer; a list worked through record by record → `Page.Split` with a `List` of
144
144
  the records (tickets, letters, invoices; `current` on the open one; a row's own Switch or menu in
145
145
  `List.ItemActions`) or a `ChatList` of the conversations. A record edited on a page of its own →
146
- `Page.Guard` holds leaving it with unsaved changes. Sections of a long rail → `Sidebar.Group collapsible`; a section with sub-pages →
147
- `Sidebar.Sub`; the signed-in user → `Sidebar.Account` in a `Dropdown.Trigger` at the bottom of
146
+ `Page.Guard` holds leaving it with unsaved changes. Sections of a long rail → `Sidebar.Group collapsible`; views of one set of objects
147
+ opened straight from the rail → `Sidebar.Sub`; the facets of one section («Каналы» / «Статистика»)
148
+ → route links in its `Page.Tools` ([a section with sub-pages](composition.md#a-section-with-sub-pages)); the signed-in user → `Sidebar.Account` in a `Dropdown.Trigger` at the bottom of
148
149
  `Sidebar.Footer`.
149
150
 
150
151
  ### A flag, a level, a set
@@ -99,7 +99,8 @@ order is Heading, Tools, Actions, Chips whatever the JSX order.
99
99
  `Page.Description` under the whole line. The title is never a row of its own under the path, and
100
100
  the path never repeats the title as its last item.
101
101
  - **`Page.Tools`** — what acts on the whole page, as kit controls in the order a person reads them:
102
- sections (a `SegmentedControl` with counts), the view, a period that drives several blocks; a
102
+ sections (a `SegmentedControl` with counts; of route links for
103
+ [a section with sub-pages](#a-section-with-sub-pages)), the view, a period that drives several blocks; a
103
104
  search and filter (`SmartFilter.Toolbar`) only when it governs more than one table — a board and
104
105
  its list, a media grid ([where the filters go](#where-the-filters-go)). Named with `aria-label`;
105
106
  no buttons and no wrappers inside.
@@ -180,6 +181,62 @@ export function DealsPage({ fields, filter, onFilterChange, view, onViewChange,
180
181
  }
181
182
  ```
182
183
 
184
+ ### A section with sub-pages
185
+
186
+ One place of the app, several facets of it: «Мессенджеры» with «Каналы» and «Статистика», a client
187
+ with «Сделки» and «Документы». Each facet is a route of its own (`/messengers/channels`,
188
+ `/messengers/stats`), and the section stays one page:
189
+
190
+ - **The title is the section.** `Page.Title` reads «Мессенджеры» on every sub-route; the facet is
191
+ the current link of the switch, never a second title or a title that names the facet.
192
+ - **The switch is links in `Page.Tools`**: a `SegmentedControl` whose items are router links
193
+ (`SegmentedControl.Item asChild`), `value` = the current path. It is a navigation (Tab moves
194
+ through the links, the current one is `aria-current="page"`), on a phone it takes a line of its
195
+ own under the title by itself.
196
+ - **The body is the sub-page**, swapped in a `Crossfade` keyed by the path (`Routes` given the
197
+ `location`, so the leaving sub-page stays on its route while it fades).
198
+ - **The Sidebar stays on the section.** Its item is current on every sub-route: a `NavLink` to
199
+ `/messengers` without `end`, or `current={pathname.startsWith("/messengers")}`.
200
+ - Never a second Sidebar level for facets, never a `Tabs` strip inside `Page.Body` for sub-pages.
201
+ `Sidebar.Sub` is for views of one set of objects people open straight from the rail («Сделки» →
202
+ «Новые», «В работе»); `Tabs` switch panels that are not routes.
203
+
204
+ Live: [router.tsx](reference/layout/page/examples/router.tsx).
205
+
206
+ ```tsx
207
+ import { Page, SegmentedControl } from "prime-ui-kit";
208
+ import type { ReactNode } from "react";
209
+ import { Link, useLocation } from "react-router-dom";
210
+
211
+ const MESSENGER_PAGES = [
212
+ { to: "/messengers/channels", label: "Каналы" },
213
+ { to: "/messengers/stats", label: "Статистика" },
214
+ ];
215
+
216
+ export function MessengersPage({ children }: { children: ReactNode }) {
217
+ const { pathname } = useLocation();
218
+ return (
219
+ <Page.Root>
220
+ <Page.TopBar>
221
+ <Page.Heading>
222
+ <Page.Title>Мессенджеры</Page.Title>
223
+ </Page.Heading>
224
+ <Page.Tools aria-label="Мессенджеры">
225
+ <SegmentedControl.Root value={pathname} aria-label="Подразделы">
226
+ {MESSENGER_PAGES.map((page) => (
227
+ <SegmentedControl.Item key={page.to} value={page.to} asChild>
228
+ <Link to={page.to}>{page.label}</Link>
229
+ </SegmentedControl.Item>
230
+ ))}
231
+ </SegmentedControl.Root>
232
+ </Page.Tools>
233
+ </Page.TopBar>
234
+ <Page.Body>{children}</Page.Body>
235
+ </Page.Root>
236
+ );
237
+ }
238
+ ```
239
+
183
240
  ## 2. Rhythm — the 4px grid
184
241
 
185
242
  Every gap is a `--prime-space-N` token (N × 4px) set as `gap` on the parent. Closer means related.
@@ -142,6 +142,7 @@ when a count gets a colour — are in
142
142
  | Everyday places | `Sidebar.Group` without `label` at the top, never folded |
143
143
  | Category of work | `Sidebar.Group label collapsible` |
144
144
  | Views of one section | `Sidebar.Sub`, one level only |
145
+ | Facets of one section (its list, its statistics) | none: route links in the page's `Page.Tools` ([a section with sub-pages](composition.md#a-section-with-sub-pages)) |
145
146
  | Meta | `Sidebar.Footer`: help, settings, then `Sidebar.Account` with its menu last |
146
147
 
147
148
  ### Sidebar icons
@@ -8,7 +8,8 @@
8
8
  ## When to use
9
9
  - Every route rendered inside `AppShell.Main`: the app is navigation and the page, and the page carries its own top (`Page.TopBar`), title (`Page.Title`, the `<h1>`) and body.
10
10
  - A work screen (a list, a board, a chat, a schedule): `Page.TopBar` with the title (`Page.Heading`), the page's tools (`Page.Tools`: sections, search and filter, view), all its buttons (`Page.Actions`) and the active filters (`Page.Chips`).
11
- - A filter stands next to what it filters. `Page.Tools` holds what acts on the whole page: sections, the view, a period that drives several blocks (the KPI cards and the table). The search, filter, chips and column chooser of a page that is one list stay there too — the list is the page. On a page that holds more than its table (KPI cards, a chart), they belong to the table's own tools row (DataTable `toolbar` + `toolbarChips`), not the bar.
11
+ - A filter stands next to what it filters. `Page.Tools` holds what acts on the whole page: sections, the view, a period that drives several blocks (the KPI cards and the table), and a search and filter only when it narrows several views of one set (a board and its list). A table's search, filter, chips, «Колонки» and selection menu always belong to its own tools row (DataTable `toolbar` + `toolbarChips`), on a page that is only the table too.
12
+ - A section with sub-pages (the «Мессенджеры» item of the Sidebar with «Каналы» and «Статистика»): each sub-page is a route of its own, `Page.Title` stays the section's name, and the switch is a `SegmentedControl` of links (`SegmentedControl.Item asChild`) in `Page.Tools`; the body is the current sub-page. The Sidebar keeps the section's item current on every sub-route — a router `NavLink` to the section's path without `end`, or `current={pathname.startsWith("/messengers")}`.
12
13
  - A document page (settings, a long form, help) or an overview (a dashboard): `Page.Header` in the flow — the large title and its buttons — and `Page.TopBar collapse`, which repeats the title once it scrolls away and keeps the path and the buttons in reach.
13
14
  - A nested record: a `Breadcrumb` path in `Page.Heading`; in the bar the title reads as its last level.
14
15
  - A text page held to a reading measure (`maxWidth="readable"`), a wide dashboard capped at the content width (`maxWidth="wide"`).
@@ -44,8 +45,8 @@ Page.Root the page column (<div>); maxWidth, fill, labe
44
45
  │ │ ├─ Page.Title <h1>, title-m, the path's last level, truncates
45
46
  │ │ └─ Page.Description <p>, muted caption under the whole line, truncates
46
47
  │ ├─ Page.Tools what acts on the whole page, in the order written: the one tool row (as the DataTable toolbar)
47
- │ │ ├─ SegmentedControl.Root sections, view (fullWidth unless set)
48
- │ │ ├─ SmartFilter.Toolbar search and filter: grows first on a line of its own
48
+ │ │ ├─ SegmentedControl.Root sections (route links of sub-pages), view (fullWidth unless set)
49
+ │ │ ├─ SmartFilter.Toolbar a search and filter over several views: grows first on a line of its own
49
50
  │ │ ├─ Datepicker.Root a period (fullWidth unless set)
50
51
  │ │ └─ Select.Root … a period, columns; the trigger fills its place
51
52
  │ ├─ Page.Actions every button of the page; icons, then «⋯» when short
@@ -122,7 +123,7 @@ Page.Root the page column (<div>); maxWidth, fill, labe
122
123
  | `…rest` | `HTMLAttributes<HTMLParagraphElement>` | — | `children`, `className` and the other paragraph attributes. |
123
124
 
124
125
  ### Page.Tools
125
- `ref` → `HTMLDivElement`. What acts on the whole page: sections, a shared period, view; the search of a one-list page. The tool row of the DataTable `toolbar`: a search takes the free room, the rest keep their width at the end. Named, a `group`.
126
+ `ref` → `HTMLDivElement`. What acts on the whole page: sections (route links of sub-pages), view, a shared period; a filter only over several views — a table's go in its `toolbar`. The DataTable tool row: a search takes the free room, the rest keep their width. Named, a `group`.
126
127
 
127
128
  | Prop | Type | Default | Description |
128
129
  |---|---|---|---|
@@ -342,7 +343,8 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
342
343
  | [top-bar.tsx](examples/top-bar.tsx) | A nested record in the top bar: the path ending in the title, a line of data, the page's tools and its buttons — `Page.TopBar`, `Page.Heading`, `Page.Tools`, `Page.Actions`. |
343
344
  | [loading.tsx](examples/loading.tsx) | A page whose figures load after it opens: the line of data keeps its place under the title while it loads, so nothing in the bar moves — `loading`. |
344
345
  | [collapse.tsx](examples/collapse.tsx) | An overview whose large title scrolls away under the top bar: the bar repeats it with its line of data and keeps the period and the page's buttons in reach — `collapse`. |
345
- | [tools.tsx](examples/tools.tsx) | The orders page's tools in its top bar: sections with counts, search and filter, the period, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. |
346
+ | [tools.tsx](examples/tools.tsx) | The orders page's tools in its top bar: sections with counts, the search and filter that narrow both views, the view, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. |
347
+ | [router.tsx](examples/router.tsx) | A section with sub-pages: the title is the section, each sub-page is a route of its own, the switch in the bar is their links and the body cross-fades between them; render it inside a router — `Page.Tools`, `asChild`. |
346
348
  | [actions-collapse.tsx](examples/actions-collapse.tsx) | The buttons of an invoices page on a tablet-wide column: the line runs short, so the secondary buttons show only their icons with the label as the name and the tooltip, the primary keeps its label — `Page.Actions`, `Button.Icon`. |
347
349
  | [sections.tsx](examples/sections.tsx) | A page with titled sections: tiles of one screen close together, a new topic set off by air, a link at the end of the heading — `Page.Section`, `label`, `description`, `Page.Actions`. |
348
350
  | [grid.tsx](examples/grid.tsx) | A dashboard board: four KPI tiles, a chart two rows high beside two panels, one gap on both axes — `Page.Grid`, `Page.Cell`, `span`, `rows`. |
@@ -361,7 +363,8 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
361
363
  - Buttons without an icon in a crowded bar → they cannot give way to an icon; give each `Button.Root` a `Button.Icon` beside its label.
362
364
  - Two places for buttons (some beside the title, the primary among the tools) → every button of the page goes into the one `Page.Actions`, the primary last.
363
365
  - `SmartFilter.Root` inside the bar, around `Page.Tools` → it holds no DOM; wrap it around `Page.Root` so its Toolbar (in `Page.Tools`) and its Chips (in `Page.Chips`) share it.
364
- - A table's search, filter, chips or «Колонки» in `Page.Tools` / `Page.Chips` above KPI cards or a chart they do not filter → they read as filters of the whole page; move them into the table's `toolbar` / `toolbarChips`. The bar keeps the title, a period that drives every block, sections and the primary action.
366
+ - A table's search, filter, chips or «Колонки» in `Page.Tools` / `Page.Chips` → they read as filters of the whole page and move when the page grows a block; they go in the table's `toolbar` / `toolbarChips` on every page, the one that is only the table too. The bar keeps the title, sections, the view, a period that drives every block and the page's buttons.
367
+ - Sub-pages of a section as a second Sidebar level, a tab strip inside `Page.Body`, or a title that names the sub-page → the title stays the section, the sub-pages are route links in `Page.Tools` (`SegmentedControl.Item asChild`), and the section's Sidebar item stays current on every sub-route.
365
368
  - `Page.Header` inside `Page.TopBar` → the bar takes `Page.Heading` and `Page.Actions` directly; `Page.Header` is the flow's document header.
366
369
  - A hand-made menu button for the phone Sidebar → the bar draws it while the Sidebar is off-canvas.
367
370
  - Hiding buttons with media queries on a phone → `Page.Actions` in the bar collapses and folds them by measured width.
@@ -0,0 +1,124 @@
1
+ /** A section with sub-pages: the title is the section, each sub-page is a route of its own, the switch in the bar is their links and the body cross-fades between them; render it inside a router — `Page.Tools`, `asChild`. */
2
+ import {
3
+ Button,
4
+ Card,
5
+ Crossfade,
6
+ DataTable,
7
+ type DataTableColumn,
8
+ Icon,
9
+ Page,
10
+ SegmentedControl,
11
+ } from "prime-ui-kit";
12
+ import { Link, Route, Routes, useLocation } from "react-router-dom";
13
+
14
+ type Channel = { id: string; name: string; dialogs: number; reply: string };
15
+
16
+ const SUB_PAGES = [
17
+ { to: "/messengers/channels", label: "Каналы" },
18
+ { to: "/messengers/stats", label: "Статистика" },
19
+ ];
20
+
21
+ const CHANNELS: Channel[] = [
22
+ { id: "tg", name: "Telegram · @prime_support", dialogs: 642, reply: "4 мин" },
23
+ { id: "wa", name: "WhatsApp · +7 495 120-44-10", dialogs: 391, reply: "7 мин" },
24
+ { id: "vk", name: "ВКонтакте · Прайм CRM", dialogs: 188, reply: "12 мин" },
25
+ { id: "avito", name: "Авито · Склад на Южной", dialogs: 59, reply: "25 мин" },
26
+ ];
27
+
28
+ const COLUMNS: DataTableColumn<Channel>[] = [
29
+ { id: "name", header: "Канал", cell: (row) => row.name },
30
+ {
31
+ id: "dialogs",
32
+ header: "Диалогов",
33
+ numeric: true,
34
+ headerAlign: "end",
35
+ cell: (row) => row.dialogs,
36
+ },
37
+ {
38
+ id: "reply",
39
+ header: "Первый ответ",
40
+ numeric: true,
41
+ headerAlign: "end",
42
+ cell: (row) => row.reply,
43
+ },
44
+ ];
45
+
46
+ export default function PageRouterExample() {
47
+ const location = useLocation();
48
+ // The first sub-page answers every other path of the section (here the preview's «/»).
49
+ const current = SUB_PAGES.find((page) => page.to === location.pathname)?.to ?? SUB_PAGES[0].to;
50
+
51
+ return (
52
+ <Page.Root>
53
+ <Page.TopBar sticky={false}>
54
+ <Page.Heading>
55
+ <Page.Title>Мессенджеры</Page.Title>
56
+ <Page.Description>4 канала · 1 280 диалогов за октябрь</Page.Description>
57
+ </Page.Heading>
58
+ <Page.Tools aria-label="Мессенджеры">
59
+ <SegmentedControl.Root value={current} aria-label="Подразделы">
60
+ {SUB_PAGES.map((page) => (
61
+ <SegmentedControl.Item key={page.to} value={page.to} asChild>
62
+ <Link to={page.to}>{page.label}</Link>
63
+ </SegmentedControl.Item>
64
+ ))}
65
+ </SegmentedControl.Root>
66
+ </Page.Tools>
67
+ <Page.Actions>
68
+ <Button.Root>
69
+ <Button.Icon>
70
+ <Icon name="action.add" />
71
+ </Button.Icon>
72
+ Подключить канал
73
+ </Button.Root>
74
+ </Page.Actions>
75
+ </Page.TopBar>
76
+ <Page.Body>
77
+ {/* `location` keeps the leaving sub-page on its own route while it fades out. */}
78
+ <Crossfade state={current}>
79
+ <Routes location={location}>
80
+ <Route
81
+ path="/messengers/stats"
82
+ element={
83
+ <Page.Grid>
84
+ <Page.Cell span={4}>
85
+ <Card.Root variant="stat-trend">
86
+ <Card.Label>Диалогов</Card.Label>
87
+ <Card.Value>1 280</Card.Value>
88
+ <Card.Delta tone="success">+14% к сентябрю</Card.Delta>
89
+ </Card.Root>
90
+ </Page.Cell>
91
+ <Page.Cell span={4}>
92
+ <Card.Root variant="stat-trend">
93
+ <Card.Label>Первый ответ</Card.Label>
94
+ <Card.Value>6 мин</Card.Value>
95
+ <Card.Delta tone="success">на 2 мин быстрее</Card.Delta>
96
+ </Card.Root>
97
+ </Page.Cell>
98
+ <Page.Cell span={4}>
99
+ <Card.Root variant="stat-trend">
100
+ <Card.Label>Без ответа</Card.Label>
101
+ <Card.Value>23</Card.Value>
102
+ <Card.Delta tone="danger">+5 за сутки</Card.Delta>
103
+ </Card.Root>
104
+ </Page.Cell>
105
+ </Page.Grid>
106
+ }
107
+ />
108
+ <Route
109
+ path="*"
110
+ element={
111
+ <DataTable
112
+ columns={COLUMNS}
113
+ rows={CHANNELS}
114
+ getRowKey={(row) => row.id}
115
+ paging="none"
116
+ />
117
+ }
118
+ />
119
+ </Routes>
120
+ </Crossfade>
121
+ </Page.Body>
122
+ </Page.Root>
123
+ );
124
+ }
@@ -1,10 +1,9 @@
1
- /** The orders page's tools in its top bar: sections with counts, search and filter, the period, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. */
1
+ /** The orders page's tools in its top bar: sections with counts, the search and filter that narrow both views, the view, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. */
2
2
  import {
3
3
  Button,
4
4
  Icon,
5
5
  Page,
6
6
  SegmentedControl,
7
- Select,
8
7
  SmartFilter,
9
8
  type SmartFilterField,
10
9
  type SmartFilterValue,
@@ -18,10 +17,9 @@ const SECTIONS = [
18
17
  { value: "cancelled", label: "Отменены", count: 20 },
19
18
  ];
20
19
 
21
- const PERIODS = [
22
- { value: "7d", label: "7 дней" },
23
- { value: "30d", label: "30 дней" },
24
- { value: "quarter", label: "Квартал" },
20
+ const VIEWS = [
21
+ { value: "list", label: "Список" },
22
+ { value: "board", label: "Доска" },
25
23
  ];
26
24
 
27
25
  const FIELDS: SmartFilterField[] = [
@@ -46,7 +44,7 @@ const FIELDS: SmartFilterField[] = [
46
44
 
47
45
  export default function PageToolsExample() {
48
46
  const [section, setSection] = React.useState("all");
49
- const [period, setPeriod] = React.useState("30d");
47
+ const [view, setView] = React.useState("list");
50
48
  const [filter, setFilter] = React.useState<SmartFilterValue>({
51
49
  city: { include: ["msk"], exclude: [] },
52
50
  });
@@ -75,21 +73,13 @@ export default function PageToolsExample() {
75
73
  ))}
76
74
  </SegmentedControl.Root>
77
75
  <SmartFilter.Toolbar />
78
- <Select.Root value={period} onValueChange={setPeriod}>
79
- <Select.Trigger aria-label="Период">
80
- <Select.TriggerIcon>
81
- <Icon name="field.calendar" />
82
- </Select.TriggerIcon>
83
- <Select.Value />
84
- </Select.Trigger>
85
- <Select.Content>
86
- {PERIODS.map((item) => (
87
- <Select.Item key={item.value} value={item.value}>
88
- {item.label}
89
- </Select.Item>
90
- ))}
91
- </Select.Content>
92
- </Select.Root>
76
+ <SegmentedControl.Root value={view} onValueChange={setView} aria-label="Вид">
77
+ {VIEWS.map((item) => (
78
+ <SegmentedControl.Item key={item.value} value={item.value}>
79
+ {item.label}
80
+ </SegmentedControl.Item>
81
+ ))}
82
+ </SegmentedControl.Root>
93
83
  </Page.Tools>
94
84
  <Page.Actions>
95
85
  <Button.Root>
@@ -26,7 +26,10 @@ same place. Structure it by what people do, not by how the backend is split.
26
26
  it stays folded; its `…` row marks the page).
27
27
  - **One level of nesting.** `Sidebar.Sub` is one section with 2–6 views of the same objects
28
28
  (Сделки → Новые · В работе · Закрытые). The parent only discloses; an overview page is the first
29
- child («Все сделки»). Never a sub-list inside a sub-list — split the section or use page tabs.
29
+ child («Все сделки»). Never a sub-list inside a sub-list — split the section. Facets of one section
30
+ («Мессенджеры» → «Каналы» · «Статистика») are no sub-list: they are route links in the page's
31
+ `Page.Tools`, and the section's item stays `current` on every sub-route (a `NavLink` without `end`,
32
+ or `current={pathname.startsWith("/messengers")}`).
30
33
  - **Icons.** Every top-level item and every sub-list parent has a distinct icon (on the rail the
31
34
  icon is all that is left); children of a sub-list have none.
32
35
  - **Numbers and dots.** A plain `Sidebar.ItemCount` is information (12 new deals): it leaves the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prime-ui-kit",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "packageManager": "bun@1.3.13",
5
5
  "description": "React 19 UI kit for product interfaces: 79 composable components on one design contract — forms, selects, tables, overlays, navigation, app shell. CSS Modules, --prime-* tokens, light and dark themes, TypeScript, a11y, Next.js App Router ready. Ships COMPONENT.md docs, copyable examples and an agent skill.",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  ## When to use
9
9
  - Every route rendered inside `AppShell.Main`: the app is navigation and the page, and the page carries its own top (`Page.TopBar`), title (`Page.Title`, the `<h1>`) and body.
10
10
  - A work screen (a list, a board, a chat, a schedule): `Page.TopBar` with the title (`Page.Heading`), the page's tools (`Page.Tools`: sections, search and filter, view), all its buttons (`Page.Actions`) and the active filters (`Page.Chips`).
11
- - A filter stands next to what it filters. `Page.Tools` holds what acts on the whole page: sections, the view, a period that drives several blocks (the KPI cards and the table). The search, filter, chips and column chooser of a page that is one list stay there too — the list is the page. On a page that holds more than its table (KPI cards, a chart), they belong to the table's own tools row (DataTable `toolbar` + `toolbarChips`), not the bar.
11
+ - A filter stands next to what it filters. `Page.Tools` holds what acts on the whole page: sections, the view, a period that drives several blocks (the KPI cards and the table), and a search and filter only when it narrows several views of one set (a board and its list). A table's search, filter, chips, «Колонки» and selection menu always belong to its own tools row (DataTable `toolbar` + `toolbarChips`), on a page that is only the table too.
12
+ - A section with sub-pages (the «Мессенджеры» item of the Sidebar with «Каналы» and «Статистика»): each sub-page is a route of its own, `Page.Title` stays the section's name, and the switch is a `SegmentedControl` of links (`SegmentedControl.Item asChild`) in `Page.Tools`; the body is the current sub-page. The Sidebar keeps the section's item current on every sub-route — a router `NavLink` to the section's path without `end`, or `current={pathname.startsWith("/messengers")}`.
12
13
  - A document page (settings, a long form, help) or an overview (a dashboard): `Page.Header` in the flow — the large title and its buttons — and `Page.TopBar collapse`, which repeats the title once it scrolls away and keeps the path and the buttons in reach.
13
14
  - A nested record: a `Breadcrumb` path in `Page.Heading`; in the bar the title reads as its last level.
14
15
  - A text page held to a reading measure (`maxWidth="readable"`), a wide dashboard capped at the content width (`maxWidth="wide"`).
@@ -44,8 +45,8 @@ Page.Root the page column (<div>); maxWidth, fill, labe
44
45
  │ │ ├─ Page.Title <h1>, title-m, the path's last level, truncates
45
46
  │ │ └─ Page.Description <p>, muted caption under the whole line, truncates
46
47
  │ ├─ Page.Tools what acts on the whole page, in the order written: the one tool row (as the DataTable toolbar)
47
- │ │ ├─ SegmentedControl.Root sections, view (fullWidth unless set)
48
- │ │ ├─ SmartFilter.Toolbar search and filter: grows first on a line of its own
48
+ │ │ ├─ SegmentedControl.Root sections (route links of sub-pages), view (fullWidth unless set)
49
+ │ │ ├─ SmartFilter.Toolbar a search and filter over several views: grows first on a line of its own
49
50
  │ │ ├─ Datepicker.Root a period (fullWidth unless set)
50
51
  │ │ └─ Select.Root … a period, columns; the trigger fills its place
51
52
  │ ├─ Page.Actions every button of the page; icons, then «⋯» when short
@@ -122,7 +123,7 @@ Page.Root the page column (<div>); maxWidth, fill, labe
122
123
  | `…rest` | `HTMLAttributes<HTMLParagraphElement>` | — | `children`, `className` and the other paragraph attributes. |
123
124
 
124
125
  ### Page.Tools
125
- `ref` → `HTMLDivElement`. What acts on the whole page: sections, a shared period, view; the search of a one-list page. The tool row of the DataTable `toolbar`: a search takes the free room, the rest keep their width at the end. Named, a `group`.
126
+ `ref` → `HTMLDivElement`. What acts on the whole page: sections (route links of sub-pages), view, a shared period; a filter only over several views — a table's go in its `toolbar`. The DataTable tool row: a search takes the free room, the rest keep their width. Named, a `group`.
126
127
 
127
128
  | Prop | Type | Default | Description |
128
129
  |---|---|---|---|
@@ -342,7 +343,8 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
342
343
  | [top-bar.tsx](examples/top-bar.tsx) | A nested record in the top bar: the path ending in the title, a line of data, the page's tools and its buttons — `Page.TopBar`, `Page.Heading`, `Page.Tools`, `Page.Actions`. |
343
344
  | [loading.tsx](examples/loading.tsx) | A page whose figures load after it opens: the line of data keeps its place under the title while it loads, so nothing in the bar moves — `loading`. |
344
345
  | [collapse.tsx](examples/collapse.tsx) | An overview whose large title scrolls away under the top bar: the bar repeats it with its line of data and keeps the period and the page's buttons in reach — `collapse`. |
345
- | [tools.tsx](examples/tools.tsx) | The orders page's tools in its top bar: sections with counts, search and filter, the period, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. |
346
+ | [tools.tsx](examples/tools.tsx) | The orders page's tools in its top bar: sections with counts, the search and filter that narrow both views, the view, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. |
347
+ | [router.tsx](examples/router.tsx) | A section with sub-pages: the title is the section, each sub-page is a route of its own, the switch in the bar is their links and the body cross-fades between them; render it inside a router — `Page.Tools`, `asChild`. |
346
348
  | [actions-collapse.tsx](examples/actions-collapse.tsx) | The buttons of an invoices page on a tablet-wide column: the line runs short, so the secondary buttons show only their icons with the label as the name and the tooltip, the primary keeps its label — `Page.Actions`, `Button.Icon`. |
347
349
  | [sections.tsx](examples/sections.tsx) | A page with titled sections: tiles of one screen close together, a new topic set off by air, a link at the end of the heading — `Page.Section`, `label`, `description`, `Page.Actions`. |
348
350
  | [grid.tsx](examples/grid.tsx) | A dashboard board: four KPI tiles, a chart two rows high beside two panels, one gap on both axes — `Page.Grid`, `Page.Cell`, `span`, `rows`. |
@@ -361,7 +363,8 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
361
363
  - Buttons without an icon in a crowded bar → they cannot give way to an icon; give each `Button.Root` a `Button.Icon` beside its label.
362
364
  - Two places for buttons (some beside the title, the primary among the tools) → every button of the page goes into the one `Page.Actions`, the primary last.
363
365
  - `SmartFilter.Root` inside the bar, around `Page.Tools` → it holds no DOM; wrap it around `Page.Root` so its Toolbar (in `Page.Tools`) and its Chips (in `Page.Chips`) share it.
364
- - A table's search, filter, chips or «Колонки» in `Page.Tools` / `Page.Chips` above KPI cards or a chart they do not filter → they read as filters of the whole page; move them into the table's `toolbar` / `toolbarChips`. The bar keeps the title, a period that drives every block, sections and the primary action.
366
+ - A table's search, filter, chips or «Колонки» in `Page.Tools` / `Page.Chips` → they read as filters of the whole page and move when the page grows a block; they go in the table's `toolbar` / `toolbarChips` on every page, the one that is only the table too. The bar keeps the title, sections, the view, a period that drives every block and the page's buttons.
367
+ - Sub-pages of a section as a second Sidebar level, a tab strip inside `Page.Body`, or a title that names the sub-page → the title stays the section, the sub-pages are route links in `Page.Tools` (`SegmentedControl.Item asChild`), and the section's Sidebar item stays current on every sub-route.
365
368
  - `Page.Header` inside `Page.TopBar` → the bar takes `Page.Heading` and `Page.Actions` directly; `Page.Header` is the flow's document header.
366
369
  - A hand-made menu button for the phone Sidebar → the bar draws it while the Sidebar is off-canvas.
367
370
  - Hiding buttons with media queries on a phone → `Page.Actions` in the bar collapses and folds them by measured width.
@@ -0,0 +1,124 @@
1
+ /** A section with sub-pages: the title is the section, each sub-page is a route of its own, the switch in the bar is their links and the body cross-fades between them; render it inside a router — `Page.Tools`, `asChild`. */
2
+ import {
3
+ Button,
4
+ Card,
5
+ Crossfade,
6
+ DataTable,
7
+ type DataTableColumn,
8
+ Icon,
9
+ Page,
10
+ SegmentedControl,
11
+ } from "prime-ui-kit";
12
+ import { Link, Route, Routes, useLocation } from "react-router-dom";
13
+
14
+ type Channel = { id: string; name: string; dialogs: number; reply: string };
15
+
16
+ const SUB_PAGES = [
17
+ { to: "/messengers/channels", label: "Каналы" },
18
+ { to: "/messengers/stats", label: "Статистика" },
19
+ ];
20
+
21
+ const CHANNELS: Channel[] = [
22
+ { id: "tg", name: "Telegram · @prime_support", dialogs: 642, reply: "4 мин" },
23
+ { id: "wa", name: "WhatsApp · +7 495 120-44-10", dialogs: 391, reply: "7 мин" },
24
+ { id: "vk", name: "ВКонтакте · Прайм CRM", dialogs: 188, reply: "12 мин" },
25
+ { id: "avito", name: "Авито · Склад на Южной", dialogs: 59, reply: "25 мин" },
26
+ ];
27
+
28
+ const COLUMNS: DataTableColumn<Channel>[] = [
29
+ { id: "name", header: "Канал", cell: (row) => row.name },
30
+ {
31
+ id: "dialogs",
32
+ header: "Диалогов",
33
+ numeric: true,
34
+ headerAlign: "end",
35
+ cell: (row) => row.dialogs,
36
+ },
37
+ {
38
+ id: "reply",
39
+ header: "Первый ответ",
40
+ numeric: true,
41
+ headerAlign: "end",
42
+ cell: (row) => row.reply,
43
+ },
44
+ ];
45
+
46
+ export default function PageRouterExample() {
47
+ const location = useLocation();
48
+ // The first sub-page answers every other path of the section (here the preview's «/»).
49
+ const current = SUB_PAGES.find((page) => page.to === location.pathname)?.to ?? SUB_PAGES[0].to;
50
+
51
+ return (
52
+ <Page.Root>
53
+ <Page.TopBar sticky={false}>
54
+ <Page.Heading>
55
+ <Page.Title>Мессенджеры</Page.Title>
56
+ <Page.Description>4 канала · 1 280 диалогов за октябрь</Page.Description>
57
+ </Page.Heading>
58
+ <Page.Tools aria-label="Мессенджеры">
59
+ <SegmentedControl.Root value={current} aria-label="Подразделы">
60
+ {SUB_PAGES.map((page) => (
61
+ <SegmentedControl.Item key={page.to} value={page.to} asChild>
62
+ <Link to={page.to}>{page.label}</Link>
63
+ </SegmentedControl.Item>
64
+ ))}
65
+ </SegmentedControl.Root>
66
+ </Page.Tools>
67
+ <Page.Actions>
68
+ <Button.Root>
69
+ <Button.Icon>
70
+ <Icon name="action.add" />
71
+ </Button.Icon>
72
+ Подключить канал
73
+ </Button.Root>
74
+ </Page.Actions>
75
+ </Page.TopBar>
76
+ <Page.Body>
77
+ {/* `location` keeps the leaving sub-page on its own route while it fades out. */}
78
+ <Crossfade state={current}>
79
+ <Routes location={location}>
80
+ <Route
81
+ path="/messengers/stats"
82
+ element={
83
+ <Page.Grid>
84
+ <Page.Cell span={4}>
85
+ <Card.Root variant="stat-trend">
86
+ <Card.Label>Диалогов</Card.Label>
87
+ <Card.Value>1 280</Card.Value>
88
+ <Card.Delta tone="success">+14% к сентябрю</Card.Delta>
89
+ </Card.Root>
90
+ </Page.Cell>
91
+ <Page.Cell span={4}>
92
+ <Card.Root variant="stat-trend">
93
+ <Card.Label>Первый ответ</Card.Label>
94
+ <Card.Value>6 мин</Card.Value>
95
+ <Card.Delta tone="success">на 2 мин быстрее</Card.Delta>
96
+ </Card.Root>
97
+ </Page.Cell>
98
+ <Page.Cell span={4}>
99
+ <Card.Root variant="stat-trend">
100
+ <Card.Label>Без ответа</Card.Label>
101
+ <Card.Value>23</Card.Value>
102
+ <Card.Delta tone="danger">+5 за сутки</Card.Delta>
103
+ </Card.Root>
104
+ </Page.Cell>
105
+ </Page.Grid>
106
+ }
107
+ />
108
+ <Route
109
+ path="*"
110
+ element={
111
+ <DataTable
112
+ columns={COLUMNS}
113
+ rows={CHANNELS}
114
+ getRowKey={(row) => row.id}
115
+ paging="none"
116
+ />
117
+ }
118
+ />
119
+ </Routes>
120
+ </Crossfade>
121
+ </Page.Body>
122
+ </Page.Root>
123
+ );
124
+ }
@@ -1,10 +1,9 @@
1
- /** The orders page's tools in its top bar: sections with counts, search and filter, the period, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. */
1
+ /** The orders page's tools in its top bar: sections with counts, the search and filter that narrow both views, the view, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. */
2
2
  import {
3
3
  Button,
4
4
  Icon,
5
5
  Page,
6
6
  SegmentedControl,
7
- Select,
8
7
  SmartFilter,
9
8
  type SmartFilterField,
10
9
  type SmartFilterValue,
@@ -18,10 +17,9 @@ const SECTIONS = [
18
17
  { value: "cancelled", label: "Отменены", count: 20 },
19
18
  ];
20
19
 
21
- const PERIODS = [
22
- { value: "7d", label: "7 дней" },
23
- { value: "30d", label: "30 дней" },
24
- { value: "quarter", label: "Квартал" },
20
+ const VIEWS = [
21
+ { value: "list", label: "Список" },
22
+ { value: "board", label: "Доска" },
25
23
  ];
26
24
 
27
25
  const FIELDS: SmartFilterField[] = [
@@ -46,7 +44,7 @@ const FIELDS: SmartFilterField[] = [
46
44
 
47
45
  export default function PageToolsExample() {
48
46
  const [section, setSection] = React.useState("all");
49
- const [period, setPeriod] = React.useState("30d");
47
+ const [view, setView] = React.useState("list");
50
48
  const [filter, setFilter] = React.useState<SmartFilterValue>({
51
49
  city: { include: ["msk"], exclude: [] },
52
50
  });
@@ -75,21 +73,13 @@ export default function PageToolsExample() {
75
73
  ))}
76
74
  </SegmentedControl.Root>
77
75
  <SmartFilter.Toolbar />
78
- <Select.Root value={period} onValueChange={setPeriod}>
79
- <Select.Trigger aria-label="Период">
80
- <Select.TriggerIcon>
81
- <Icon name="field.calendar" />
82
- </Select.TriggerIcon>
83
- <Select.Value />
84
- </Select.Trigger>
85
- <Select.Content>
86
- {PERIODS.map((item) => (
87
- <Select.Item key={item.value} value={item.value}>
88
- {item.label}
89
- </Select.Item>
90
- ))}
91
- </Select.Content>
92
- </Select.Root>
76
+ <SegmentedControl.Root value={view} onValueChange={setView} aria-label="Вид">
77
+ {VIEWS.map((item) => (
78
+ <SegmentedControl.Item key={item.value} value={item.value}>
79
+ {item.label}
80
+ </SegmentedControl.Item>
81
+ ))}
82
+ </SegmentedControl.Root>
93
83
  </Page.Tools>
94
84
  <Page.Actions>
95
85
  <Button.Root>