prime-ui-kit 0.20.1 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunks/{chunk-4KONMO7Z.js → chunk-6L7VWEY6.js} +1 -1
- package/dist/chunks/{chunk-3PY52JP3.js → chunk-S27VR4ON.js} +5828 -5596
- package/dist/chunks/{chunk-4374EBWV.js → chunk-S4H2ZKWJ.js} +1049 -644
- package/dist/color-picker.js +2 -2
- package/dist/components/badge/Badge.d.ts +19 -2
- package/dist/components/card/Card.d.ts +10 -3
- package/dist/components/composer/Composer.d.ts +49 -1
- package/dist/components/crossfade/Crossfade.d.ts +6 -4
- package/dist/components/data-table/DataTable.d.ts +10 -2
- package/dist/components/data-table/Row.d.ts +12 -1
- package/dist/components/data-table/SelectionMenu.d.ts +20 -0
- package/dist/components/data-table/useRowSelection.d.ts +2 -0
- package/dist/components/empty-page/EmptyPage.d.ts +5 -2
- package/dist/components/hint/Hint.d.ts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/input/Input.d.ts +15 -3
- package/dist/components/message/Message.d.ts +6 -3
- package/dist/components/segmented-control/SegmentedControl.d.ts +5 -1
- package/dist/components/smart-filter/SmartFilter.d.ts +4 -0
- package/dist/components/spinner/Spinner.d.ts +3 -2
- package/dist/components/tabs/Tabs.d.ts +10 -1
- package/dist/hooks/useModalLayer.d.ts +15 -8
- package/dist/index.css +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/RegionSwap.d.ts +18 -10
- package/dist/internal/RollingNumber.d.ts +7 -5
- package/dist/internal/ToolRow.d.ts +6 -0
- package/dist/internal/mediaSlot.d.ts +29 -0
- package/dist/internal/overlay/DialogParts.d.ts +30 -6
- package/dist/internal/overlay/focus.d.ts +18 -3
- package/dist/internal/partPresence.d.ts +19 -2
- package/dist/layout/page/Page.d.ts +1 -1
- package/dist/layout/sidebar/Sidebar.d.ts +3 -2
- package/dist/skill/anti-slop.md +35 -37
- package/dist/skill/api-contract.md +50 -2
- package/dist/skill/cheatsheet.md +15 -10
- package/dist/skill/checklist.md +20 -6
- package/dist/skill/choosing.md +82 -8
- package/dist/skill/components.md +1 -1
- package/dist/skill/composition.md +268 -51
- package/dist/skill/foundations.md +17 -6
- package/dist/skill/layouts.md +65 -14
- package/dist/skill/motion.md +9 -3
- package/dist/skill/reference/components/avatar/COMPONENT.md +1 -0
- package/dist/skill/reference/components/badge/COMPONENT.md +29 -5
- package/dist/skill/reference/components/badge/examples/examples.module.css +14 -0
- package/dist/skill/reference/components/badge/examples/overflow-count.tsx +32 -0
- package/dist/skill/reference/components/banner/COMPONENT.md +1 -1
- package/dist/skill/reference/components/card/COMPONENT.md +8 -4
- package/dist/skill/reference/components/card/examples/person-header.tsx +38 -0
- package/dist/skill/reference/components/composer/COMPONENT.md +35 -5
- package/dist/skill/reference/components/composer/examples/bot-keyboard.tsx +43 -0
- package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
- package/dist/skill/reference/components/crossfade/COMPONENT.md +18 -10
- package/dist/skill/reference/components/data-table/COMPONENT.md +17 -11
- package/dist/skill/reference/components/data-table/examples/screen.tsx +6 -9
- package/dist/skill/reference/components/data-table/examples/selection.tsx +60 -18
- package/dist/skill/reference/components/drawer/COMPONENT.md +41 -19
- package/dist/skill/reference/components/drawer/examples/footer-actions.tsx +54 -0
- package/dist/skill/reference/components/drawer/examples/header-actions.tsx +46 -25
- package/dist/skill/reference/components/drawer/examples/link-loading.tsx +57 -0
- package/dist/skill/reference/components/drawer/examples/screen.tsx +3 -5
- package/dist/skill/reference/components/empty-page/COMPONENT.md +2 -2
- package/dist/skill/reference/components/field-list/COMPONENT.md +4 -4
- package/dist/skill/reference/components/field-list/examples/row-menu.tsx +1 -1
- package/dist/skill/reference/components/hint/COMPONENT.md +1 -1
- package/dist/skill/reference/components/input/COMPONENT.md +16 -3
- package/dist/skill/reference/components/input/examples/password-policy.tsx +65 -0
- package/dist/skill/reference/components/list/COMPONENT.md +1 -1
- package/dist/skill/reference/components/login-form/COMPONENT.md +13 -11
- package/dist/skill/reference/components/login-form/examples/overview.tsx +5 -5
- package/dist/skill/reference/components/login-form/examples/states.tsx +7 -6
- package/dist/skill/reference/components/message/COMPONENT.md +5 -5
- package/dist/skill/reference/components/message/examples/bot-actions.tsx +47 -28
- package/dist/skill/reference/components/modal/COMPONENT.md +31 -12
- package/dist/skill/reference/components/modal/examples/structure.tsx +2 -4
- package/dist/skill/reference/components/segmented-control/COMPONENT.md +11 -6
- package/dist/skill/reference/components/segmented-control/examples/router.tsx +22 -0
- package/dist/skill/reference/components/select/COMPONENT.md +4 -1
- package/dist/skill/reference/components/sheet/COMPONENT.md +31 -12
- package/dist/skill/reference/components/sheet/examples/header-actions.tsx +5 -3
- package/dist/skill/reference/components/skeleton/COMPONENT.md +3 -2
- package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -1
- package/dist/skill/reference/components/smart-filter/examples/ranges.tsx +8 -2
- package/dist/skill/reference/components/spinner/COMPONENT.md +1 -1
- package/dist/skill/reference/components/tabs/COMPONENT.md +7 -4
- package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +18 -2
- package/dist/skill/reference/components/thumbnail/COMPONENT.md +1 -0
- package/dist/skill/reference/components/tooltip/COMPONENT.md +3 -3
- package/dist/skill/reference/foundation.md +15 -9
- package/dist/skill/reference/layout/page/COMPONENT.md +10 -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 +8 -3
- package/dist/skill/reference/layout/sidebar/examples/account-profile.tsx +66 -0
- package/dist/skill/responsive.md +28 -6
- package/dist/skill/ux.md +97 -2
- package/dist/tokens/semantic.d.ts +2 -1
- package/dist/tokens/themes/dark.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/avatar/COMPONENT.md +1 -0
- package/src/components/badge/COMPONENT.md +29 -5
- package/src/components/badge/examples/examples.module.css +14 -0
- package/src/components/badge/examples/overflow-count.tsx +32 -0
- package/src/components/banner/COMPONENT.md +1 -1
- package/src/components/card/COMPONENT.md +8 -4
- package/src/components/card/examples/person-header.tsx +38 -0
- package/src/components/composer/COMPONENT.md +35 -5
- package/src/components/composer/examples/bot-keyboard.tsx +43 -0
- package/src/components/confirm-dialog/COMPONENT.md +1 -1
- package/src/components/crossfade/COMPONENT.md +18 -10
- package/src/components/data-table/COMPONENT.md +17 -11
- package/src/components/data-table/examples/screen.tsx +6 -9
- package/src/components/data-table/examples/selection.tsx +60 -18
- package/src/components/drawer/COMPONENT.md +41 -19
- package/src/components/drawer/examples/footer-actions.tsx +54 -0
- package/src/components/drawer/examples/header-actions.tsx +46 -25
- package/src/components/drawer/examples/link-loading.tsx +57 -0
- package/src/components/drawer/examples/screen.tsx +3 -5
- package/src/components/empty-page/COMPONENT.md +2 -2
- package/src/components/field-list/COMPONENT.md +4 -4
- package/src/components/field-list/examples/row-menu.tsx +1 -1
- package/src/components/hint/COMPONENT.md +1 -1
- package/src/components/input/COMPONENT.md +16 -3
- package/src/components/input/examples/password-policy.tsx +65 -0
- package/src/components/list/COMPONENT.md +1 -1
- package/src/components/login-form/COMPONENT.md +13 -11
- package/src/components/login-form/examples/overview.tsx +5 -5
- package/src/components/login-form/examples/states.tsx +7 -6
- package/src/components/message/COMPONENT.md +5 -5
- package/src/components/message/examples/bot-actions.tsx +47 -28
- package/src/components/modal/COMPONENT.md +31 -12
- package/src/components/modal/examples/structure.tsx +2 -4
- package/src/components/segmented-control/COMPONENT.md +11 -6
- package/src/components/segmented-control/examples/router.tsx +22 -0
- package/src/components/select/COMPONENT.md +4 -1
- package/src/components/sheet/COMPONENT.md +31 -12
- package/src/components/sheet/examples/header-actions.tsx +5 -3
- package/src/components/skeleton/COMPONENT.md +3 -2
- package/src/components/smart-filter/COMPONENT.md +2 -1
- package/src/components/smart-filter/examples/ranges.tsx +8 -2
- package/src/components/spinner/COMPONENT.md +1 -1
- package/src/components/tabs/COMPONENT.md +7 -4
- package/src/components/tabs/examples/in-drawer.tsx +18 -2
- package/src/components/thumbnail/COMPONENT.md +1 -0
- package/src/components/tooltip/COMPONENT.md +3 -3
- package/src/layout/page/COMPONENT.md +10 -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 +8 -3
- package/src/layout/sidebar/examples/account-profile.tsx +66 -0
- package/src/styles/theme-dark.css +1 -0
- package/src/styles/theme-light.css +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Router links as the segments: the current route is the value, each segment is a link and the row is a navigation; render it inside a router — `asChild`, `value`. */
|
|
2
|
+
import { SegmentedControl } from "prime-ui-kit";
|
|
3
|
+
import { Link, useLocation } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
const SECTIONS = [
|
|
6
|
+
{ to: "/", label: "Сотрудники" },
|
|
7
|
+
{ to: "/roles", label: "Роли" },
|
|
8
|
+
{ to: "/invites", label: "Приглашения" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export default function SegmentedControlRouterExample() {
|
|
12
|
+
const location = useLocation();
|
|
13
|
+
return (
|
|
14
|
+
<SegmentedControl.Root value={location.pathname} aria-label="Команда и роли">
|
|
15
|
+
{SECTIONS.map((section) => (
|
|
16
|
+
<SegmentedControl.Item key={section.to} value={section.to} asChild>
|
|
17
|
+
<Link to={section.to}>{section.label}</Link>
|
|
18
|
+
</SegmentedControl.Item>
|
|
19
|
+
))}
|
|
20
|
+
</SegmentedControl.Root>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -238,7 +238,10 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
238
238
|
- A rich row and its trigger use the same parts in the same order: leading media (`Avatar.Root`,
|
|
239
239
|
`Thumbnail.Root`) → `Select.ItemText` (+ `Select.ItemDescription`) → trailing `Badge.Root` /
|
|
240
240
|
`Select.ItemMeta`. In a row the trailing part sits before the check; in the trigger, at the end
|
|
241
|
-
before the chevron.
|
|
241
|
+
before the chevron. Every single-choice row keeps the check's column, chosen or not, so the
|
|
242
|
+
trailing parts of all rows end on one line, under the trailing part of the trigger (the check
|
|
243
|
+
column stands where the chevron does), and picking another option moves no badge or price.
|
|
244
|
+
Draw the trigger with `renderValue` returning the row's parts, so the picked
|
|
242
245
|
person keeps their avatar and role badge when the list is closed:
|
|
243
246
|
|
|
244
247
|
```tsx
|
|
@@ -29,12 +29,13 @@ Sheet.Root state and dismiss policy (no DOM)
|
|
|
29
29
|
├─ (grab handle) aria-hidden bar on top while the sheet swipes (`closeOnOutsideClick`); a drag starts here
|
|
30
30
|
├─ Sheet.Guard no DOM: holds the sheet (busy / dirty) from inside
|
|
31
31
|
├─ Sheet.Header optional: [Icon] [Title + Description] [Actions] [close Button]
|
|
32
|
-
│ ├─ Sheet.Icon
|
|
33
|
-
│ ├─ Sheet.Title <h2
|
|
34
|
-
│ ├─ Sheet.Description <p
|
|
35
|
-
│ └─ Sheet.Actions
|
|
32
|
+
│ ├─ Sheet.Icon tone tile around an Icon, or the record's Avatar / Thumbnail, no tile, 40
|
|
33
|
+
│ ├─ Sheet.Title line 1: <h2>; `loading` holds it with a skeleton
|
|
34
|
+
│ ├─ Sheet.Description line 2: <p>; the record's status Badges lead it (tier s), then the text
|
|
35
|
+
│ └─ Sheet.Actions buttons only (icon buttons, a ⋯ Dropdown), tier s, on the title's line
|
|
36
36
|
├─ Sheet.Body the content; the only scrolling zone (ScrollContainer)
|
|
37
37
|
└─ Sheet.Footer optional: only for a form with an explicit submit
|
|
38
|
+
├─ Sheet.Actions the start group: an action apart from the main pair
|
|
38
39
|
├─ Sheet.Meta <p>, muted status at the start
|
|
39
40
|
└─ Sheet.Close clones its child; closes on click
|
|
40
41
|
```
|
|
@@ -77,19 +78,19 @@ No DOM, no ref. State and dismiss policy.
|
|
|
77
78
|
| `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the `<header>`. |
|
|
78
79
|
|
|
79
80
|
### Sheet.Actions
|
|
80
|
-
`ref` → `HTMLDivElement`.
|
|
81
|
+
`ref` → `HTMLDivElement`. Header buttons only (icon buttons, a ⋯ Dropdown; a status goes in `Sheet.Description`), tier `s`, on the title's line; under it below 12rem. In `Sheet.Footer`: the start group at the footer tier.
|
|
81
82
|
|
|
82
83
|
| Prop | Type | Default | Description |
|
|
83
84
|
|---|---|---|---|
|
|
84
85
|
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `children` and the other attributes of the row. |
|
|
85
86
|
|
|
86
87
|
### Sheet.Icon
|
|
87
|
-
`ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around an `Avatar.Root` or a `Thumbnail.Root`
|
|
88
|
+
`ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around the record's picture (an `Avatar.Root` or a `Thumbnail.Root` anywhere inside, also from your own component) no tile (`data-media`), the picture at 40 px.
|
|
88
89
|
|
|
89
90
|
| Prop | Type | Default | Description |
|
|
90
91
|
|---|---|---|---|
|
|
91
92
|
| `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color; not used around a picture. |
|
|
92
|
-
| `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (
|
|
93
|
+
| `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (size `l`) or a `Thumbnail.Root` (size `m`), set by the slot. |
|
|
93
94
|
| `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
|
|
94
95
|
|
|
95
96
|
### Sheet.Title
|
|
@@ -97,10 +98,11 @@ No DOM, no ref. State and dismiss policy.
|
|
|
97
98
|
|
|
98
99
|
| Prop | Type | Default | Description |
|
|
99
100
|
|---|---|---|---|
|
|
101
|
+
| `loading` | `boolean` | `false` | The name loads (a record opened by a link): a `Skeleton` holds the title line and `aria-busy` is set, so the header never moves; the name fades in. |
|
|
100
102
|
| `…rest` | `Omit<HTMLAttributes<HTMLHeadingElement>, "id">` | — | `className`, `children` and the other attributes except `id` (the dialog owns it). |
|
|
101
103
|
|
|
102
104
|
### Sheet.Description
|
|
103
|
-
`ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog.
|
|
105
|
+
`ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog. The record's status `Badge.Root`s go here: first in the line, chip tier `s` (20 px), the text wraps under them when narrow.
|
|
104
106
|
|
|
105
107
|
| Prop | Type | Default | Description |
|
|
106
108
|
|---|---|---|---|
|
|
@@ -115,7 +117,7 @@ No DOM, no ref. State and dismiss policy.
|
|
|
115
117
|
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `children` and the other attributes of the zone. |
|
|
116
118
|
|
|
117
119
|
### Sheet.Footer
|
|
118
|
-
`ref` → `HTMLElement`. Renders `<footer>` with the actions, primary last
|
|
120
|
+
`ref` → `HTMLElement`. Renders `<footer>` with the actions, primary last; at the start an optional `Sheet.Actions` (an action apart from the main pair) and `Meta`. Stacked, the start comes first and the primary stays last.
|
|
119
121
|
|
|
120
122
|
| Prop | Type | Default | Description |
|
|
121
123
|
|---|---|---|---|
|
|
@@ -161,7 +163,7 @@ Sheet has no `variant`, `side` or `size`: it always rises from the bottom edge,
|
|
|
161
163
|
| `fill` | equal-width buttons in one row | two equal answers | |
|
|
162
164
|
|
|
163
165
|
### tone (Icon)
|
|
164
|
-
`neutral` (default) · `accent` · `success` · `warning` · `danger` · `info` — soft tile fill and icon color, as in Modal. Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root`
|
|
166
|
+
`neutral` (default) · `accent` · `success` · `warning` · `danger` · `info` — soft tile fill and icon color, as in Modal. Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root` anywhere inside, also from your own wrapper component — there is no tile and no tone: `data-media`, and the picture takes the tile's 40 px (avatar `l`, thumbnail `m`) whatever `size` it was given.
|
|
165
167
|
|
|
166
168
|
## States
|
|
167
169
|
| State | Driven by | DOM |
|
|
@@ -174,8 +176,11 @@ Sheet has no `variant`, `side` or `size`: it always rises from the bottom edge,
|
|
|
174
176
|
| busy | `Sheet.Guard busy` | Escape, the scrim, a swipe, the close button and `Sheet.Close` do nothing; the close button and the `Close` child are `disabled` in place; the handle stays, `data-swipe-disabled` on the panel (no grab cursor) |
|
|
175
177
|
| dirty | `Sheet.Guard dirty` | a dismiss opens the «Не сохранять изменения?» ConfirmDialog (`labels.discard*`); a swipe past the threshold glides back and asks the same |
|
|
176
178
|
| leaving the tab | `Sheet.Guard busy` or `dirty` | the browser asks before the tab closes or reloads (`beforeunload`), as with `Page.Guard` |
|
|
179
|
+
| title loading | `Sheet.Title loading` | a `Skeleton` holds the title line, `aria-busy` on the `<h2>`; the name fades in under the same element |
|
|
177
180
|
| description loading | `Sheet.Description loading` | a `Skeleton` holds the line, `aria-busy` on the `<p>`; the data fades in under the same element |
|
|
178
181
|
| actions below | the title would get less than 12rem beside `Sheet.Actions` | `data-actions-below` on the header: the actions move under the title and the description |
|
|
182
|
+
| picture mark | an `Avatar.Root` or a `Thumbnail.Root` inside `Sheet.Icon` | `data-media` on the icon, no tile; the picture's `data-size` is the slot's (`l` / `m`) |
|
|
183
|
+
| status in the line | a `Badge.Root` inside `Sheet.Description` | the line is a wrapping flex row, the badges first (`order`), each `data-tier="s"` |
|
|
179
184
|
|
|
180
185
|
Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`overlayMotion`); under `prefers-reduced-motion` it unmounts at once. A swipe down moves the sheet with the pointer; on release it closes past 30% of its height or on a quick flick and glides the rest of the way out, otherwise it glides back — both over `base` on the `emphasized` (iOS sheet) curve. A pull upward meets growing resistance and stops at 24; the sheet's fill continues that far below the screen edge. The scrim thins as the sheet goes down. The drag starts from the handle or the header, never from the body, so its content scrolls and its controls work as usual.
|
|
181
186
|
|
|
@@ -183,6 +188,16 @@ Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`
|
|
|
183
188
|
- A sheet is a short task that ends by itself — a pick, a tap on an action, a swipe down. Its zones follow the task, not a template:
|
|
184
189
|
- **No footer by default.** A footer only for a form with an explicit submit: «Отмена» (`Sheet.Close`, outline neutral) and the submit (solid, last; `type="submit" form={formId}`).
|
|
185
190
|
- **The header is optional.** Keep it when the content needs a title to be understood (a list of actions on «Заказ № 1042»). Drop it — and with it the × and the hairline — when the content speaks for itself (a calendar opened from a «Дата отгрузки» button): name the sheet with `aria-label` on `Sheet.Content`.
|
|
191
|
+
- The header has one anatomy, the same in every modal, drawer and sheet:
|
|
192
|
+
```
|
|
193
|
+
[mark] Title [icon buttons] [⋯] [×]
|
|
194
|
+
[status badges] description text
|
|
195
|
+
```
|
|
196
|
+
- mark — `Sheet.Icon`: a glyph on the tone tile, or the record's picture (`Avatar.Root`, `Thumbnail.Root`, also inside your own wrapper component) with no tile; both are 40 px.
|
|
197
|
+
- line 1 — `Sheet.Title`; `loading` holds it with a Skeleton while the name loads.
|
|
198
|
+
- line 2 — `Sheet.Description`: the record's statuses as `Badge.Root`s first, then the text. The kit sizes the badges (chip tier `s`, the line's 20 px), keeps them first and one gap apart, and wraps the text under them on a narrow panel.
|
|
199
|
+
- end — `Sheet.Actions`: only buttons — ghost icon buttons with a Tooltip and a ⋯ Dropdown — at the close button's tier, centred on the title's line; a status never goes here.
|
|
200
|
+
- A form's action apart from its submit pair: `Sheet.Actions` inside `Sheet.Footer` puts it at the start, at the footer's tier; stacked on a phone it comes first and the primary stays last.
|
|
186
201
|
- The content is the sheet's own, across its width: `Datepicker.Panel` or a column of actions straight in `Sheet.Body` — never a Card or a padded box inside it.
|
|
187
202
|
- Zone padding `--prime-sheet-padding`, width `--prime-sheet-width`. The bottom-sheet surface (`src/internal/sheet.module.css`, shared with a Modal and the floating layers on a phone) gives the height by content up to the top gap, the rounded top, the handle strip and the bottom safe-area inset below the content.
|
|
188
203
|
- Only the body scrolls; header and footer stay in place.
|
|
@@ -232,7 +247,7 @@ Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`
|
|
|
232
247
|
|---|---|
|
|
233
248
|
| [overview.tsx](examples/overview.tsx) | Actions with an order raised from the bottom edge: a header names the order, a column of actions follows, and each action closes the sheet — `Sheet.Trigger`, `Sheet.Header`, `Sheet.Close`. |
|
|
234
249
|
| [structure.tsx](examples/structure.tsx) | The content alone: a date sheet with no header and no footer, the calendar across its width, named by `aria-label`; a pick or a swipe down closes it — `aria-label`, `Sheet.Body`. |
|
|
235
|
-
| [header-actions.tsx](examples/header-actions.tsx) | The courier is at the door: an icon tile heads the sheet, the payment status
|
|
250
|
+
| [header-actions.tsx](examples/header-actions.tsx) | The courier is at the door: an icon tile heads the sheet, the payment status leads the line under the title, a ⋯ menu sits before the close button, and two equal buttons fill the footer — `Sheet.Icon`, `Sheet.Description`, `Sheet.Actions`, `layout`. |
|
|
236
251
|
| [guard.tsx](examples/guard.tsx) | A review form that holds the sheet: after an edit a swipe, the scrim, Escape and Cancel ask before discarding and the footer says so; sending locks every way out — `Sheet.Guard`, `Sheet.Meta`. |
|
|
237
252
|
| [dismiss.tsx](examples/dismiss.tsx) | A handover the courier must answer: no swipe, scrim or Escape closes the sheet, only its two buttons — `closeOnOutsideClick`, `closeOnEscape`. |
|
|
238
253
|
| [controlled-open.tsx](examples/controlled-open.tsx) | The parent owns the state and raises the sheet from a link in the text, without a trigger — `open`, `onOpenChange`. |
|
|
@@ -243,10 +258,14 @@ Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`
|
|
|
243
258
|
- «Отмена» / «Готово» buttons under a sheet that closes on a pick → drop the footer; the pick, the swipe, the scrim and Escape close it.
|
|
244
259
|
- A header with only a title over a calendar or a list that already says what it is → drop the header and name the sheet with `aria-label`.
|
|
245
260
|
- No header and no `aria-label` → the dialog has no name; give `Sheet.Content` an `aria-label`.
|
|
261
|
+
- A status `Badge` in the header's `Sheet.Actions`, beside the buttons → put it in `Sheet.Description`, before the text: the kit sizes it to the line and keeps it first.
|
|
262
|
+
- A Badge with its own `size` in `Sheet.Description` → leave `size` out: the line gives every header the same chip tier.
|
|
263
|
+
- A `Skeleton` placed by hand in `Sheet.Title` or `Sheet.Description` → their `loading`.
|
|
264
|
+
- A start-side button («Выйти», «Удалить») put inside `Sheet.Meta` → `Sheet.Actions` inside `Sheet.Footer`; `Meta` is status text only.
|
|
246
265
|
- A Card or a padded `div` around the content → put it straight into `Sheet.Body`, across its width.
|
|
247
266
|
- Record details or a long form in a sheet → use Drawer; a sheet is a short task.
|
|
248
267
|
- `closeOnOutsideClick={false}` and `closeOnEscape={false}` with no header × and no `Sheet.Close` → nothing closes it; keep a button that answers.
|
|
249
268
|
|
|
250
269
|
## Related
|
|
251
|
-
- **Built from:** the dialog shared with Modal and Drawer (`internal/overlay/DialogParts`), [Button](../button/COMPONENT.md) (close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Sheet.Body`),
|
|
270
|
+
- **Built from:** the dialog shared with Modal and Drawer (`internal/overlay/DialogParts`), [Button](../button/COMPONENT.md) (close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Sheet.Body`), `internal/mediaSlot` (a record's Avatar or Thumbnail in `Sheet.Icon`), [Skeleton](../skeleton/COMPONENT.md) (`Sheet.Description loading`), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (the `Sheet.Guard` discard confirm), `Icon` (the close glyph)
|
|
252
271
|
- **See also:** [Drawer](../drawer/COMPONENT.md) (the same parts as a side panel), [Modal](../modal/COMPONENT.md) (the same parts in the centre), [Datepicker](../datepicker/COMPONENT.md) (`Datepicker.Panel` in a sheet), [Dropdown](../dropdown/COMPONENT.md)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The courier is at the door: an icon tile heads the sheet, the payment status
|
|
1
|
+
/** The courier is at the door: an icon tile heads the sheet, the payment status leads the line under the title, a ⋯ menu sits before the close button, and two equal buttons fill the footer — `Sheet.Icon`, `Sheet.Description`, `Sheet.Actions`, `layout`. */
|
|
2
2
|
import { Badge, Button, ConfirmDialog, Dropdown, Icon, Sheet, Typography } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
@@ -24,9 +24,11 @@ export default function SheetHeaderActionsExample() {
|
|
|
24
24
|
<Icon name="object.truck" />
|
|
25
25
|
</Sheet.Icon>
|
|
26
26
|
<Sheet.Title>Курьер у двери</Sheet.Title>
|
|
27
|
-
<Sheet.Description
|
|
28
|
-
<Sheet.Actions>
|
|
27
|
+
<Sheet.Description>
|
|
29
28
|
<Badge.Root tone="success">Оплачен</Badge.Root>
|
|
29
|
+
Заказ № 1042 · 3 коробки
|
|
30
|
+
</Sheet.Description>
|
|
31
|
+
<Sheet.Actions>
|
|
30
32
|
<Dropdown.Root>
|
|
31
33
|
<Dropdown.Trigger>
|
|
32
34
|
<Button.Root variant="ghost" tone="neutral" aria-label="Действия с заказом">
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## When to use
|
|
9
9
|
- A region whose content has a known shape is loading: a list, a card, a profile, a form with saved values.
|
|
10
|
-
- The loading state inside [Crossfade](../crossfade/COMPONENT.md): the skeleton holds the data's geometry and
|
|
10
|
+
- The loading state inside [Crossfade](../crossfade/COMPONENT.md) (the state `"loading"`), rendered from the first paint: the skeleton holds the data's geometry and the data replaces it in place with a short fade — the height never glides.
|
|
11
11
|
- First load of a page section, when the structure is known before the values.
|
|
12
12
|
|
|
13
13
|
## When not to use
|
|
@@ -99,7 +99,8 @@ No `labels`.
|
|
|
99
99
|
## Mistakes
|
|
100
100
|
- A hand-drawn grey box with its own `@keyframes` → use `Skeleton`.
|
|
101
101
|
- A spinner in the middle of an empty card while its list loads → skeleton rows of that list.
|
|
102
|
-
- Skeletons of a different geometry than the data (one tall block for three rows) → the swap jumps; mirror the rows.
|
|
102
|
+
- Skeletons of a different geometry than the data (one tall block for three rows) → the swap jumps (it settles in one frame, nothing glides to hide it); mirror the rows.
|
|
103
|
+
- Rendering nothing until the data arrives, or revealing the data by unfolding or sliding it in → the skeleton is there from the first render and the data takes its place.
|
|
103
104
|
- Swapping a skeleton for data without `Crossfade` → wrap the region: `Crossfade state={loading ? "loading" : "ready"}`.
|
|
104
105
|
- Returning to a skeleton on every refresh of data already shown → keep the data, set `aria-busy`.
|
|
105
106
|
|
|
@@ -93,6 +93,7 @@ One entry of `fields`: data, not a part. Selected values missing from `options`
|
|
|
93
93
|
| `finite` | `boolean` | `true` | `options` fields: a fixed set — hiding every value is refused. `false` for an open set (services, users). |
|
|
94
94
|
| `multiple` | `boolean` | `true` | `options` fields: many values at once. `false` — one value and no hiding: a press picks only that value, a second press clears it (an API that takes one value per field). |
|
|
95
95
|
| `formatValue` | `(value: number) => string` | Russian digit groups | `range` fields: a bound in the chip («1 000 ₽»). |
|
|
96
|
+
| `calendar` | `Pick<DatepickerPanelProps, "months" \| "presets" \| "prompt" \| "footer" \| "withTime" \| "isDayDisabled" \| "disableFuture" \| "today" \| "locale" \| "weekStartsOn" \| "labels">` | — | `date-range` fields: the calendar of the period, as on a report's own Datepicker — `presets`, `months`, `prompt`, `footer`, `withTime`; the chip names a matching preset («Период: Этот месяц»). Without it the panel opens one month with no presets. |
|
|
96
97
|
|
|
97
98
|
### Functions
|
|
98
99
|
|
|
@@ -233,7 +234,7 @@ same height and type.
|
|
|
233
234
|
| [overview.tsx](examples/overview.tsx) | Filters above a request list: the filter button and the search, applied filters as tags, rows narrowed by the value and the text — `fields`, `value`, `search`, `matchesSmartFilter`. |
|
|
234
235
|
| [sizes.tsx](examples/sizes.tsx) | Every size tier: the filter button, the search, the tags and the panel follow one tier — `size`. |
|
|
235
236
|
| [many-values.tsx](examples/many-values.tsx) | An open set of twenty services, degraded ones marked with an icon, collapsed after eight; typing narrows the panel and marks the match in accent — `finite`, `icon`, `collapsedLimit`. |
|
|
236
|
-
| [ranges.tsx](examples/ranges.tsx) | Deals narrowed by an amount from–to and a period from
|
|
237
|
+
| [ranges.tsx](examples/ranges.tsx) | Deals narrowed by an amount from–to and a period from a calendar with presets, two months and an Apply footer; each range is one readable chip, and the bounds go to the server as from / to parameters — `kind`, `formatValue`, `calendar`, `matchesSmartFilter`, `isSmartFilterRange`. |
|
|
237
238
|
| [single-value.tsx](examples/single-value.tsx) | For an API that takes one value per field: the filter button without a search, a press picks only that value and nothing can be hidden — `searchable`, `multiple`. |
|
|
238
239
|
| [option-groups.tsx](examples/option-groups.tsx) | Values of different kinds in one field: owner types and people under their own headings — `options`, `group`. |
|
|
239
240
|
| [controlled.tsx](examples/controlled.tsx) | A saved view sets the value from outside; the selection is read back as the list to ask the server for — `value`, `onValueChange`, `resolveSmartFilterValues`. |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
/** Deals narrowed by an amount from–to and a period from
|
|
1
|
+
/** Deals narrowed by an amount from–to and a period from a calendar with presets, two months and an Apply footer; each range is one readable chip, and the bounds go to the server as from / to parameters — `kind`, `formatValue`, `calendar`, `matchesSmartFilter`, `isSmartFilterRange`. */
|
|
2
2
|
import {
|
|
3
3
|
Card,
|
|
4
|
+
DEFAULT_DATEPICKER_PRESETS,
|
|
4
5
|
formatDateValue,
|
|
5
6
|
isSmartFilterRange,
|
|
6
7
|
matchesSmartFilter,
|
|
@@ -17,7 +18,12 @@ const formatRubles = (value: number) => `${value.toLocaleString("ru-RU")} ₽`;
|
|
|
17
18
|
|
|
18
19
|
const FIELDS: SmartFilterField[] = [
|
|
19
20
|
{ key: "amount", label: "Сумма", kind: "range", formatValue: formatRubles },
|
|
20
|
-
{
|
|
21
|
+
{
|
|
22
|
+
key: "closed",
|
|
23
|
+
label: "Период",
|
|
24
|
+
kind: "date-range",
|
|
25
|
+
calendar: { presets: DEFAULT_DATEPICKER_PRESETS, months: 2, prompt: true, footer: true },
|
|
26
|
+
},
|
|
21
27
|
{
|
|
22
28
|
key: "stage",
|
|
23
29
|
label: "Этап",
|
|
@@ -68,7 +68,7 @@ Spinner <span role="status">; size and tone
|
|
|
68
68
|
## States
|
|
69
69
|
| State | Driven by | DOM |
|
|
70
70
|
|---|---|---|
|
|
71
|
-
| appearing | mounted | fades in over `fast` (`internal/enterMotion`), so a spinner that comes and goes never blinks |
|
|
71
|
+
| appearing | mounted into a host already shown | fades in over `fast` (`internal/enterMotion`), so a spinner that comes and goes never blinks; one that mounts with its page stands still |
|
|
72
72
|
| spinning | mounted | the ring turns at a constant speed (`--prime-motion-loop × 3` per turn, linear) |
|
|
73
73
|
| reduced motion | `prefers-reduced-motion` | the ring keeps turning, twice as slow (`--prime-motion-loop × 6` per turn) and linear: a loading cue is information, so the token is not zeroed |
|
|
74
74
|
| size / tone | `size`, `tone` | `data-size` (the explicit size, else the host control tier, else `m`), `data-tone` (only for a non-default tone) |
|
|
@@ -35,7 +35,7 @@ Tabs.Root value, size (else the host tier), orientation, tone, f
|
|
|
35
35
|
│ │ ├─ Tabs.Count counter Badge, one tier below the tabs size
|
|
36
36
|
│ │ └─ Tabs.Description muted second line; makes the tab two-line
|
|
37
37
|
│ └─ Tabs.Separator Divider hairline between groups of tabs
|
|
38
|
-
└─ Tabs.Panel role="tabpanel", rendered only while its tab is active; no fill, no padding
|
|
38
|
+
└─ Tabs.Panel role="tabpanel", rendered only while its tab is active (keepMounted: kept hidden once opened); no fill, no padding
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## API
|
|
@@ -120,11 +120,12 @@ Tabs.Root value, size (else the host tier), orientation, tone, f
|
|
|
120
120
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other div attributes. |
|
|
121
121
|
|
|
122
122
|
### Tabs.Panel
|
|
123
|
-
`ref` → `HTMLDivElement`. `<div role="tabpanel">`, focusable, rendered only while its tab is active. No fill and no padding: its content is the host's content, aligned with the strip's edges.
|
|
123
|
+
`ref` → `HTMLDivElement`. `<div role="tabpanel">`, focusable, rendered only while its tab is active (with `keepMounted`, kept hidden once opened). No fill and no padding: its content is the host's content, aligned with the strip's edges. Content opened after mount enters.
|
|
124
124
|
|
|
125
125
|
| Prop | Type | Default | Description |
|
|
126
126
|
|---|---|---|---|
|
|
127
127
|
| `value` | `string` | — (required) | Tab value this panel belongs to. |
|
|
128
|
+
| `keepMounted` | `boolean` | `false` | Once opened, the panel stays mounted and `hidden` while another tab is active: its data, form draft and scroll survive a switch. A panel never opened is not mounted. |
|
|
128
129
|
| `children` | `ReactNode` | — | Panel content. |
|
|
129
130
|
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className` and the other div attributes. |
|
|
130
131
|
|
|
@@ -187,13 +188,14 @@ Avoid icons or descriptions on only some tabs (uneven rows; without an icon on e
|
|
|
187
188
|
| disabled | `disabled` on Item | native `disabled`, `data-disabled="true"`, `text-disabled`; a removable tab loses its close button |
|
|
188
189
|
| removable | `onRemove` on Item | item wrapper `data-removable="true"`, close button, tab `aria-keyshortcuts="Delete"` |
|
|
189
190
|
| closing, confirm pending | `onRemove` returned a promise that has not settled | the close button is `loading` (`aria-busy`); the tab, its selection and focus stay; resolved → the neighbour takes over, rejected → nothing changes |
|
|
191
|
+
| kept | `keepMounted` on Panel, its tab once opened and now inactive | the panel stays mounted with `hidden` (out of the layout, the tab order and the accessibility tree); its tab keeps `aria-controls`; shown again, its content enters as on a first open |
|
|
190
192
|
| two-line | `Tabs.Description` child | `data-two-line="true"` |
|
|
191
193
|
| collapsed | tabs wider than the list, or a label that would be cut (horizontal) | list `data-collapse="full" \| "compact" \| "icon"`: `compact` hides icons and descriptions; `icon` takes labels, counts and close buttons out of the row — a `VisuallyHidden` copy of the label names the tab and a `Tooltip` shows it — only when every tab has an icon |
|
|
192
194
|
| overflow | tabs at their narrowest (content width, `minItemWidth` or icon squares) still wider than the list | `data-overflow-start` / `data-overflow-end` on the list (the switcher row, a ScrollContainer), faded edges as deep as the hidden part at most (they grow and melt with the scroll, never switch); when the end edge would cut only a tab's empty side, the tabs at `minItemWidth` widen a little (`--tabs-peek`, up to 30%) so a label fades out at the edge and shows that more tabs follow; the active tab is kept in view and clear of the fades on mount and on every value change, the first and the last tab scroll all the way to the end — only the list scrolls, never the page. A column (a wide vertical list) has plain edges |
|
|
193
195
|
|
|
194
196
|
The indicator glides (`emphasized` + `base`, `data-animate="true"`) only into a choice by click, arrow keys or closing the active tab; on mount, a value set from outside, a resize, font loading, tabs added or removed and collapse it is placed without motion, and it stays still under `prefers-reduced-motion`. The list scrolls smoothly to the active tab only after such a choice too.
|
|
195
197
|
|
|
196
|
-
Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-width`, inline `--tabs-item-min` / `--tabs-item-max` when set; the folder (the list's first child, `data-indicator`) `data-axis="x" | "y"` (follows the list's direction, so a vertical list in a narrow container gets the top folder); Item `data-value` on the tab, `data-state` / `data-disabled` on its wrapper too; Label `data-text`. Inactive panels are unmounted, and a tab points at its panel (`aria-controls`) only while the panel is in the document; a panel's content enters each time it opens.
|
|
198
|
+
Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-width`, inline `--tabs-item-min` / `--tabs-item-max` when set; the folder (the list's first child, `data-indicator`) `data-axis="x" | "y"` (follows the list's direction, so a vertical list in a narrow container gets the top folder); Item `data-value` on the tab, `data-state` / `data-disabled` on its wrapper too; Label `data-text`. Inactive panels are unmounted (unless `keepMounted`), and a tab points at its panel (`aria-controls`) only while the panel is in the document; a panel's content enters each time it opens after mount (the panel shown on mount stands still).
|
|
197
199
|
|
|
198
200
|
## Layout & spacing
|
|
199
201
|
- Tabs are a bar, not a surface: the root has no fill, padding, radius or `data-depth`. On a page or in a `Card.Body`, put it straight into the host's padded region and the panel content straight into `Tabs.Panel`. The strip's edges and the panel content share the host's content edge; the panel stands one tier gap below the strip (`--prime-space-3` … `--prime-space-8`).
|
|
@@ -261,7 +263,7 @@ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-
|
|
|
261
263
|
| [overflow.tsx](examples/overflow.tsx) | Drag the frame narrower: icons go first, then labels, and icons with tooltips stay; then the list scrolls — `Tabs.Icon`. |
|
|
262
264
|
| [closable.tsx](examples/closable.tsx) | Open order cards as browser tabs: each closes by its button, Delete or a middle click; tabs stay between two widths, and the list scrolls when they do not fit — `onRemove`, `minItemWidth`, `maxItemWidth`. |
|
|
263
265
|
| [two-line.tsx](examples/two-line.tsx) | A label and a counter on the first line, a summary on the second — `Tabs.Count`, `Tabs.Description`. |
|
|
264
|
-
| [in-drawer.tsx](examples/in-drawer.tsx) | Order details in a Drawer: the tab bar runs along the bottom of the header, the active tab is the body rising into it,
|
|
266
|
+
| [in-drawer.tsx](examples/in-drawer.tsx) | Order details in a Drawer: the tab bar runs along the bottom of the header and scrolls sideways when the tabs outgrow the drawer, the active tab is the body rising into it, every panel is the body's own content, and the delivery form keeps its edits across tabs — `Tabs.Root`, `Tabs.List`, `Tabs.Panel`, `keepMounted`. |
|
|
265
267
|
| [controlled.tsx](examples/controlled.tsx) | The active tab lives in parent state, e.g. synced with the URL — `value`, `onValueChange`. |
|
|
266
268
|
|
|
267
269
|
## Mistakes
|
|
@@ -274,6 +276,7 @@ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-
|
|
|
274
276
|
- A hand-made × inside `Tabs.Item` → a button inside a tab button; pass `onRemove` instead.
|
|
275
277
|
- `onRemove` that does not drop the tab from your list → the tab stays; the kit only moves the selection.
|
|
276
278
|
- `Tabs.List` without `aria-label` → add one.
|
|
279
|
+
- Lifting a panel's data or form draft to the parent only so it survives a tab switch → `keepMounted` on that `Tabs.Panel`.
|
|
277
280
|
- Tabs inside `Drawer.Body` / `Modal.Body` under the header → a floating strip under the header's hairline; wrap the header and the body in `Tabs.Root` and put `Tabs.List` into the header.
|
|
278
281
|
|
|
279
282
|
## Related
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Order details in a Drawer: the tab bar runs along the bottom of the header, the active tab is the body rising into it,
|
|
1
|
+
/** Order details in a Drawer: the tab bar runs along the bottom of the header and scrolls sideways when the tabs outgrow the drawer, the active tab is the body rising into it, every panel is the body's own content, and the delivery form keeps its edits across tabs — `Tabs.Root`, `Tabs.List`, `Tabs.Panel`, `keepMounted`. */
|
|
2
2
|
import { Badge, Button, DescriptionList, Drawer, Fieldset, Input, Tabs } from "prime-ui-kit";
|
|
3
3
|
|
|
4
4
|
const ITEMS = [
|
|
@@ -7,6 +7,11 @@ const ITEMS = [
|
|
|
7
7
|
{ name: "Сборка и подъём на этаж", amount: "1 × 3 200 ₽" },
|
|
8
8
|
];
|
|
9
9
|
|
|
10
|
+
const DOCUMENTS = [
|
|
11
|
+
{ name: "Счёт № 1042-С", date: "3 октября" },
|
|
12
|
+
{ name: "Накладная № 1042-Н", date: "4 октября" },
|
|
13
|
+
];
|
|
14
|
+
|
|
10
15
|
const HISTORY = [
|
|
11
16
|
{ time: "3 октября, 10:14", event: "Заказ оформлен" },
|
|
12
17
|
{ time: "3 октября, 10:20", event: "Оплата прошла" },
|
|
@@ -33,6 +38,7 @@ export default function TabsInDrawerExample() {
|
|
|
33
38
|
<Tabs.Count>{ITEMS.length}</Tabs.Count>
|
|
34
39
|
</Tabs.Item>
|
|
35
40
|
<Tabs.Item value="delivery">Доставка</Tabs.Item>
|
|
41
|
+
<Tabs.Item value="documents">Документы</Tabs.Item>
|
|
36
42
|
<Tabs.Item value="history">История</Tabs.Item>
|
|
37
43
|
</Tabs.List>
|
|
38
44
|
</Drawer.Header>
|
|
@@ -58,7 +64,7 @@ export default function TabsInDrawerExample() {
|
|
|
58
64
|
</DescriptionList.Root>
|
|
59
65
|
</Tabs.Panel>
|
|
60
66
|
|
|
61
|
-
<Tabs.Panel value="delivery">
|
|
67
|
+
<Tabs.Panel value="delivery" keepMounted>
|
|
62
68
|
<Fieldset aria-label="Доставка">
|
|
63
69
|
<Input.Root label="Адрес">
|
|
64
70
|
<Input.Wrapper>
|
|
@@ -73,6 +79,16 @@ export default function TabsInDrawerExample() {
|
|
|
73
79
|
</Fieldset>
|
|
74
80
|
</Tabs.Panel>
|
|
75
81
|
|
|
82
|
+
<Tabs.Panel value="documents">
|
|
83
|
+
<DescriptionList.Root>
|
|
84
|
+
{DOCUMENTS.map((document) => (
|
|
85
|
+
<DescriptionList.Item key={document.name} label={document.name}>
|
|
86
|
+
{document.date}
|
|
87
|
+
</DescriptionList.Item>
|
|
88
|
+
))}
|
|
89
|
+
</DescriptionList.Root>
|
|
90
|
+
</Tabs.Panel>
|
|
91
|
+
|
|
76
92
|
<Tabs.Panel value="history">
|
|
77
93
|
<DescriptionList.Root>
|
|
78
94
|
{HISTORY.map((entry) => (
|
|
@@ -142,6 +142,7 @@ Thumbnail.Root frame (a <div>; the child with asChild; a <button role="
|
|
|
142
142
|
- In a table the row grows to the thumbnail; never shrink the thumbnail to the row.
|
|
143
143
|
- Card covers: `fullWidth` inside the card body, the body's gap to the title.
|
|
144
144
|
- No outline by default: the fill separates the preview.
|
|
145
|
+
- In a leading media slot (`Drawer.Icon`, `Modal.Icon`, `Sheet.Icon`, `Card.Icon`) the thumbnail takes the slot's size (`m`, 40px) whatever `size` it was given, also through a wrapper component of yours; the slot then draws no tile.
|
|
145
146
|
|
|
146
147
|
## Accessibility
|
|
147
148
|
|
|
@@ -110,7 +110,7 @@ A chip of tier T is as tall as a menu item of tier T; text, padding and radius s
|
|
|
110
110
|
## States
|
|
111
111
|
| State | Driven by | DOM |
|
|
112
112
|
|---|---|---|
|
|
113
|
-
| open / closed | hover (mouse, pen) or focus after `delayDuration
|
|
113
|
+
| open / closed | hover (mouse, pen) or keyboard focus after `delayDuration` (not the focus a click causes or a dialog moves in after a click); `open` / `defaultOpen` / `onOpenChange` | `data-state="open" \| "closed"` on the chip (closed during the exit) and on the trigger |
|
|
114
114
|
| closing | blur, a press on the trigger, Escape, pointer leaving (unless it moves onto the chip) | — |
|
|
115
115
|
| warm group | another tooltip of the Provider was just shown | `data-instant="true"`: opens and leaves without motion |
|
|
116
116
|
| side | `side` and the room around the trigger | `data-side` (resolved), `data-align` |
|
|
@@ -128,7 +128,7 @@ A chip of tier T is as tall as a menu item of tier T; text, padding and radius s
|
|
|
128
128
|
|
|
129
129
|
| Key | Action |
|
|
130
130
|
|---|---|
|
|
131
|
-
| `Tab` |
|
|
131
|
+
| `Tab` | Keyboard focus on the trigger opens the tooltip after the delay; focus a click causes, or a layer moves in after a click, does not. |
|
|
132
132
|
| `Escape` | Hides the tooltip; focus stays on the trigger. |
|
|
133
133
|
|
|
134
134
|
### ARIA
|
|
@@ -137,7 +137,7 @@ A chip of tier T is as tall as a menu item of tier T; text, padding and radius s
|
|
|
137
137
|
- The chip is `role="tooltip"`; while open the trigger points at it with `aria-describedby` (appended to the child's own).
|
|
138
138
|
- `Tooltip.Trigger` composes the child's `ref`, sets `data-state` and chains the child's `onPointerEnter`, `onPointerLeave`, `onPointerDown`, `onFocus` and `onBlur`.
|
|
139
139
|
- The tooltip is a description, not the name: an icon-only button needs its own `aria-label`.
|
|
140
|
-
- The pointer may move onto the chip without closing it (WCAG 1.4.13); a touch does not open it, focus does.
|
|
140
|
+
- The pointer may move onto the chip without closing it (WCAG 1.4.13); a touch does not open it, keyboard focus does.
|
|
141
141
|
- Tooltip has no `closeOnEscape` / `closeOnOutsideClick` (foundation §8): it closes on pointer-leave, blur, a press on the trigger and Escape, and Escape cannot be turned off — WCAG 1.4.13 requires the chip to be dismissable without moving the pointer or focus.
|
|
142
142
|
- A `disabled` control (its own prop or a disabled `Fieldset`) is wrapped by the kit in a focusable `<span>`: the host takes hover and Tab, the focus ring is drawn on the control. Around a button the host is that button for assistive tech — `role="button"`, `aria-disabled`, named by the control through `aria-labelledby` — and the control itself leaves the tree.
|
|
143
143
|
- A trigger that is no tab stop (Badge, Avatar, ProgressCircle, `Icon` with `label`) gets `tabIndex={0}` and the focus ring; its name comes from `aria-label` or the icon `label`.
|
|
@@ -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
|
|---|---|---|---|
|
|
@@ -292,6 +293,7 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
|
|
|
292
293
|
- The edge air comes from the host: `AppShell.Main` sets `--page-gutter` (the page gutter, 16 → 24 → 32) and `--page-top` (24 → 32 from 1024px); `Page.Root` pads itself with them. Outside a host the page brings no air. The page resets both for what it holds, so a page nested in it starts clean.
|
|
293
294
|
- `Page.TopBar` steps out of the gutter to the panel's edges and keeps the gutter inside; padding 12 above (or the top safe-area inset) and below; what follows starts 24 under its controls (header → body; the bar's own 12 counts in). The first line is as high as the Sidebar brand row (control m + 8), so the two read as one line. Groups 16 apart on a line, lines 12 apart; tools 8 apart; buttons 8 apart.
|
|
294
295
|
- `Page.Tools` and `Page.Chips` are the kit's one tool row and chips line (`ToolRow`), the same as the DataTable `toolbar` and `toolbarChips`: the bar only places them. On the first line every tool keeps its natural width; on a line of their own they take the line and lay out from its width (container `prime-tool-row`), never from the window.
|
|
296
|
+
- The tool row lays out the elements it is given, each in a cell: pass the tools as direct children of `Page.Tools` or a fragment. A wrapper component that returns several controls is one cell — no gaps between them, no growing search.
|
|
295
297
|
- In the bar only the heading's column gives (`minmax(0, 1fr)`): the tools and the actions keep their width. The heading keeps its natural width until every button step is taken; then a heading with a path stacks (the path over the title, the title and the description wrapping), so no level is cut to a fragment; a heading without a path truncates its title.
|
|
296
298
|
- The plan is measured, never chosen by breakpoints: the bar lays itself out from its own width.
|
|
297
299
|
- Header (flow) → Body 24; blocks inside Body 24; a `Page.Section` 40 from its neighbours (the body's 24 + 16); tile → tile in a Grid 16 on both axes; section heading → content 16, heading → description 4; flow Title → Description 8; heading → Actions 24 side by side, 16 once they wrap below.
|
|
@@ -341,7 +343,8 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
|
|
|
341
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`. |
|
|
342
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`. |
|
|
343
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`. |
|
|
344
|
-
| [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`. |
|
|
345
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`. |
|
|
346
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`. |
|
|
347
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`. |
|
|
@@ -360,7 +363,8 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
|
|
|
360
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.
|
|
361
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.
|
|
362
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.
|
|
363
|
-
- 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.
|
|
364
368
|
- `Page.Header` inside `Page.TopBar` → the bar takes `Page.Heading` and `Page.Actions` directly; `Page.Header` is the flow's document header.
|
|
365
369
|
- A hand-made menu button for the phone Sidebar → the bar draws it while the Sidebar is off-canvas.
|
|
366
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
|
+
}
|