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
package/dist/index.js
CHANGED
|
@@ -88,7 +88,7 @@ import {
|
|
|
88
88
|
useSidebar,
|
|
89
89
|
useTokenValue,
|
|
90
90
|
useUploadQueue
|
|
91
|
-
} from "./chunks/chunk-
|
|
91
|
+
} from "./chunks/chunk-ME4VEAQ5.js";
|
|
92
92
|
import {
|
|
93
93
|
Avatar,
|
|
94
94
|
Badge,
|
|
@@ -123,11 +123,11 @@ import {
|
|
|
123
123
|
tierMappings,
|
|
124
124
|
tones,
|
|
125
125
|
variants
|
|
126
|
-
} from "./chunks/chunk-
|
|
126
|
+
} from "./chunks/chunk-7JYPFRZ3.js";
|
|
127
127
|
import {
|
|
128
128
|
ControlSizeProvider,
|
|
129
129
|
useOptionalControlSize
|
|
130
|
-
} from "./chunks/chunk-
|
|
130
|
+
} from "./chunks/chunk-T7IEJSTH.js";
|
|
131
131
|
export {
|
|
132
132
|
Accordion,
|
|
133
133
|
AppShell,
|
|
@@ -214,7 +214,7 @@ export declare namespace DialogActions {
|
|
|
214
214
|
export type DialogBodyProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
215
215
|
ref?: React.Ref<HTMLDivElement>;
|
|
216
216
|
};
|
|
217
|
-
/** The only scrolling zone; header and footer stay put. */
|
|
217
|
+
/** The only scrolling zone; header and footer stay put. An open layer: its end never scrolls the page. */
|
|
218
218
|
export declare function DialogBody({ className, ...rest }: DialogBodyProps): import("react/jsx-runtime").JSX.Element;
|
|
219
219
|
export declare namespace DialogBody {
|
|
220
220
|
var displayName: string;
|
|
@@ -12,11 +12,17 @@ export declare function setFlag(element: HTMLElement, name: `data-${string}`, on
|
|
|
12
12
|
/**
|
|
13
13
|
* Places the indicator over `target` — its offset box in the parent they share — straight in the
|
|
14
14
|
* DOM, so a resize or a font swap never re-renders the host. With `animate` (a person's choice) a
|
|
15
|
-
* move of a visible indicator glides
|
|
16
|
-
*
|
|
15
|
+
* move of a visible indicator glides, and an indicator that appears or leaves fades
|
|
16
|
+
* (`data-fade`); every other placement snaps, and its first one shows it at once: a fade on mount
|
|
17
|
+
* would leave the selected item without its fill while the host opens (a Drawer sliding in), and
|
|
18
|
+
* an opacity transition makes the indicator a composited layer that browsers paint over the
|
|
19
|
+
* item's text inside the row's edge-fade mask until it ends.
|
|
17
20
|
*/
|
|
18
21
|
export declare function placeIndicator(indicator: HTMLElement, target: HTMLElement | null, animate: boolean): void;
|
|
19
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* `onTransitionEnd` of the indicator: the glide or the fade is over, so the next layout change
|
|
24
|
+
* snaps.
|
|
25
|
+
*/
|
|
20
26
|
export declare function endIndicatorGlide(event: Pick<React.TransitionEvent<HTMLElement>, "target" | "currentTarget" | "propertyName">): void;
|
|
21
27
|
/**
|
|
22
28
|
* Scrolls a sideways scroller (never the page: `scrollIntoView` would scroll it too) so `item` is
|
|
@@ -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`
|
|
@@ -377,7 +377,7 @@ crumb of your own.
|
|
|
377
377
|
<!-- icons:start -->
|
|
378
378
|
`<Icon name="…" />` names:
|
|
379
379
|
- `nav.*`: `chevronDown`, `chevronLeft`, `chevronRight`, `chevronUp`, `chevronsLeft`, `chevronsUpDown`, `dashboard`, `home`, `itemDot`, `layoutGrid`, `menu`, `sidebarCollapse`, `sidebarExpand`;
|
|
380
|
-
- `action.*`: `add`, `addContact`, `attach`, `check`, `close`, `copy`, `delete`, `download`, `drag`, `edit`, `externalLink`, `eyedropper`, `filter`, `forward`, `login`, `logout`, `more`, `pause`, `pin`, `play`, `react`, `refresh`, `report`, `remove`, `reply`, `search`, `send`, `settings`, `transcribe`, `upload`, `video`, `voice`, `zoomIn`, `zoomOut`;
|
|
380
|
+
- `action.*`: `add`, `addContact`, `attach`, `check`, `close`, `copy`, `delete`, `download`, `drag`, `edit`, `externalLink`, `eyedropper`, `filter`, `forward`, `login`, `logout`, `more`, `openApp`, `pause`, `pin`, `play`, `react`, `refresh`, `report`, `remove`, `reply`, `search`, `send`, `settings`, `transcribe`, `upload`, `video`, `voice`, `zoomIn`, `zoomOut`;
|
|
381
381
|
- `field.*`: `calendar`, `email`, `password.show`, `password.hide`;
|
|
382
382
|
- `format.*`: `bold`, `code`, `italic`, `link`, `list`, `quote`, `spoiler`, `strikethrough`, `underline`;
|
|
383
383
|
- `object.*`: `activity`, `bell`, `bike`, `book`, `cart`, `chart`, `crown`, `disc`, `document`, `file`, `fuel`, `funnel`, `globe`, `headset`, `image`, `inbox`, `key`, `mapPin`, `megaphone`, `message`, `motorbike`, `package`, `phone`, `receipt`, `rocket`, `scale`, `storage`, `tasks`, `trophy`, `truck`, `user`, `users`, `wallet`;
|
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
|
|
@@ -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).
|
|
@@ -20,6 +20,7 @@ export { AlignVerticalSpaceAround as AlignVerticalSpaceAroundIcon } from "./icon
|
|
|
20
20
|
export { Ambulance as AmbulanceIcon } from "./icons/glyphs/ambulance";
|
|
21
21
|
export { Angry as AngryIcon } from "./icons/glyphs/angry";
|
|
22
22
|
export { Annoyed as AnnoyedIcon } from "./icons/glyphs/annoyed";
|
|
23
|
+
export { AppWindow as AppWindowIcon } from "./icons/glyphs/app-window";
|
|
23
24
|
export { Archive as ArchiveIcon } from "./icons/glyphs/archive";
|
|
24
25
|
export { ArrowBigDown as ArrowBigDownIcon } from "./icons/glyphs/arrow-big-down";
|
|
25
26
|
export { ArrowBigDownDash as ArrowBigDownDashIcon } from "./icons/glyphs/arrow-big-down-dash";
|
|
@@ -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
|
+
}
|