prime-ui-kit 0.21.0 → 0.21.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.
- package/dist/chunks/{chunk-S27VR4ON.js → chunk-7JYPFRZ3.js} +77 -23
- package/dist/chunks/{chunk-S4H2ZKWJ.js → chunk-ME4VEAQ5.js} +40 -13
- package/dist/chunks/{chunk-IVS3J7AS.js → chunk-SQGPQOUO.js} +1 -1
- package/dist/chunks/{chunk-QFHE5GKW.js → chunk-T7IEJSTH.js} +569 -556
- package/dist/chunks/{chunk-6L7VWEY6.js → chunk-VQ3UCVDV.js} +2 -2
- package/dist/color-picker.js +3 -3
- package/dist/components/message/Message.d.ts +2 -2
- package/dist/components/scroll-container/ScrollContainer.d.ts +1 -1
- package/dist/icon-set.d.ts +1 -0
- package/dist/icon-set.js +4 -2
- package/dist/icons/glyphs/app-window.d.ts +2 -0
- package/dist/icons/registry.d.ts +2 -0
- package/dist/index.css +1 -1
- package/dist/index.js +3 -3
- package/dist/internal/overlay/DialogParts.d.ts +1 -1
- package/dist/internal/selectionIndicator.d.ts +9 -3
- package/dist/layout/page/Page.d.ts +1 -1
- package/dist/skill/api-contract.md +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/components/data-table/COMPONENT.md +1 -0
- package/dist/skill/reference/components/message/COMPONENT.md +6 -5
- package/dist/skill/reference/components/message/examples/bot-actions.tsx +6 -2
- package/dist/skill/reference/components/scroll-container/COMPONENT.md +5 -5
- package/dist/skill/reference/components/scroll-container/examples/overscroll.tsx +2 -2
- package/dist/skill/reference/components/tabs/COMPONENT.md +1 -1
- package/dist/skill/reference/icon-set.ts +1 -0
- package/dist/skill/reference/layout/app-shell/COMPONENT.md +1 -1
- 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/components/data-table/COMPONENT.md +1 -0
- package/src/components/message/COMPONENT.md +6 -5
- package/src/components/message/examples/bot-actions.tsx +6 -2
- package/src/components/scroll-container/COMPONENT.md +5 -5
- package/src/components/scroll-container/examples/overscroll.tsx +2 -2
- package/src/components/tabs/COMPONENT.md +1 -1
- package/src/layout/app-shell/COMPONENT.md +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
|
@@ -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.2",
|
|
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": {
|
|
@@ -305,6 +305,7 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
|
|
|
305
305
|
|
|
306
306
|
## Layout & spacing
|
|
307
307
|
- The root is a layer of the surface ladder (`data-depth`, one above its host): it fills its container on `--prime-color-layer-current`, radius 12, and clips full-bleed head and rows; zebra rows and the sticky column are one ladder step off it, the head two. On a card or in a modal it is the next layer by itself — never wrap it in a Card.
|
|
308
|
+
- The table scrolls inside its own viewport (a ScrollContainer, `axis="both"`): at its top or bottom — or when it fits and does not scroll — the wheel and the trackpad go on to the page; sideways it stops at its edge without a bounce, so the sticky column stays on its rows and the browser never swipes back.
|
|
308
309
|
- A filter stands next to what it filters. `toolbar` is the table's own tools row, always — on a page with other blocks (KPI cards, a chart) and on a page that is the table: its search and filter (`SmartFilter.Toolbar`, chips in `toolbarChips`), `DataTableColumnsMenu`, a table / cards view. Actions on the selected rows go to `renderSelectionActions`, never into `toolbar`: while rows are selected the table adds a «Действия · N» menu button last in the tool row — the search, filter and chooser stay, the selection never replaces the row. The page's bar keeps what acts on the whole page — the title with its line of data, sections or a view switch, a period that drives several blocks, the actions (`Page.Actions`). One control never gets a row of its own.
|
|
309
310
|
- The tools row is the one tool row of the kit (`Page.Tools` lays out the same way): controls in the order written, a search or a line of text (`Typography`) takes the free room, switchers, pickers and buttons keep their content width at the end edge. Under 36rem of table width the search takes a line of its own and a `SegmentedControl` (`fullWidth` unless set) fills the line it shares with the buttons; a search never parts from its filter button (`SmartFilter.Toolbar` keeps both). Wrap the table in `SmartFilter.Root` so the toolbar and the chips share one state.
|
|
310
311
|
- Pass the tools as direct children of `toolbar` or a fragment: the row lays out each element as a cell. A wrapper component that returns several controls is one cell — no gaps between them, no growing search.
|
|
@@ -233,7 +233,7 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
|
|
|
233
233
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes. |
|
|
234
234
|
|
|
235
235
|
### Message.Actions
|
|
236
|
-
`ref` → `HTMLDivElement`. A
|
|
236
|
+
`ref` → `HTMLDivElement`. A bot's inline keyboard row under the bubble: `Button.Root` keys (`soft` `neutral` by default) share one line, long labels ellipsize. A link key gets a corner mark: ↗ out of the product, a window into it; a promise from `onClick` keeps a key loading.
|
|
237
237
|
|
|
238
238
|
| Prop | Type | Default | Description |
|
|
239
239
|
|---|---|---|---|
|
|
@@ -363,7 +363,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
363
363
|
- `Message.Text` wraps anywhere; the meta stands at the end of its last line.
|
|
364
364
|
- Round controls (download, cancel, retry, play) are a Button `shape="circle"`: hover, press, focus ring and the touch area follow the circle. A file's action is its round tile (48): the transfer disk (`internal/transferDisk`, `xl`) with its ring, the retry or the download stand on it — never a separate button beside the name. Every transfer, either way (media, file, video note, voice), is that one disk.
|
|
365
365
|
- `Message.Typing` is the last row of a list; a chat puts it there itself (`Chat.Root typing`).
|
|
366
|
-
- **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and keys, on the chat's own surface (outside the bubble's fill). The keyboard is as wide as the message: the column takes the wider of the bubble and the keyboard (up to the bubble's cap) and the bubble widens to it, so their edges line up. A row is one line, as in Telegram: its keys share it equally (`flex: 1 1 0`, each key `fullWidth`), and a label longer than its share ellipsizes. Keys keep the control radius; the last row's outer bottom corners take the bubble's (`--prime-message-radius`), so the keyboard closes the message. A key is `Button.Root` `variant="soft" tone="neutral"` at the host tier unless it sets its own: Telegram's button `style` maps onto `tone` — `primary` → `accent`, `success`, `danger` (`variant="solid"` for a filled key). A custom emoji before the label is a `Button.Icon`. A link key (`asChild` around an
|
|
366
|
+
- **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and keys, on the chat's own surface (outside the bubble's fill). The keyboard is as wide as the message: the column takes the wider of the bubble and the keyboard (up to the bubble's cap) and the bubble widens to it, so their edges line up. A row is one line, as in Telegram: its keys share it equally (`flex: 1 1 0`, each key `fullWidth`), and a label longer than its share ellipsizes. Keys keep the control radius; the last row's outer bottom corners take the bubble's (`--prime-message-radius`), so the keyboard closes the message. A key is `Button.Root` `variant="soft" tone="neutral"` at the host tier unless it sets its own: Telegram's button `style` maps onto `tone` — `primary` → `accent`, `success`, `danger` (`variant="solid"` for a filled key). A custom emoji before the label is a `Button.Icon`. A link key (`asChild` around an element with `href`, or a router link's `to`) carries one mark (`xs`) at its top-end corner, out of the label's flow, chosen by where the address leads, as Telegram tells a url key from a web app key: `action.externalLink` when it leaves the product (another origin, or another scheme — `mailto:`, `tel:`, an app link — which opens another program), `action.openApp` when it opens a page of the product (a relative path, a hash, an absolute address on this origin; on the server, without `location`, every address that is not absolute). A callback key whose `onClick` returns a promise is `loading` until it settles: the spinner takes its leading icon's place or covers the label, the width never changes. A message with a keyboard has no tail: the keyboard closes it.
|
|
367
367
|
|
|
368
368
|
## Accessibility
|
|
369
369
|
|
|
@@ -390,7 +390,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
390
390
|
- Reactions are a group of buttons with `aria-pressed`; a chip is named «{emoji}: {count}», with `users` «{emoji} {count}: {users}» («❤️ 3: Анна, Пётр, Мария», «ещё N» when not everyone is listed); its avatars are `aria-hidden`; without `onReactionToggle` the chips are `disabled`. The reaction bar is a `toolbar` of toggle buttons named by the reaction («Нравится», «Огонь»…); its «⌄» is `aria-expanded`. The quick reaction is announced in the list's polite live region.
|
|
391
391
|
- `VideoNote` and `VoicePlayer` carry their own ARIA (see their pages); inside a message their transcript is a plain paragraph, selectable and marked by the chat's search.
|
|
392
392
|
- Media tiles are buttons «Открыть: {alt}» that open the album in MediaViewer, the focus returning to the tile on close; a transfer is the round cancel, retry or download button, the share its accessible description; a refused retry or cancel is announced politely; a done transfer's disk is `inert`.
|
|
393
|
-
- `Message.Actions` are plain buttons and links (`Button.Root`, `asChild`) with visible labels, in row order; in a chat's selection mode they take no presses.
|
|
393
|
+
- `Message.Actions` are plain buttons and links (`Button.Root`, `asChild`) with visible labels, in row order; the corner mark of a link key is decorative (`aria-hidden`), the link is named by its label; in a chat's selection mode they take no presses.
|
|
394
394
|
- Color is never the only signal: the status is named, the own reaction is `aria-pressed`, a selected message is `aria-selected` with a check, the author is text.
|
|
395
395
|
|
|
396
396
|
### Labels
|
|
@@ -506,7 +506,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
506
506
|
| [location.tsx](examples/location.tsx) | A place on a map picture from your map service with a pin, the place and its address, and a live location of a courier on its way — `Message.Location`, `href`, `live`. |
|
|
507
507
|
| [contact.tsx](examples/contact.tsx) | A shared contact: initials or a photo, name and phone, with the actions to write or save — `Message.Contact`, `phone`. |
|
|
508
508
|
| [link-preview.tsx](examples/link-preview.tsx) | A message with a link and the card of the page under it: site, title, description and picture — `Message.LinkPreview`, `site`, `image`. |
|
|
509
|
-
| [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, a
|
|
509
|
+
| [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, links marked by where they lead: a page of the product or another site — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. |
|
|
510
510
|
| [sticker.tsx](examples/sticker.tsx) | Stickers in a conversation: a picture by its address and a video sticker in your own player, no bubble, the time on a pill over the corner — `Message.Sticker`, `src`, `loading`. |
|
|
511
511
|
| [preview.tsx](examples/preview.tsx) | A bot's welcome message as its settings page shows it: the same bubble with its keyboard, nothing in it pressed or opened, the time in the zone of the profile — `preview`, `timeZone`. |
|
|
512
512
|
| [narrow.tsx](examples/narrow.tsx) | At phone width bubbles take up to 85% of the chat, long words and links wrap inside, an album keeps its rows — `Message.Root`, `Message.Media`. |
|
|
@@ -532,7 +532,8 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
532
532
|
- A transcript as a separate `Message.Text`, or a height animation of your own around it → `transcript` / `onTranscribe` on `VoicePlayer` and `VideoNote`: the text opens inside the same message, in one stroke with its height.
|
|
533
533
|
- A list, dates, search or selection built around messages → [Chat](../chat/COMPONENT.md).
|
|
534
534
|
- A bot's buttons placed inside `Message.Text`, or a row of buttons of your own under a message → `Message.Actions` with `Button.Root` children: one row per part, at the message's width.
|
|
535
|
+
- An arrow icon of your own in a link key (`Button.Icon` with `action.externalLink`) → leave the label alone: `Message.Actions` draws the key's one corner mark itself, ↗ for another site and a window for a page of the product; a second arrow doubles it.
|
|
535
536
|
|
|
536
537
|
## Related
|
|
537
|
-
- **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`, the buttons of `Message.Actions`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), [Skeleton](../skeleton/COMPONENT.md) (a sticker while it loads), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`, `action.externalLink` — the
|
|
538
|
+
- **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`, the buttons of `Message.Actions`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), [Skeleton](../skeleton/COMPONENT.md) (a sticker while it loads), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`, `action.externalLink` and `action.openApp` — the marks of a link key); `internal/messagePart` (the contract of content parts), `internal/messageList` (the contract with a chat's list), `internal/palette` (`hueOf`), `internal/Disclosure` (the reactions row, transcripts), `bubbleMorph` (every size change as one stroke), `bubbleHug` (a bubble of text as wide as its widest line), `internal/transferDisk` (the transfer of a file), `internal/mediaPill` (the meta, the author's pill), `internal/HighlightMatch` (search marks), `internal/richText` (Markdown), [VisuallyHidden](../visually-hidden/COMPONENT.md)
|
|
538
539
|
- **See also:** [Chat](../chat/COMPONENT.md) — the conversation the messages stand in; [Gallery](../gallery/COMPONENT.md) — the album of `Message.Media`; [VideoNote](../video-note/COMPONENT.md) — the round video message; [VoicePlayer](../voice-player/COMPONENT.md) — the content of a voice message; [Composer](../composer/COMPONENT.md) — the input under the list; [RichText](../rich-text/COMPONENT.md) — the text of `Message.Text` outside a message; [Timeline](../timeline/COMPONENT.md)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
/** A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, a
|
|
1
|
+
/** A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, links marked by where they lead: a page of the product or another site — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. */
|
|
2
2
|
import { Button, Icon, Message } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
5
|
import styles from "./examples.module.css";
|
|
6
6
|
|
|
7
7
|
const INVOICE_URL = "https://vector.example/invoices/4821";
|
|
8
|
+
const CATALOG_PATH = "#catalog";
|
|
8
9
|
const CONFIRM_DELAY_MS = 1200;
|
|
9
10
|
|
|
10
11
|
export default function MessageBotActionsExample() {
|
|
@@ -58,9 +59,12 @@ export default function MessageBotActionsExample() {
|
|
|
58
59
|
</Message.Actions>
|
|
59
60
|
) : null}
|
|
60
61
|
<Message.Actions>
|
|
62
|
+
<Button.Root asChild>
|
|
63
|
+
<a href={CATALOG_PATH}>Каталог</a>
|
|
64
|
+
</Button.Root>
|
|
61
65
|
<Button.Root asChild>
|
|
62
66
|
<a href={INVOICE_URL} target="_blank" rel="noreferrer">
|
|
63
|
-
|
|
67
|
+
Счёт
|
|
64
68
|
</a>
|
|
65
69
|
</Button.Root>
|
|
66
70
|
</Message.Actions>
|
|
@@ -48,7 +48,7 @@ ScrollContainer <div> (or the `as` tag); thin scrollbar, soft hidden edges
|
|
|
48
48
|
| `anchor` | `"start" \| "end"` | `"start"` | `end` — a chat or a log: opens at the end and follows new content while the person is at the end; scrolled away, the view holds still. |
|
|
49
49
|
| `onReachStart` | `() => void \| Promise<void>` | — | Fires within 120 px of the start, once per approach: load older content. A returned promise blocks repeats and sets `aria-busy`; content added above keeps the view. |
|
|
50
50
|
| `onReachEnd` | `() => void \| Promise<void>` | — | The scroll position came within 120 px of the end: load the next page. Same once-per-approach and promise rules as `onReachStart`. |
|
|
51
|
-
| `overscrollBehavior` | `"auto" \| "contain" \| "none"` | `"
|
|
51
|
+
| `overscrollBehavior` | `"auto" \| "contain" \| "none"` | `"auto"` | At the region's end: `auto` hands the scroll on to the page (sideways it stops); `contain` / `none` hold both axes — an open layer, where the page behind must not move. |
|
|
52
52
|
| `as` | `"div" \| "main" \| "aside" \| "section" \| "nav" \| "article" \| "pre"` | `"div"` | Root element; a landmark tag when the region is one, `pre` for preformatted text (CodeBlock). |
|
|
53
53
|
| `…rest` | `HTMLAttributes<HTMLElement>` | — | `children`, `className` (height, padding), `aria-label`, `tabIndex`, `role`, event handlers and the other attributes. |
|
|
54
54
|
|
|
@@ -64,9 +64,9 @@ ScrollContainer <div> (or the `as` tag); thin scrollbar, soft hidden edges
|
|
|
64
64
|
### overscrollBehavior
|
|
65
65
|
| Value | Looks like | Use when | Default |
|
|
66
66
|
|---|---|---|---|
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `none` |
|
|
67
|
+
| `auto` | At its end up or down the scroll goes on to the page (or the region around); a region that does not scroll never holds the wheel. Sideways it stops without a bounce: a page never scrolls sideways, and a sideways chain would swipe the browser's history | every region on a page: lists, tables, boards, chats, code | yes |
|
|
68
|
+
| `contain` | Reaching the end on either axis does not scroll what is behind | an open layer — Modal / Drawer / Sheet body, a floating panel, the CommandMenu list set it themselves; a panel of your own that floats over the page | |
|
|
69
|
+
| `none` | `contain` without the overscroll glow / bounce | a full-screen layer of your own | |
|
|
70
70
|
|
|
71
71
|
### edges
|
|
72
72
|
| Value | Looks like | Use when | Default |
|
|
@@ -165,7 +165,7 @@ No `labels`.
|
|
|
165
165
|
| [snap.tsx](examples/snap.tsx) | A strip of warehouse cards that always comes to rest on a card's start, scrolled by touch, a trackpad or the mouse wheel — `snap`, `axis`. |
|
|
166
166
|
| [lock.tsx](examples/lock.tsx) | While an order is opened for review, the queue behind it holds still: no wheel, touch, key or drag moves it — `lock`. |
|
|
167
167
|
| [auto-scroll.tsx](examples/auto-scroll.tsx) | A task dragged to the top or bottom edge of a scrolling list scrolls it, faster the closer it comes — `ScrollContainer`, `Dnd.Sortable`. |
|
|
168
|
-
| [overscroll.tsx](examples/overscroll.tsx) |
|
|
168
|
+
| [overscroll.tsx](examples/overscroll.tsx) | A panel that keeps its scroll: at the end of the list the page behind stays put — `overscrollBehavior`. |
|
|
169
169
|
| [log.tsx](examples/log.tsx) | A live exchange log pinned to its last entry: a new entry keeps it at the end, scrolling up loads older ones without moving what is on screen — `anchor`, `onReachStart`. |
|
|
170
170
|
| [load-more.tsx](examples/load-more.tsx) | An order log that loads the next page as the reader nears its end, with loading lines in the rows' place — `onReachEnd`. |
|
|
171
171
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** A panel that keeps its scroll: at the end of the list the page behind stays put — `overscrollBehavior`. */
|
|
2
2
|
import { Card, ScrollContainer, Typography } from "prime-ui-kit";
|
|
3
3
|
|
|
4
4
|
import styles from "./examples.module.css";
|
|
@@ -11,7 +11,7 @@ export default function ScrollContainerOverscrollExample() {
|
|
|
11
11
|
<Card.Header>
|
|
12
12
|
<Card.Title>Журнал выгрузок</Card.Title>
|
|
13
13
|
</Card.Header>
|
|
14
|
-
<ScrollContainer overscrollBehavior="
|
|
14
|
+
<ScrollContainer overscrollBehavior="contain" tabIndex={0} aria-label="Журнал выгрузок">
|
|
15
15
|
<Card.List>
|
|
16
16
|
{HOURS.map((hour) => (
|
|
17
17
|
<Card.ListItem key={hour}>
|
|
@@ -216,7 +216,7 @@ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-
|
|
|
216
216
|
```
|
|
217
217
|
- The folder paints the host's `layer-current`, so the active tab is always the colour of the surface it stands on; controls in a panel read the host's field and fill tokens.
|
|
218
218
|
- Vertical: the column is as wide as its widest section (at least four control heights, at most 40% of the root) and as tall as its sections; the panel sits beside it, one gap away.
|
|
219
|
-
- The list is a switcher (foundation §9): it never wraps — it collapses, then scrolls horizontally with faded edges (hidden scrollbar,
|
|
219
|
+
- The list is a switcher (foundation §9): it never wraps — it collapses, then scrolls horizontally with faded edges (hidden scrollbar; at its sideways edge it stops, a vertical wheel goes on to the page). Give the list's container a width (`min-width: 0` on flex children).
|
|
220
220
|
- Without `size` the tabs take the tier of their host (a Card, a Drawer, a form), else `m`.
|
|
221
221
|
- A horizontal tab is never narrower than its content (`min-width: max-content`; the tab bounds that content between `minItemWidth` and `maxItemWidth`), so a label is never cut to make room. Full width, tabs tend to equal widths, not widths in proportion to their labels (`flex: 1 1 0`): a long label keeps its width, the short ones share the rest.
|
|
222
222
|
- A vertical Tabs.Root is a size container: give it a width (it measures itself to switch to a row).
|
|
@@ -128,7 +128,7 @@ The navigation of the nearest `AppShell.Root` for a trigger outside the rail (a
|
|
|
128
128
|
- Two full-height planes edge to edge: no inset, radius, shadow or border — the boundary is the fill change (the Sidebar rail is layer 1, the panel the page: white rail beside a gray page in light, a rail one step lighter than the near-black page in dark).
|
|
129
129
|
- Main hands the page its edge air: `--page-gutter` is the page gutter `--prime-layout-gutter` — `--prime-layout-gutter-s` (16) → `-m` (24) from 640px → `-l` (32) from 1024px; `--page-top` is `--prime-space-6` (24) → `--prime-space-8` (32) from 1024px. `Page.Root` pads itself with them (its top bar steps out to the panel's edges). Main itself pads only the bottom: `--prime-space-16`, the scroll tail of a long page. Inside Main the page gutter is the small one again, so a frame nested in the page starts from its own gutter, not the shell's.
|
|
130
130
|
- A page that fills the panel height (`Page.Root fill`): Main's bottom padding drops to the side gutter, so no long scroll tail stays under it — beside the Sidebar rail too.
|
|
131
|
-
- Main
|
|
131
|
+
- Main hands its scroll on at its end (ScrollContainer `overscrollBehavior="auto"`): without `fillViewport` it is not a scroller and the document scrolls under the pointer.
|
|
132
132
|
- Nav column: sticky at the top, viewport high, `z-index: --prime-z-sticky` (above the panel, so the compact Sidebar toggle can ride the rail edge); with `fillViewport` it fills the shell height instead.
|
|
133
133
|
- Footer: no padding of its own (BottomNav keeps the bottom safe-area inset), sticky at the bottom, `z-index: --prime-z-sticky`.
|
|
134
134
|
- A floating BottomNav (`floating`) in the footer: the footer takes no height, the capsule overlays the page, and Main keeps extra bottom room (the capsule height and the safe-area inset) so its last line never ends up under it.
|
|
@@ -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
|