prime-ui-kit 0.20.1 → 0.21.0
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/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 +19 -6
- package/dist/skill/choosing.md +79 -6
- package/dist/skill/components.md +1 -1
- package/dist/skill/composition.md +211 -51
- package/dist/skill/foundations.md +17 -6
- package/dist/skill/layouts.md +64 -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 +1 -0
- package/dist/skill/reference/layout/sidebar/COMPONENT.md +4 -2
- 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 +1 -0
- package/src/layout/sidebar/COMPONENT.md +4 -2
- 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
|
@@ -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`.
|
|
@@ -51,11 +51,14 @@ The design contract for every component. Tokens live in `tokens/` and are genera
|
|
|
51
51
|
perfectly in a row (Button, Input, Select, Datepicker trigger, SegmentedControl, Tabs).
|
|
52
52
|
5. **Nested radius = outer radius − padding.** Panel 12 with padding 4 → items 8.
|
|
53
53
|
6. **Air is hierarchy.** Important content gets more space; secondary content is grouped tighter.
|
|
54
|
-
7. **State changes are continuous.** Nothing on screen flips from one state to
|
|
55
|
-
into their new state (§7 Motion, rule 8)
|
|
54
|
+
7. **State changes are continuous; loading never unfolds.** Nothing on screen flips from one state to
|
|
55
|
+
another. Controls move into their new state (§7 Motion, rule 8). Whatever loads is a `Skeleton` in
|
|
56
|
+
the data's geometry from the first paint — never nothing, never a region that grows from zero —
|
|
57
|
+
and the data then replaces it in place: no height glide, no reveal, no slide, at most a short
|
|
58
|
+
cross-fade, the layout settled in one frame. A region swapping between settled states — data →
|
|
56
59
|
empty → error, one record → another — cross-fades and glides to its new height (`Crossfade`). A
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
region that mounts in its state shows it still. This holds for every region of every screen, not
|
|
61
|
+
only for prominent ones.
|
|
59
62
|
|
|
60
63
|
## 2. Token layers and naming
|
|
61
64
|
|
|
@@ -339,8 +342,8 @@ Every component is designed with its micro-animations, not only its static state
|
|
|
339
342
|
`padding`, `top/left`; never `transition: all` — list the properties. Where size must animate, prefer
|
|
340
343
|
`transform` / `scale` (progress fills). The exceptions are a closed list, each with one engine,
|
|
341
344
|
and `src/styles/motion-perf.test.ts` allows nothing else:
|
|
342
|
-
- **height glide** — a region that swaps its content glides to the new
|
|
343
|
-
Notification stack);
|
|
345
|
+
- **height glide** — a region that swaps its content between settled states glides to the new
|
|
346
|
+
height (`Crossfade`, the Notification stack); a swap into or out of loading never glides;
|
|
344
347
|
- **row fold** — a part that folds open or closed animates `grid-template-rows` 0fr ↔ 1fr
|
|
345
348
|
(`Disclosure`: Accordion, DataTable expanded rows);
|
|
346
349
|
- **text width** — a label that morphs into another text glides its `width` (`MorphText`).
|
|
@@ -353,8 +356,11 @@ Every component is designed with its micro-animations, not only its static state
|
|
|
353
356
|
8. **State moves, it does not swap.** Checkmark draws/scales in, switch thumb slides, selection indicator
|
|
354
357
|
(tabs, segmented control) glides to the new item, chevrons rotate, progress fills, counters and fills
|
|
355
358
|
ease. Two states cross-fade rather than flip when they replace each other in place: a region
|
|
356
|
-
swapping its content uses `Crossfade` (inside a component: `RegionSwap`)
|
|
357
|
-
|
|
359
|
+
swapping its content uses `Crossfade` (inside a component: `RegionSwap`). Its loading state is
|
|
360
|
+
the state `"loading"`: a `Skeleton` of the coming content, drawn from the first paint, which the
|
|
361
|
+
data replaces in place — the height settles in one frame and the fade takes `fast`; only a swap
|
|
362
|
+
between settled states fades on `base` and glides its height (principle 7). Nothing moves on the
|
|
363
|
+
first render of a region.
|
|
358
364
|
9. **Interruptible.** Toggled state uses CSS transitions (retarget mid-flight), not keyframes. Keyframes only
|
|
359
365
|
for one-shot enter/exit (overlay presence, spinner, skeleton) and one-shot gestures (icons, rule 13;
|
|
360
366
|
a field's error shake, rule 14).
|
|
@@ -394,7 +400,7 @@ should stay still, and say so in a CSS comment only when the omission is non-obv
|
|
|
394
400
|
| selected | `accent-soft` bg + `accent-text`, or check icon in `accent-text` |
|
|
395
401
|
| disabled | `fill-muted`/`field-bg-disabled` bg, `text-disabled` color, no shadow, `cursor: not-allowed` |
|
|
396
402
|
| loading (control) | spinner replaces leading icon, label stays, width does not change, `aria-busy` |
|
|
397
|
-
| loading (region) | `Skeleton` in the data's geometry
|
|
403
|
+
| loading (region) | `Skeleton` in the data's geometry from the first paint, as the `"loading"` state of `Crossfade` (the data replaces it in place, no height glide), `aria-busy` on the region; data already shown stays during a refresh |
|
|
398
404
|
| error | `danger-border` inset ring + `danger-text` message; layout does not shift (support row reserves height when the component asks for it) |
|
|
399
405
|
| empty | centered muted text in `body-s`, optional action; reached through `Crossfade` like every region state |
|
|
400
406
|
|
|
@@ -292,6 +292,7 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
|
|
|
292
292
|
- 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
293
|
- `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
294
|
- `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.
|
|
295
|
+
- 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
296
|
- 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
297
|
- The plan is measured, never chosen by breakpoints: the bar lays itself out from its own width.
|
|
297
298
|
- 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.
|
|
@@ -70,7 +70,7 @@ Sidebar.Root rail wrapper; renders <nav> inside; size tier
|
|
|
70
70
|
│ ├─ Sidebar.SubTrigger disclosure row, chevron at the end
|
|
71
71
|
│ └─ Sidebar.SubContent child Items on a guide line (a flyout on the compact rail)
|
|
72
72
|
└─ Sidebar.Footer bottom items, the item Toggle, the account
|
|
73
|
-
└─ Sidebar.Account avatar + name + muted line; a Dropdown.Trigger child
|
|
73
|
+
└─ Sidebar.Account avatar + name + muted line; a ↕ chevron as a Dropdown.Trigger child
|
|
74
74
|
useSidebar() state hook for custom parts inside Root (useAppShellNav outside it, in a shell)
|
|
75
75
|
```
|
|
76
76
|
|
|
@@ -220,7 +220,7 @@ useSidebar() state hook for custom parts inside Root (useA
|
|
|
220
220
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "role">` | — | `children` (Items), `className` (on the list) and the other div attributes. |
|
|
221
221
|
|
|
222
222
|
### Sidebar.Account
|
|
223
|
-
`ref` → `HTMLButtonElement`. The signed-in person at the bottom of `Sidebar.Footer`: avatar, name, a muted line
|
|
223
|
+
`ref` → `HTMLButtonElement`. The signed-in person at the bottom of `Sidebar.Footer`: avatar, name, a muted line; a ↕ chevron only as a `Dropdown.Trigger` child. In compact mode only the avatar stays, with the name as a tooltip.
|
|
224
224
|
|
|
225
225
|
| Prop | Type | Default | Description |
|
|
226
226
|
|---|---|---|---|
|
|
@@ -311,6 +311,7 @@ Put `<Sidebar.Toggle variant="header" />` next to `Sidebar.Brand` in the header,
|
|
|
311
311
|
| flyout | hover / click / keyboard on a SubTrigger, or on the `…` row of a folded group, in compact mode | the row `aria-haspopup="dialog"`, `aria-expanded`; a Popover `role="dialog"` |
|
|
312
312
|
| attention dot | a coloured `Sidebar.ItemCount` inside a folded Sub (always on the rail) or a folded Group | `Badge.Dot` with `data-attention="shown" \| "hidden"` on the parent row / heading / `…` row |
|
|
313
313
|
| persisted | `persistKey` on Root | the mode and the open state of collapsible groups and sub-lists are read from and written to `localStorage` |
|
|
314
|
+
| account menu | `Sidebar.Account` inside a `Dropdown.Trigger` (`aria-haspopup="menu"`) | the ↕ chevron at the end of the row; without a menu (a plain `onClick`, a `Drawer.Trigger`) no chevron |
|
|
314
315
|
| disabled | `disabled` | Item `disabled` / `aria-disabled`, `data-disabled` |
|
|
315
316
|
|
|
316
317
|
`persistKey` stores collapsible groups and sub-lists by their `id`, else by their label text — give an `id` when labels are translated or change. The stored state wins over `defaultMode` / `defaultOpen`; controlled `mode` / `open` are not stored, and unavailable storage is ignored. A hidden rail is stored as the mode it left, so a reload never strands a person on a hidden rail.
|
|
@@ -388,6 +389,7 @@ Leaving the narrow viewport closes the off-canvas panel; activating an item (a l
|
|
|
388
389
|
| [collapsible-groups.tsx](examples/collapsible-groups.tsx) | Group headings that fold their items away, one group open at a time, with a chevron at the end of the heading; on the compact rail a folded group becomes one row whose items open in a flyout — `collapsible`, `open`, `onOpenChange`. |
|
|
389
390
|
| [nested-items.tsx](examples/nested-items.tsx) | A parent item with child items on a guide line: a current child opens it and marks the parent; on the compact rail the children open in a flyout — `Sidebar.Sub`, `Sidebar.SubTrigger`, `Sidebar.SubContent`. |
|
|
390
391
|
| [account.tsx](examples/account.tsx) | Footer items and the collapse row above the signed-in person: avatar, name and email open the account menu; collapse the rail from that row and only the avatar stays, on the icon axis — `Sidebar.Footer`, `Sidebar.Account`, `description`, `Sidebar.Toggle`. |
|
|
392
|
+
| [account-profile.tsx](examples/account-profile.tsx) | The signed-in person opens a profile panel, not a menu, so the row draws no chevron — `Sidebar.Account`, `description`. |
|
|
391
393
|
| [router.tsx](examples/router.tsx) | Router links as the brand and the items: the router marks the current link and the item shows it as current; render it inside a router — `asChild`. |
|
|
392
394
|
| [persist.tsx](examples/persist.tsx) | The first visit opens on the icon rail; the mode and the folded groups are remembered and come back after a reload — `persistKey`, `defaultMode`. |
|
|
393
395
|
| [custom-part.tsx](examples/custom-part.tsx) | A part of your own in the footer reads the rail state: the storage meter shows on the full rail and in the off-canvas panel and steps aside on the icon rail — `useSidebar`. |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** The signed-in person opens a profile panel, not a menu, so the row draws no chevron — `Sidebar.Account`, `description`. */
|
|
2
|
+
import { Avatar, Button, DescriptionList, Drawer, Icon, Sidebar } from "prime-ui-kit";
|
|
3
|
+
|
|
4
|
+
import styles from "./examples.module.css";
|
|
5
|
+
|
|
6
|
+
const PROFILE = [
|
|
7
|
+
{ term: "Почта", value: "anna@company.ru" },
|
|
8
|
+
{ term: "Роль", value: "Руководитель отдела продаж" },
|
|
9
|
+
{ term: "Часовой пояс", value: "Москва, UTC+3" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export default function SidebarAccountProfileExample() {
|
|
13
|
+
return (
|
|
14
|
+
<div className={styles.stage}>
|
|
15
|
+
<Sidebar.Root offCanvas="never">
|
|
16
|
+
<Sidebar.Content>
|
|
17
|
+
<Sidebar.Item current>
|
|
18
|
+
<Sidebar.ItemIcon>
|
|
19
|
+
<Icon name="nav.home" />
|
|
20
|
+
</Sidebar.ItemIcon>
|
|
21
|
+
Обзор
|
|
22
|
+
</Sidebar.Item>
|
|
23
|
+
<Sidebar.Item>
|
|
24
|
+
<Sidebar.ItemIcon>
|
|
25
|
+
<Icon name="object.document" />
|
|
26
|
+
</Sidebar.ItemIcon>
|
|
27
|
+
Сделки
|
|
28
|
+
</Sidebar.Item>
|
|
29
|
+
</Sidebar.Content>
|
|
30
|
+
<Sidebar.Footer>
|
|
31
|
+
<Drawer.Root>
|
|
32
|
+
<Drawer.Trigger>
|
|
33
|
+
<Sidebar.Account description="anna@company.ru">
|
|
34
|
+
<Avatar.Root color="purple">
|
|
35
|
+
<Avatar.Fallback>АС</Avatar.Fallback>
|
|
36
|
+
</Avatar.Root>
|
|
37
|
+
Анна Смирнова
|
|
38
|
+
</Sidebar.Account>
|
|
39
|
+
</Drawer.Trigger>
|
|
40
|
+
<Drawer.Content size="s">
|
|
41
|
+
<Drawer.Header>
|
|
42
|
+
<Drawer.Title>Анна Смирнова</Drawer.Title>
|
|
43
|
+
<Drawer.Description>Профиль</Drawer.Description>
|
|
44
|
+
</Drawer.Header>
|
|
45
|
+
<Drawer.Body>
|
|
46
|
+
<DescriptionList.Root>
|
|
47
|
+
{PROFILE.map(({ term, value }) => (
|
|
48
|
+
<DescriptionList.Item key={term} label={term}>
|
|
49
|
+
{value}
|
|
50
|
+
</DescriptionList.Item>
|
|
51
|
+
))}
|
|
52
|
+
</DescriptionList.Root>
|
|
53
|
+
</Drawer.Body>
|
|
54
|
+
<Drawer.Footer>
|
|
55
|
+
<Button.Root variant="outline" tone="neutral">
|
|
56
|
+
Выйти
|
|
57
|
+
</Button.Root>
|
|
58
|
+
</Drawer.Footer>
|
|
59
|
+
</Drawer.Content>
|
|
60
|
+
</Drawer.Root>
|
|
61
|
+
</Sidebar.Footer>
|
|
62
|
+
</Sidebar.Root>
|
|
63
|
+
<div className={styles.content} />
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
package/dist/skill/responsive.md
CHANGED
|
@@ -119,7 +119,7 @@ const FIELDS: SmartFilterField[] = [
|
|
|
119
119
|
{ key: "city", label: "Город", options: [{ value: "msk", label: "Москва" }] },
|
|
120
120
|
];
|
|
121
121
|
|
|
122
|
-
export function
|
|
122
|
+
export function DealsPage() {
|
|
123
123
|
const [section, setSection] = useState("all");
|
|
124
124
|
const [period, setPeriod] = useState("30d");
|
|
125
125
|
const [filter, setFilter] = useState<SmartFilterValue>({});
|
|
@@ -136,10 +136,10 @@ export function OrdersPage() {
|
|
|
136
136
|
<Page.Root>
|
|
137
137
|
<Page.TopBar>
|
|
138
138
|
<Page.Heading>
|
|
139
|
-
<Page.Title
|
|
140
|
-
<Page.Description>128 в работе · 6
|
|
139
|
+
<Page.Title>Сделки</Page.Title>
|
|
140
|
+
<Page.Description>128 в работе · 6,5 млн ₽</Page.Description>
|
|
141
141
|
</Page.Heading>
|
|
142
|
-
<Page.Tools aria-label="
|
|
142
|
+
<Page.Tools aria-label="Сделки">
|
|
143
143
|
<SegmentedControl.Root value={section} onValueChange={setSection} aria-label="Раздел">
|
|
144
144
|
<SegmentedControl.Item value="all">
|
|
145
145
|
Все<SegmentedControl.Count>128</SegmentedControl.Count>
|
|
@@ -170,14 +170,14 @@ export function OrdersPage() {
|
|
|
170
170
|
<Button.Icon>
|
|
171
171
|
<Icon name="action.add" />
|
|
172
172
|
</Button.Icon>
|
|
173
|
-
|
|
173
|
+
Новая сделка
|
|
174
174
|
</Button.Root>
|
|
175
175
|
</Page.Actions>
|
|
176
176
|
<Page.Chips>
|
|
177
177
|
<SmartFilter.Chips />
|
|
178
178
|
</Page.Chips>
|
|
179
179
|
</Page.TopBar>
|
|
180
|
-
<Page.Body>{/* the
|
|
180
|
+
<Page.Body>{/* the deals as a board or a list: the filter in the bar narrows both views */}</Page.Body>
|
|
181
181
|
</Page.Root>
|
|
182
182
|
</SmartFilter.Root>
|
|
183
183
|
);
|
|
@@ -210,6 +210,28 @@ export function OrdersPage() {
|
|
|
210
210
|
segments' padding, and one whose items all have icons then collapses to icons (each named by its
|
|
211
211
|
tooltip) before it scrolls. On a 320px phone two sections with counts fit a line of their own;
|
|
212
212
|
more, or long labels, scroll — keep the section a person needs first at the start.
|
|
213
|
+
- **Overflow is never fixed by merging.** A tab or a section is one topic; when the topics outgrow
|
|
214
|
+
the line the strip scrolls as above — never two topics joined in one tab («Доступ и история»),
|
|
215
|
+
never the last ones hidden in a «Ещё» of your own, never a second row. Shorten a label to one word
|
|
216
|
+
first; a topic that is rarely opened still keeps its own tab, at the end.
|
|
217
|
+
|
|
218
|
+
```tsx partial
|
|
219
|
+
// bad — the history folded into «Доступ» because five tabs did not fit the drawer
|
|
220
|
+
<Tabs.Item value="access">Доступ и история</Tabs.Item>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```tsx good
|
|
224
|
+
// good — every topic its own tab; the strip scrolls when it runs out of width
|
|
225
|
+
<Tabs.Root defaultValue="profile">
|
|
226
|
+
<Tabs.List>
|
|
227
|
+
<Tabs.Item value="profile">Профиль</Tabs.Item>
|
|
228
|
+
<Tabs.Item value="access">Доступ</Tabs.Item>
|
|
229
|
+
<Tabs.Item value="notifications">Уведомления</Tabs.Item>
|
|
230
|
+
<Tabs.Item value="security">Безопасность</Tabs.Item>
|
|
231
|
+
<Tabs.Item value="history">История</Tabs.Item>
|
|
232
|
+
</Tabs.List>
|
|
233
|
+
</Tabs.Root>
|
|
234
|
+
```
|
|
213
235
|
- Your duty as its host: give the switcher a bounded width. A flex or grid child that holds one sets
|
|
214
236
|
`min-width: 0`, otherwise the switcher grows the row instead of scrolling
|
|
215
237
|
([foundation §9](reference/foundation.md#9-layout-and-responsiveness)).
|
package/dist/skill/ux.md
CHANGED
|
@@ -32,19 +32,55 @@ adds text that explains what the layout failed to show.
|
|
|
32
32
|
| Records of one shape a person reads, sorts, filters | a table | `DataTable` |
|
|
33
33
|
| One value in several units or currencies (a deposit in ₽, $, ฿) | the fields side by side in one row under one heading, the unit in each field | `Input.InlineAffix` |
|
|
34
34
|
| A base value and its overrides (a price and its changes by month) | one list: the base is the first, pinned row | `FieldList` `getLocked` |
|
|
35
|
-
| Small attributes of a record (an icon, a color, a flag) | compact controls in the record's row, not fields of their own | icon-only `Select`, `ColorPresets`, `
|
|
35
|
+
| Small attributes of a record (an icon, a color, a flag) | compact controls in the record's row, not fields of their own | icon-only `Select`, `ColorPresets`, `Switch` |
|
|
36
36
|
| Short texts in an order the person sets | one column, a handle per row | `FieldList` `reorderable` |
|
|
37
37
|
| Settings that most people never touch | behind a disclosure, after the common ones | `Accordion`, a «Дополнительно» section |
|
|
38
38
|
| One of a few options, sent with a form | all visible | `Radio.Group` |
|
|
39
39
|
| One of a few modes of what the screen shows | all visible | `SegmentedControl` |
|
|
40
40
|
| One of many options | a list that opens | `Select` |
|
|
41
41
|
| Many values picked from options | tags | `TagSelect` |
|
|
42
|
+
| A section named after a set of records (sessions, devices, API keys, members) | the records themselves, each with its own actions | `List` with `List.ItemActions` |
|
|
42
43
|
|
|
43
44
|
How many options count as «a few» and the other selection cases: [choosing.md](choosing.md#2-inside-a-category).
|
|
44
45
|
|
|
45
46
|
One record is one row or one block, never half a record per line. If a record does not fit one
|
|
46
47
|
row, it is not a row: open it in a `Drawer`.
|
|
47
48
|
|
|
49
|
+
**A section is what its name says.** A heading that names a set of records — «Сеансы», «Устройства»,
|
|
50
|
+
«Ключи API», «Участники» — promises those records: show them, one row each (where, when, which one is
|
|
51
|
+
this one), with the action on one record in its row and the action on all of them in the section's
|
|
52
|
+
actions. One button standing for the set («Выйти на других устройствах») leaves the person to act on
|
|
53
|
+
records they cannot see. A set that can be empty shows its empty state, never a missing section.
|
|
54
|
+
|
|
55
|
+
```tsx partial
|
|
56
|
+
// bad — «Сеансы» with nothing but a button
|
|
57
|
+
<Drawer.Section label="Сеансы">
|
|
58
|
+
<Button.Root variant="soft" tone="neutral">Выйти на других устройствах</Button.Root>
|
|
59
|
+
</Drawer.Section>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```tsx good
|
|
63
|
+
// good — the sessions themselves, an end per row, the end of all of them beside the heading
|
|
64
|
+
<Drawer.Section label="Сеансы">
|
|
65
|
+
<Drawer.Actions>
|
|
66
|
+
<Button.Root variant="soft" tone="neutral">Завершить остальные</Button.Root>
|
|
67
|
+
</Drawer.Actions>
|
|
68
|
+
<List.Root aria-label="Сеансы">
|
|
69
|
+
<List.Item value="mac">
|
|
70
|
+
<List.ItemText>Chrome · macOS</List.ItemText>
|
|
71
|
+
<List.ItemDescription>Москва · это устройство</List.ItemDescription>
|
|
72
|
+
</List.Item>
|
|
73
|
+
<List.Item value="iphone">
|
|
74
|
+
<List.ItemText>Safari · iPhone</List.ItemText>
|
|
75
|
+
<List.ItemDescription>Казань · вчера в 21:40</List.ItemDescription>
|
|
76
|
+
<List.ItemActions>
|
|
77
|
+
<Button.Root size="s" variant="ghost" tone="neutral">Завершить</Button.Root>
|
|
78
|
+
</List.ItemActions>
|
|
79
|
+
</List.Item>
|
|
80
|
+
</List.Root>
|
|
81
|
+
</Drawer.Section>
|
|
82
|
+
```
|
|
83
|
+
|
|
48
84
|
## 3. Label economy
|
|
49
85
|
|
|
50
86
|
Every word on screen must carry something the person does not see already.
|
|
@@ -60,7 +96,21 @@ Every word on screen must carry something the person does not see already.
|
|
|
60
96
|
beside the number when the row stacks, and a screen reader hears it with the field (the affix
|
|
61
97
|
describes the input; `aria-label="рублей"` on it when the sign reads badly).
|
|
62
98
|
- **No text that explains the layout.** «Двигатель, места и скорость всегда первыми» — the lock on
|
|
63
|
-
those rows says it.
|
|
99
|
+
those rows says it.
|
|
100
|
+
- **A hint is the few words the label and the control leave out** — two to five: a format («ДД.ММ.ГГГГ»),
|
|
101
|
+
a limit («До 10 МБ»), a consequence nobody would expect («Увидит клиент»). Never what the control
|
|
102
|
+
already shows («Применяется сразу» under a Switch, «Время в CRM показывается в этом поясе» under
|
|
103
|
+
«Часовой пояс»), never the obvious outcome of the action or what stays unchanged («пароль
|
|
104
|
+
останется прежним», «история его действий останется»), never the label again in other words. A
|
|
105
|
+
field that needs no hint has none; a hint that needs a sentence is a missing label or a missing
|
|
106
|
+
control.
|
|
107
|
+
|
|
108
|
+
| bad hint | good |
|
|
109
|
+
|---|---|
|
|
110
|
+
| «Часовой пояс» — «Время в CRM показывается в этом поясе» | no hint |
|
|
111
|
+
| «Звук» (Switch) — «Применяется сразу, без сохранения» | no hint |
|
|
112
|
+
| «Отключить доступ» — «Сотрудник не сможет войти, история его действий останется» | «Входы прекратятся» |
|
|
113
|
+
| «Почта для входа» — «На эту почту придёт ссылка для входа в систему» | «Для входа и уведомлений» |
|
|
64
114
|
- **One name per thing.** The same action, entity and status keep one word on the whole screen
|
|
65
115
|
(and across screens): «Счёт», not «Счёт» in the title and «Инвойс» in the button.
|
|
66
116
|
- **Count the labels.** On a form of repeating rows, labels on screen ≤ number of columns + the
|
|
@@ -157,3 +207,48 @@ Code that passes every check can still read badly. Open the screen in a browser
|
|
|
157
207
|
button without an object — remove or rewrite it.
|
|
158
208
|
- **Fill it badly:** the longest real name, an empty list, an error on every field, the slowest
|
|
159
209
|
load. Nothing overflows, jumps or flips.
|
|
210
|
+
|
|
211
|
+
## 10. Where a setting lives
|
|
212
|
+
|
|
213
|
+
A setting lives with what it governs, as a field of the form that already holds its neighbours.
|
|
214
|
+
|
|
215
|
+
- **Group by what it changes, not by how it looks.** A signal (a sound, a desktop banner, an e-mail
|
|
216
|
+
digest) changes how the person is told — it goes to the notifications; the theme, the language and
|
|
217
|
+
the time zone change how the app reads — profile preferences; what one record does goes to that
|
|
218
|
+
record. Ask «what would a person open to change this?» and put it there.
|
|
219
|
+
- **No section for one setting.** A section with a single field joins the closest section; a new
|
|
220
|
+
section starts at three related fields.
|
|
221
|
+
- **A stored preference is a field.** A setting with options (the theme: «Системная», «Светлая»,
|
|
222
|
+
«Тёмная») is a `Select` beside its related fields in the profile form — never a `SegmentedControl`,
|
|
223
|
+
which switches what a screen shows right now ([a flag, a level, a set](choosing.md#a-flag-a-level-a-set)).
|
|
224
|
+
|
|
225
|
+
```tsx partial
|
|
226
|
+
// bad — a section «Оформление» for the theme as a switch, and the dialog sounds next to it
|
|
227
|
+
<Drawer.Section label="Оформление">
|
|
228
|
+
<SegmentedControl.Root label="Тема" defaultValue="system">…</SegmentedControl.Root>
|
|
229
|
+
<Switch.Root><Switch.Label>Звуки диалогов</Switch.Label></Switch.Root>
|
|
230
|
+
</Drawer.Section>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```tsx good
|
|
234
|
+
// good — the theme is one field of the profile, the sounds live with the notifications
|
|
235
|
+
<>
|
|
236
|
+
<Drawer.Section label="Профиль">
|
|
237
|
+
<Select.Root label="Тема" defaultValue="system">
|
|
238
|
+
<Select.Trigger>
|
|
239
|
+
<Select.Value />
|
|
240
|
+
</Select.Trigger>
|
|
241
|
+
<Select.Content>
|
|
242
|
+
<Select.Item value="system">Системная</Select.Item>
|
|
243
|
+
<Select.Item value="light">Светлая</Select.Item>
|
|
244
|
+
<Select.Item value="dark">Тёмная</Select.Item>
|
|
245
|
+
</Select.Content>
|
|
246
|
+
</Select.Root>
|
|
247
|
+
</Drawer.Section>
|
|
248
|
+
<Drawer.Section label="Уведомления">
|
|
249
|
+
<Switch.Root name="dialogSounds">
|
|
250
|
+
<Switch.Label>Звуки диалогов</Switch.Label>
|
|
251
|
+
</Switch.Root>
|
|
252
|
+
</Drawer.Section>
|
|
253
|
+
</>
|
|
254
|
+
```
|
|
@@ -117,6 +117,7 @@ export declare const semanticTokens: {
|
|
|
117
117
|
readonly fg: "{color.white}";
|
|
118
118
|
readonly soft: "{color.green.100}";
|
|
119
119
|
readonly text: "{color.green.700}";
|
|
120
|
+
readonly border: "{color.green.500}";
|
|
120
121
|
};
|
|
121
122
|
readonly warning: {
|
|
122
123
|
readonly default: "{color.orange.700}";
|
|
@@ -410,7 +411,7 @@ export declare const semanticTokens: {
|
|
|
410
411
|
/** State that glides into place (thumbs, indicators, checkmarks); pair with `base`. */
|
|
411
412
|
readonly emphasized: "{easing.emphasized}";
|
|
412
413
|
};
|
|
413
|
-
/** Step between items of a staggered
|
|
414
|
+
/** Step between items of a staggered entrance of parts that appear after their page is shown. */
|
|
414
415
|
readonly stagger: "{duration.80}";
|
|
415
416
|
/**
|
|
416
417
|
* Period unit of a loading loop (Spinner turns once in 3 units, 6 under reduced motion). A
|
|
@@ -60,6 +60,7 @@ export declare const darkThemeOverrides: {
|
|
|
60
60
|
readonly fg: "{color.gray.950}";
|
|
61
61
|
readonly soft: "color-mix(in srgb, {color.green.500} 16%, transparent)";
|
|
62
62
|
readonly text: "{color.green.300}";
|
|
63
|
+
readonly border: "{color.green.400}";
|
|
63
64
|
};
|
|
64
65
|
readonly warning: {
|
|
65
66
|
readonly default: "{color.orange.400}";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prime-ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"packageManager": "bun@1.3.13",
|
|
5
5
|
"description": "React 19 UI kit for product interfaces: 79 composable components on one design contract — forms, selects, tables, overlays, navigation, app shell. CSS Modules, --prime-* tokens, light and dark themes, TypeScript, a11y, Next.js App Router ready. Ships COMPONENT.md docs, copyable examples and an agent skill.",
|
|
6
6
|
"bin": {
|
|
@@ -121,6 +121,7 @@ The soft fill is opaque: the soft hue is composited over the ground the avatar s
|
|
|
121
121
|
- Avatar → name: space step 3 (12px) in lists and cards, step 2 (8px) in table cells.
|
|
122
122
|
- Group: members overlap by an eighth of the diameter (initials are never cut) with a 2px ring in the ground they stand on: the current layer of the surface around them (no surface sets anything), or `--avatar-ring` from a host whose ground is not a layer (a reaction chip sets its own fill); the fills of initials and «+N» are composited over the same ground.
|
|
123
123
|
- An icon fallback is half the diameter: a kit `Icon` reads `--prime-icon-size`, a raw svg too.
|
|
124
|
+
- In a leading media slot (`Drawer.Icon`, `Modal.Icon`, `Sheet.Icon`, `Card.Icon`) the avatar takes the slot's size (`l`, 40px) whatever `size` it was given, also through a wrapper component of yours; the slot then draws no tile.
|
|
124
125
|
|
|
125
126
|
## Accessibility
|
|
126
127
|
|