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.
- package/dist/index.css +1 -1
- package/dist/layout/page/Page.d.ts +1 -1
- package/dist/skill/checklist.md +1 -0
- package/dist/skill/choosing.md +3 -2
- package/dist/skill/composition.md +58 -1
- package/dist/skill/layouts.md +1 -0
- package/dist/skill/reference/layout/page/COMPONENT.md +9 -6
- package/dist/skill/reference/layout/page/examples/router.tsx +124 -0
- package/dist/skill/reference/layout/page/examples/tools.tsx +12 -22
- package/dist/skill/reference/layout/sidebar/COMPONENT.md +4 -1
- package/package.json +1 -1
- package/src/layout/page/COMPONENT.md +9 -6
- package/src/layout/page/examples/router.tsx +124 -0
- package/src/layout/page/examples/tools.tsx +12 -22
- package/src/layout/sidebar/COMPONENT.md +4 -1
|
@@ -128,7 +128,7 @@ declare namespace PageActions {
|
|
|
128
128
|
}
|
|
129
129
|
export type PageToolsProps = DivProps;
|
|
130
130
|
/**
|
|
131
|
-
* The page's tools — sections,
|
|
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`
|
package/dist/skill/checklist.md
CHANGED
|
@@ -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)).
|
package/dist/skill/choosing.md
CHANGED
|
@@ -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`;
|
|
147
|
-
|
|
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
|
|
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.
|
package/dist/skill/layouts.md
CHANGED
|
@@ -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)
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
22
|
-
{ value: "
|
|
23
|
-
{ value: "
|
|
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 [
|
|
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
|
-
<
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
</
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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.
|
|
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)
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
22
|
-
{ value: "
|
|
23
|
-
{ value: "
|
|
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 [
|
|
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
|
-
<
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
</
|
|
83
|
-
|
|
84
|
-
|
|
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>
|